]> git.proxmox.com Git - pve-storage.git/commitdiff
separate packaging and source build system
authorThomas Lamprecht <t.lamprecht@proxmox.com>
Wed, 24 May 2023 14:20:27 +0000 (16:20 +0200)
committerThomas Lamprecht <t.lamprecht@proxmox.com>
Wed, 24 May 2023 14:20:27 +0000 (16:20 +0200)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
423 files changed:
Makefile
PVE/API2/Disks.pm [deleted file]
PVE/API2/Disks/Directory.pm [deleted file]
PVE/API2/Disks/LVM.pm [deleted file]
PVE/API2/Disks/LVMThin.pm [deleted file]
PVE/API2/Disks/Makefile [deleted file]
PVE/API2/Disks/ZFS.pm [deleted file]
PVE/API2/Makefile [deleted file]
PVE/API2/Storage/Config.pm [deleted file]
PVE/API2/Storage/Content.pm [deleted file]
PVE/API2/Storage/FileRestore.pm [deleted file]
PVE/API2/Storage/Makefile [deleted file]
PVE/API2/Storage/PruneBackups.pm [deleted file]
PVE/API2/Storage/Scan.pm [deleted file]
PVE/API2/Storage/Status.pm [deleted file]
PVE/CLI/Makefile [deleted file]
PVE/CLI/pvesm.pm [deleted file]
PVE/CephConfig.pm [deleted file]
PVE/Diskmanage.pm [deleted file]
PVE/Makefile [deleted file]
PVE/Storage.pm [deleted file]
PVE/Storage/BTRFSPlugin.pm [deleted file]
PVE/Storage/CIFSPlugin.pm [deleted file]
PVE/Storage/CephFSPlugin.pm [deleted file]
PVE/Storage/DirPlugin.pm [deleted file]
PVE/Storage/GlusterfsPlugin.pm [deleted file]
PVE/Storage/ISCSIDirectPlugin.pm [deleted file]
PVE/Storage/ISCSIPlugin.pm [deleted file]
PVE/Storage/LVMPlugin.pm [deleted file]
PVE/Storage/LunCmd/Comstar.pm [deleted file]
PVE/Storage/LunCmd/Iet.pm [deleted file]
PVE/Storage/LunCmd/Istgt.pm [deleted file]
PVE/Storage/LunCmd/LIO.pm [deleted file]
PVE/Storage/LunCmd/Makefile [deleted file]
PVE/Storage/LvmThinPlugin.pm [deleted file]
PVE/Storage/Makefile [deleted file]
PVE/Storage/NFSPlugin.pm [deleted file]
PVE/Storage/PBSPlugin.pm [deleted file]
PVE/Storage/Plugin.pm [deleted file]
PVE/Storage/RBDPlugin.pm [deleted file]
PVE/Storage/ZFSPlugin.pm [deleted file]
PVE/Storage/ZFSPoolPlugin.pm [deleted file]
pvesm [deleted file]
src/Makefile [new file with mode: 0644]
src/PVE/API2/Disks.pm [new file with mode: 0644]
src/PVE/API2/Disks/Directory.pm [new file with mode: 0644]
src/PVE/API2/Disks/LVM.pm [new file with mode: 0644]
src/PVE/API2/Disks/LVMThin.pm [new file with mode: 0644]
src/PVE/API2/Disks/Makefile [new file with mode: 0644]
src/PVE/API2/Disks/ZFS.pm [new file with mode: 0644]
src/PVE/API2/Makefile [new file with mode: 0644]
src/PVE/API2/Storage/Config.pm [new file with mode: 0755]
src/PVE/API2/Storage/Content.pm [new file with mode: 0644]
src/PVE/API2/Storage/FileRestore.pm [new file with mode: 0644]
src/PVE/API2/Storage/Makefile [new file with mode: 0644]
src/PVE/API2/Storage/PruneBackups.pm [new file with mode: 0644]
src/PVE/API2/Storage/Scan.pm [new file with mode: 0644]
src/PVE/API2/Storage/Status.pm [new file with mode: 0644]
src/PVE/CLI/Makefile [new file with mode: 0644]
src/PVE/CLI/pvesm.pm [new file with mode: 0755]
src/PVE/CephConfig.pm [new file with mode: 0644]
src/PVE/Diskmanage.pm [new file with mode: 0644]
src/PVE/Makefile [new file with mode: 0644]
src/PVE/Storage.pm [new file with mode: 0755]
src/PVE/Storage/BTRFSPlugin.pm [new file with mode: 0644]
src/PVE/Storage/CIFSPlugin.pm [new file with mode: 0644]
src/PVE/Storage/CephFSPlugin.pm [new file with mode: 0644]
src/PVE/Storage/DirPlugin.pm [new file with mode: 0644]
src/PVE/Storage/GlusterfsPlugin.pm [new file with mode: 0644]
src/PVE/Storage/ISCSIDirectPlugin.pm [new file with mode: 0644]
src/PVE/Storage/ISCSIPlugin.pm [new file with mode: 0644]
src/PVE/Storage/LVMPlugin.pm [new file with mode: 0644]
src/PVE/Storage/LunCmd/Comstar.pm [new file with mode: 0644]
src/PVE/Storage/LunCmd/Iet.pm [new file with mode: 0644]
src/PVE/Storage/LunCmd/Istgt.pm [new file with mode: 0644]
src/PVE/Storage/LunCmd/LIO.pm [new file with mode: 0644]
src/PVE/Storage/LunCmd/Makefile [new file with mode: 0644]
src/PVE/Storage/LvmThinPlugin.pm [new file with mode: 0644]
src/PVE/Storage/Makefile [new file with mode: 0644]
src/PVE/Storage/NFSPlugin.pm [new file with mode: 0644]
src/PVE/Storage/PBSPlugin.pm [new file with mode: 0644]
src/PVE/Storage/Plugin.pm [new file with mode: 0644]
src/PVE/Storage/RBDPlugin.pm [new file with mode: 0644]
src/PVE/Storage/ZFSPlugin.pm [new file with mode: 0644]
src/PVE/Storage/ZFSPoolPlugin.pm [new file with mode: 0644]
src/bin/Makefile [new file with mode: 0644]
src/bin/pvesm [new file with mode: 0755]
src/test/Makefile [new file with mode: 0644]
src/test/archive_info_test.pm [new file with mode: 0644]
src/test/disk_tests/cciss/cciss!c0d0/device/model [new file with mode: 0644]
src/test/disk_tests/cciss/cciss!c0d0/device/vendor [new file with mode: 0644]
src/test/disk_tests/cciss/cciss!c0d0/queue/rotational [new file with mode: 0644]
src/test/disk_tests/cciss/cciss!c0d0/size [new file with mode: 0644]
src/test/disk_tests/cciss/cciss!c0d0_udevadm [new file with mode: 0644]
src/test/disk_tests/cciss/disklist [new file with mode: 0644]
src/test/disk_tests/cciss/disklist_expected.json [new file with mode: 0644]
src/test/disk_tests/hdd_smart/disklist [new file with mode: 0644]
src/test/disk_tests/hdd_smart/disklist_expected.json [new file with mode: 0644]
src/test/disk_tests/hdd_smart/sda/device/vendor [new file with mode: 0644]
src/test/disk_tests/hdd_smart/sda/queue/rotational [new file with mode: 0644]
src/test/disk_tests/hdd_smart/sda/size [new file with mode: 0644]
src/test/disk_tests/hdd_smart/sda_health [new file with mode: 0644]
src/test/disk_tests/hdd_smart/sda_smart [new file with mode: 0644]
src/test/disk_tests/hdd_smart/sda_smart_expected.json [new file with mode: 0644]
src/test/disk_tests/hdd_smart/sda_udevadm [new file with mode: 0644]
src/test/disk_tests/hdd_smart/sdb/device/vendor [new file with mode: 0644]
src/test/disk_tests/hdd_smart/sdb/queue/rotational [new file with mode: 0644]
src/test/disk_tests/hdd_smart/sdb/size [new file with mode: 0644]
src/test/disk_tests/hdd_smart/sdb_health [new file with mode: 0644]
src/test/disk_tests/hdd_smart/sdb_smart [new file with mode: 0644]
src/test/disk_tests/hdd_smart/sdb_smart_expected.json [new file with mode: 0644]
src/test/disk_tests/hdd_smart/sdb_udevadm [new file with mode: 0644]
src/test/disk_tests/nvme_smart/disklist [new file with mode: 0644]
src/test/disk_tests/nvme_smart/disklist_expected.json [new file with mode: 0644]
src/test/disk_tests/nvme_smart/nvme0n1/device/model [new file with mode: 0644]
src/test/disk_tests/nvme_smart/nvme0n1/queue/rotational [new file with mode: 0644]
src/test/disk_tests/nvme_smart/nvme0n1/size [new file with mode: 0644]
src/test/disk_tests/nvme_smart/nvme0n1_smart [new file with mode: 0644]
src/test/disk_tests/nvme_smart/nvme0n1_smart_expected.json [new file with mode: 0644]
src/test/disk_tests/nvme_smart/nvme0n1_udevadm [new file with mode: 0644]
src/test/disk_tests/sas/disklist [new file with mode: 0644]
src/test/disk_tests/sas/disklist_expected.json [new file with mode: 0644]
src/test/disk_tests/sas/sda/device/model [new file with mode: 0644]
src/test/disk_tests/sas/sda/device/vendor [new file with mode: 0644]
src/test/disk_tests/sas/sda/queue/rotational [new file with mode: 0644]
src/test/disk_tests/sas/sda/size [new file with mode: 0644]
src/test/disk_tests/sas/sda_smart [new file with mode: 0644]
src/test/disk_tests/sas/sda_smart_expected.json [new file with mode: 0644]
src/test/disk_tests/sas/sda_udevadm [new file with mode: 0644]
src/test/disk_tests/sas_ssd/disklist [new file with mode: 0644]
src/test/disk_tests/sas_ssd/disklist_expected.json [new file with mode: 0644]
src/test/disk_tests/sas_ssd/sda/device/model [new file with mode: 0644]
src/test/disk_tests/sas_ssd/sda/device/vendor [new file with mode: 0644]
src/test/disk_tests/sas_ssd/sda/queue/rotational [new file with mode: 0644]
src/test/disk_tests/sas_ssd/sda/size [new file with mode: 0644]
src/test/disk_tests/sas_ssd/sda_smart [new file with mode: 0644]
src/test/disk_tests/sas_ssd/sda_smart_expected.json [new file with mode: 0644]
src/test/disk_tests/sas_ssd/sda_udevadm [new file with mode: 0644]
src/test/disk_tests/ssd_smart/disklist [new file with mode: 0644]
src/test/disk_tests/ssd_smart/disklist_expected.json [new file with mode: 0644]
src/test/disk_tests/ssd_smart/sda/device/vendor [new file with mode: 0644]
src/test/disk_tests/ssd_smart/sda/queue/rotational [new file with mode: 0644]
src/test/disk_tests/ssd_smart/sda/size [new file with mode: 0644]
src/test/disk_tests/ssd_smart/sda_smart [new file with mode: 0644]
src/test/disk_tests/ssd_smart/sda_smart_expected.json [new file with mode: 0644]
src/test/disk_tests/ssd_smart/sda_udevadm [new file with mode: 0644]
src/test/disk_tests/ssd_smart/sdb/device/vendor [new file with mode: 0644]
src/test/disk_tests/ssd_smart/sdb/queue/rotational [new file with mode: 0644]
src/test/disk_tests/ssd_smart/sdb/size [new file with mode: 0644]
src/test/disk_tests/ssd_smart/sdb_smart [new file with mode: 0644]
src/test/disk_tests/ssd_smart/sdb_smart_expected.json [new file with mode: 0644]
src/test/disk_tests/ssd_smart/sdb_udevadm [new file with mode: 0644]
src/test/disk_tests/ssd_smart/sdc/device/vendor [new file with mode: 0644]
src/test/disk_tests/ssd_smart/sdc/queue/rotational [new file with mode: 0644]
src/test/disk_tests/ssd_smart/sdc/size [new file with mode: 0644]
src/test/disk_tests/ssd_smart/sdc_smart [new file with mode: 0644]
src/test/disk_tests/ssd_smart/sdc_smart_expected.json [new file with mode: 0644]
src/test/disk_tests/ssd_smart/sdc_udevadm [new file with mode: 0644]
src/test/disk_tests/ssd_smart/sdd/device/vendor [new file with mode: 0644]
src/test/disk_tests/ssd_smart/sdd/queue/rotational [new file with mode: 0644]
src/test/disk_tests/ssd_smart/sdd/size [new file with mode: 0644]
src/test/disk_tests/ssd_smart/sdd_smart [new file with mode: 0644]
src/test/disk_tests/ssd_smart/sdd_smart_expected.json [new file with mode: 0644]
src/test/disk_tests/ssd_smart/sdd_udevadm [new file with mode: 0644]
src/test/disk_tests/ssd_smart/sde/device/vendor [new file with mode: 0644]
src/test/disk_tests/ssd_smart/sde/queue/rotational [new file with mode: 0644]
src/test/disk_tests/ssd_smart/sde/size [new file with mode: 0644]
src/test/disk_tests/ssd_smart/sde_smart [new file with mode: 0644]
src/test/disk_tests/ssd_smart/sde_smart_expected.json [new file with mode: 0644]
src/test/disk_tests/ssd_smart/sde_udevadm [new file with mode: 0644]
src/test/disk_tests/usages/disklist [new file with mode: 0644]
src/test/disk_tests/usages/disklist_expected.json [new file with mode: 0644]
src/test/disk_tests/usages/lsblk [new file with mode: 0644]
src/test/disk_tests/usages/lvs [new file with mode: 0644]
src/test/disk_tests/usages/mounts [new file with mode: 0644]
src/test/disk_tests/usages/partlist [new file with mode: 0644]
src/test/disk_tests/usages/pvs [new file with mode: 0644]
src/test/disk_tests/usages/sda/device/vendor [new file with mode: 0644]
src/test/disk_tests/usages/sda/queue/rotational [new file with mode: 0644]
src/test/disk_tests/usages/sda/size [new file with mode: 0644]
src/test/disk_tests/usages/sda_udevadm [new file with mode: 0644]
src/test/disk_tests/usages/sdb/device/vendor [new file with mode: 0644]
src/test/disk_tests/usages/sdb/queue/rotational [new file with mode: 0644]
src/test/disk_tests/usages/sdb/size [new file with mode: 0644]
src/test/disk_tests/usages/sdb_udevadm [new file with mode: 0644]
src/test/disk_tests/usages/sdc/device/vendor [new file with mode: 0644]
src/test/disk_tests/usages/sdc/queue/rotational [new file with mode: 0644]
src/test/disk_tests/usages/sdc/size [new file with mode: 0644]
src/test/disk_tests/usages/sdc_udevadm [new file with mode: 0644]
src/test/disk_tests/usages/sdd/device/vendor [new file with mode: 0644]
src/test/disk_tests/usages/sdd/queue/rotational [new file with mode: 0644]
src/test/disk_tests/usages/sdd/sdd1/size [new file with mode: 0644]
src/test/disk_tests/usages/sdd/sdd2/size [new file with mode: 0644]
src/test/disk_tests/usages/sdd/size [new file with mode: 0644]
src/test/disk_tests/usages/sdd_udevadm [new file with mode: 0644]
src/test/disk_tests/usages/sde/device/vendor [new file with mode: 0644]
src/test/disk_tests/usages/sde/queue/rotational [new file with mode: 0644]
src/test/disk_tests/usages/sde/sde1/size [new file with mode: 0644]
src/test/disk_tests/usages/sde/size [new file with mode: 0644]
src/test/disk_tests/usages/sde_udevadm [new file with mode: 0644]
src/test/disk_tests/usages/sdf/device/vendor [new file with mode: 0644]
src/test/disk_tests/usages/sdf/queue/rotational [new file with mode: 0644]
src/test/disk_tests/usages/sdf/sdf1/size [new file with mode: 0644]
src/test/disk_tests/usages/sdf/size [new file with mode: 0644]
src/test/disk_tests/usages/sdf_udevadm [new file with mode: 0644]
src/test/disk_tests/usages/sdg/device/vendor [new file with mode: 0644]
src/test/disk_tests/usages/sdg/queue/rotational [new file with mode: 0644]
src/test/disk_tests/usages/sdg/size [new file with mode: 0644]
src/test/disk_tests/usages/sdg_udevadm [new file with mode: 0644]
src/test/disk_tests/usages/sdh/device/vendor [new file with mode: 0644]
src/test/disk_tests/usages/sdh/queue/rotational [new file with mode: 0644]
src/test/disk_tests/usages/sdh/size [new file with mode: 0644]
src/test/disk_tests/usages/sdh_udevadm [new file with mode: 0644]
src/test/disk_tests/usages/sdi/device/vendor [new file with mode: 0644]
src/test/disk_tests/usages/sdi/queue/rotational [new file with mode: 0644]
src/test/disk_tests/usages/sdi/size [new file with mode: 0644]
src/test/disk_tests/usages/sdi_udevadm [new file with mode: 0644]
src/test/disk_tests/usages/sdj/device/vendor [new file with mode: 0644]
src/test/disk_tests/usages/sdj/queue/rotational [new file with mode: 0644]
src/test/disk_tests/usages/sdj/size [new file with mode: 0644]
src/test/disk_tests/usages/sdj_udevadm [new file with mode: 0644]
src/test/disk_tests/usages/sdk/device/vendor [new file with mode: 0644]
src/test/disk_tests/usages/sdk/queue/rotational [new file with mode: 0644]
src/test/disk_tests/usages/sdk/size [new file with mode: 0644]
src/test/disk_tests/usages/sdk_udevadm [new file with mode: 0644]
src/test/disk_tests/usages/sdl/device/vendor [new file with mode: 0644]
src/test/disk_tests/usages/sdl/queue/rotational [new file with mode: 0644]
src/test/disk_tests/usages/sdl/size [new file with mode: 0644]
src/test/disk_tests/usages/sdl_udevadm [new file with mode: 0644]
src/test/disk_tests/usages/sdm/device/vendor [new file with mode: 0644]
src/test/disk_tests/usages/sdm/queue/rotational [new file with mode: 0644]
src/test/disk_tests/usages/sdm/sdm1/size [new file with mode: 0644]
src/test/disk_tests/usages/sdm/sdm9/size [new file with mode: 0644]
src/test/disk_tests/usages/sdm/size [new file with mode: 0644]
src/test/disk_tests/usages/sdm_udevadm [new file with mode: 0644]
src/test/disk_tests/usages/sdn/device/vendor [new file with mode: 0644]
src/test/disk_tests/usages/sdn/queue/rotational [new file with mode: 0644]
src/test/disk_tests/usages/sdn/size [new file with mode: 0644]
src/test/disk_tests/usages/sdn_udevadm [new file with mode: 0644]
src/test/disk_tests/usages/zpool [new file with mode: 0644]
src/test/disklist_test.pm [new file with mode: 0644]
src/test/filesystem_path_test.pm [new file with mode: 0644]
src/test/get_subdir_test.pm [new file with mode: 0644]
src/test/list_volumes_test.pm [new file with mode: 0644]
src/test/parse_volname_test.pm [new file with mode: 0644]
src/test/path_to_volume_id_test.pm [new file with mode: 0644]
src/test/prune_backups_test.pm [new file with mode: 0644]
src/test/rbd_namespace.pl [new file with mode: 0755]
src/test/run_bwlimit_tests.pl [new file with mode: 0755]
src/test/run_disk_tests.pl [new file with mode: 0755]
src/test/run_plugin_tests.pl [new file with mode: 0755]
src/test/run_test_zfspoolplugin.pl [new file with mode: 0755]
src/udev-rbd/50-rbd-pve.rules [new file with mode: 0644]
src/udev-rbd/Makefile [new file with mode: 0644]
src/udev-rbd/ceph-rbdnamer-pve [new file with mode: 0755]
test/Makefile [deleted file]
test/archive_info_test.pm [deleted file]
test/disk_tests/cciss/cciss!c0d0/device/model [deleted file]
test/disk_tests/cciss/cciss!c0d0/device/vendor [deleted file]
test/disk_tests/cciss/cciss!c0d0/queue/rotational [deleted file]
test/disk_tests/cciss/cciss!c0d0/size [deleted file]
test/disk_tests/cciss/cciss!c0d0_udevadm [deleted file]
test/disk_tests/cciss/disklist [deleted file]
test/disk_tests/cciss/disklist_expected.json [deleted file]
test/disk_tests/hdd_smart/disklist [deleted file]
test/disk_tests/hdd_smart/disklist_expected.json [deleted file]
test/disk_tests/hdd_smart/sda/device/vendor [deleted file]
test/disk_tests/hdd_smart/sda/queue/rotational [deleted file]
test/disk_tests/hdd_smart/sda/size [deleted file]
test/disk_tests/hdd_smart/sda_health [deleted file]
test/disk_tests/hdd_smart/sda_smart [deleted file]
test/disk_tests/hdd_smart/sda_smart_expected.json [deleted file]
test/disk_tests/hdd_smart/sda_udevadm [deleted file]
test/disk_tests/hdd_smart/sdb/device/vendor [deleted file]
test/disk_tests/hdd_smart/sdb/queue/rotational [deleted file]
test/disk_tests/hdd_smart/sdb/size [deleted file]
test/disk_tests/hdd_smart/sdb_health [deleted file]
test/disk_tests/hdd_smart/sdb_smart [deleted file]
test/disk_tests/hdd_smart/sdb_smart_expected.json [deleted file]
test/disk_tests/hdd_smart/sdb_udevadm [deleted file]
test/disk_tests/nvme_smart/disklist [deleted file]
test/disk_tests/nvme_smart/disklist_expected.json [deleted file]
test/disk_tests/nvme_smart/nvme0n1/device/model [deleted file]
test/disk_tests/nvme_smart/nvme0n1/queue/rotational [deleted file]
test/disk_tests/nvme_smart/nvme0n1/size [deleted file]
test/disk_tests/nvme_smart/nvme0n1_smart [deleted file]
test/disk_tests/nvme_smart/nvme0n1_smart_expected.json [deleted file]
test/disk_tests/nvme_smart/nvme0n1_udevadm [deleted file]
test/disk_tests/sas/disklist [deleted file]
test/disk_tests/sas/disklist_expected.json [deleted file]
test/disk_tests/sas/sda/device/model [deleted file]
test/disk_tests/sas/sda/device/vendor [deleted file]
test/disk_tests/sas/sda/queue/rotational [deleted file]
test/disk_tests/sas/sda/size [deleted file]
test/disk_tests/sas/sda_smart [deleted file]
test/disk_tests/sas/sda_smart_expected.json [deleted file]
test/disk_tests/sas/sda_udevadm [deleted file]
test/disk_tests/sas_ssd/disklist [deleted file]
test/disk_tests/sas_ssd/disklist_expected.json [deleted file]
test/disk_tests/sas_ssd/sda/device/model [deleted file]
test/disk_tests/sas_ssd/sda/device/vendor [deleted file]
test/disk_tests/sas_ssd/sda/queue/rotational [deleted file]
test/disk_tests/sas_ssd/sda/size [deleted file]
test/disk_tests/sas_ssd/sda_smart [deleted file]
test/disk_tests/sas_ssd/sda_smart_expected.json [deleted file]
test/disk_tests/sas_ssd/sda_udevadm [deleted file]
test/disk_tests/ssd_smart/disklist [deleted file]
test/disk_tests/ssd_smart/disklist_expected.json [deleted file]
test/disk_tests/ssd_smart/sda/device/vendor [deleted file]
test/disk_tests/ssd_smart/sda/queue/rotational [deleted file]
test/disk_tests/ssd_smart/sda/size [deleted file]
test/disk_tests/ssd_smart/sda_smart [deleted file]
test/disk_tests/ssd_smart/sda_smart_expected.json [deleted file]
test/disk_tests/ssd_smart/sda_udevadm [deleted file]
test/disk_tests/ssd_smart/sdb/device/vendor [deleted file]
test/disk_tests/ssd_smart/sdb/queue/rotational [deleted file]
test/disk_tests/ssd_smart/sdb/size [deleted file]
test/disk_tests/ssd_smart/sdb_smart [deleted file]
test/disk_tests/ssd_smart/sdb_smart_expected.json [deleted file]
test/disk_tests/ssd_smart/sdb_udevadm [deleted file]
test/disk_tests/ssd_smart/sdc/device/vendor [deleted file]
test/disk_tests/ssd_smart/sdc/queue/rotational [deleted file]
test/disk_tests/ssd_smart/sdc/size [deleted file]
test/disk_tests/ssd_smart/sdc_smart [deleted file]
test/disk_tests/ssd_smart/sdc_smart_expected.json [deleted file]
test/disk_tests/ssd_smart/sdc_udevadm [deleted file]
test/disk_tests/ssd_smart/sdd/device/vendor [deleted file]
test/disk_tests/ssd_smart/sdd/queue/rotational [deleted file]
test/disk_tests/ssd_smart/sdd/size [deleted file]
test/disk_tests/ssd_smart/sdd_smart [deleted file]
test/disk_tests/ssd_smart/sdd_smart_expected.json [deleted file]
test/disk_tests/ssd_smart/sdd_udevadm [deleted file]
test/disk_tests/ssd_smart/sde/device/vendor [deleted file]
test/disk_tests/ssd_smart/sde/queue/rotational [deleted file]
test/disk_tests/ssd_smart/sde/size [deleted file]
test/disk_tests/ssd_smart/sde_smart [deleted file]
test/disk_tests/ssd_smart/sde_smart_expected.json [deleted file]
test/disk_tests/ssd_smart/sde_udevadm [deleted file]
test/disk_tests/usages/disklist [deleted file]
test/disk_tests/usages/disklist_expected.json [deleted file]
test/disk_tests/usages/lsblk [deleted file]
test/disk_tests/usages/lvs [deleted file]
test/disk_tests/usages/mounts [deleted file]
test/disk_tests/usages/partlist [deleted file]
test/disk_tests/usages/pvs [deleted file]
test/disk_tests/usages/sda/device/vendor [deleted file]
test/disk_tests/usages/sda/queue/rotational [deleted file]
test/disk_tests/usages/sda/size [deleted file]
test/disk_tests/usages/sda_udevadm [deleted file]
test/disk_tests/usages/sdb/device/vendor [deleted file]
test/disk_tests/usages/sdb/queue/rotational [deleted file]
test/disk_tests/usages/sdb/size [deleted file]
test/disk_tests/usages/sdb_udevadm [deleted file]
test/disk_tests/usages/sdc/device/vendor [deleted file]
test/disk_tests/usages/sdc/queue/rotational [deleted file]
test/disk_tests/usages/sdc/size [deleted file]
test/disk_tests/usages/sdc_udevadm [deleted file]
test/disk_tests/usages/sdd/device/vendor [deleted file]
test/disk_tests/usages/sdd/queue/rotational [deleted file]
test/disk_tests/usages/sdd/sdd1/size [deleted file]
test/disk_tests/usages/sdd/sdd2/size [deleted file]
test/disk_tests/usages/sdd/size [deleted file]
test/disk_tests/usages/sdd_udevadm [deleted file]
test/disk_tests/usages/sde/device/vendor [deleted file]
test/disk_tests/usages/sde/queue/rotational [deleted file]
test/disk_tests/usages/sde/sde1/size [deleted file]
test/disk_tests/usages/sde/size [deleted file]
test/disk_tests/usages/sde_udevadm [deleted file]
test/disk_tests/usages/sdf/device/vendor [deleted file]
test/disk_tests/usages/sdf/queue/rotational [deleted file]
test/disk_tests/usages/sdf/sdf1/size [deleted file]
test/disk_tests/usages/sdf/size [deleted file]
test/disk_tests/usages/sdf_udevadm [deleted file]
test/disk_tests/usages/sdg/device/vendor [deleted file]
test/disk_tests/usages/sdg/queue/rotational [deleted file]
test/disk_tests/usages/sdg/size [deleted file]
test/disk_tests/usages/sdg_udevadm [deleted file]
test/disk_tests/usages/sdh/device/vendor [deleted file]
test/disk_tests/usages/sdh/queue/rotational [deleted file]
test/disk_tests/usages/sdh/size [deleted file]
test/disk_tests/usages/sdh_udevadm [deleted file]
test/disk_tests/usages/sdi/device/vendor [deleted file]
test/disk_tests/usages/sdi/queue/rotational [deleted file]
test/disk_tests/usages/sdi/size [deleted file]
test/disk_tests/usages/sdi_udevadm [deleted file]
test/disk_tests/usages/sdj/device/vendor [deleted file]
test/disk_tests/usages/sdj/queue/rotational [deleted file]
test/disk_tests/usages/sdj/size [deleted file]
test/disk_tests/usages/sdj_udevadm [deleted file]
test/disk_tests/usages/sdk/device/vendor [deleted file]
test/disk_tests/usages/sdk/queue/rotational [deleted file]
test/disk_tests/usages/sdk/size [deleted file]
test/disk_tests/usages/sdk_udevadm [deleted file]
test/disk_tests/usages/sdl/device/vendor [deleted file]
test/disk_tests/usages/sdl/queue/rotational [deleted file]
test/disk_tests/usages/sdl/size [deleted file]
test/disk_tests/usages/sdl_udevadm [deleted file]
test/disk_tests/usages/sdm/device/vendor [deleted file]
test/disk_tests/usages/sdm/queue/rotational [deleted file]
test/disk_tests/usages/sdm/sdm1/size [deleted file]
test/disk_tests/usages/sdm/sdm9/size [deleted file]
test/disk_tests/usages/sdm/size [deleted file]
test/disk_tests/usages/sdm_udevadm [deleted file]
test/disk_tests/usages/sdn/device/vendor [deleted file]
test/disk_tests/usages/sdn/queue/rotational [deleted file]
test/disk_tests/usages/sdn/size [deleted file]
test/disk_tests/usages/sdn_udevadm [deleted file]
test/disk_tests/usages/zpool [deleted file]
test/disklist_test.pm [deleted file]
test/filesystem_path_test.pm [deleted file]
test/get_subdir_test.pm [deleted file]
test/list_volumes_test.pm [deleted file]
test/parse_volname_test.pm [deleted file]
test/path_to_volume_id_test.pm [deleted file]
test/prune_backups_test.pm [deleted file]
test/rbd_namespace.pl [deleted file]
test/run_bwlimit_tests.pl [deleted file]
test/run_disk_tests.pl [deleted file]
test/run_plugin_tests.pl [deleted file]
test/run_test_zfspoolplugin.pl [deleted file]
udev-rbd/50-rbd-pve.rules [deleted file]
udev-rbd/Makefile [deleted file]
udev-rbd/ceph-rbdnamer-pve [deleted file]

index 04be6954f21ac509905b8a6c1ae301bca81f2d6b..9a2230474a35f1424c901f82b185b761e025d972 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -2,73 +2,31 @@ include /usr/share/dpkg/pkg-info.mk
 
 PACKAGE=libpve-storage-perl
 
-DESTDIR=
-PREFIX=/usr
-BINDIR=$(PREFIX)/bin
-SBINDIR=$(PREFIX)/sbin
-MANDIR=$(PREFIX)/share/man
-DOCDIR=$(PREFIX)/share/doc/$(PACKAGE)
-MAN1DIR=$(MANDIR)/man1/
-BASHCOMPLDIR=$(PREFIX)/share/bash-completion/completions/
-ZSHCOMPLDIR=$(PREFIX)/share/zsh/vendor-completions/
-
-export PERLDIR=$(PREFIX)/share/perl5
-
 GITVERSION:=$(shell git rev-parse HEAD)
 
 DEB=$(PACKAGE)_$(DEB_VERSION_UPSTREAM_REVISION)_all.deb
 
--include /usr/share/pve-doc-generator/pve-doc-generator.mk
-
 all:
 
 .PHONY: dinstall
 dinstall: deb
        dpkg -i $(DEB)
 
-pvesm.bash-completion:
-       perl -I. -T -e "use PVE::CLI::pvesm; PVE::CLI::pvesm->generate_bash_completions();" >$@.tmp
-       mv $@.tmp $@
-
-pvesm.zsh-completion:
-       perl -I. -T -e "use PVE::CLI::pvesm; PVE::CLI::pvesm->generate_zsh_completions();" >$@.tmp
-       mv $@.tmp $@
-
-.PHONY: install
-install: PVE pvesm.1 pvesm.bash-completion pvesm.zsh-completion
-       install -d $(DESTDIR)$(SBINDIR)
-       install -m 0755 pvesm $(DESTDIR)$(SBINDIR)
-       make -C PVE install
-       make -C udev-rbd install
-       install -d $(DESTDIR)/usr/share/man/man1
-       install -m 0644 pvesm.1 $(DESTDIR)/usr/share/man/man1/
-       gzip -9 -n $(DESTDIR)/usr/share/man/man1/pvesm.1
-       install -m 0644 -D pvesm.bash-completion $(DESTDIR)$(BASHCOMPLDIR)/pvesm
-       install -m 0644 -D pvesm.zsh-completion $(DESTDIR)$(ZSHCOMPLDIR)/_pvesm
-
 .PHONY: deb
 deb: $(DEB)
 $(DEB):
        rm -rf build
-       rsync -a * build
+       cp -a src build
+       cp -a debian build/
        echo "git clone git://git.proxmox.com/git/pve-storage.git\\ngit checkout $(GITVERSION)" >build/debian/SOURCE
        cd build; dpkg-buildpackage -b -us -uc
        lintian $(DEB)
 
-.PHONY: test
-test:
-       perl -I. -T -e "use PVE::CLI::pvesm; PVE::CLI::pvesm->verify_api();"
-       make -C test
-
-.PHONY: clean
+.PHONY: clean distclean
+distclean: clean
 clean:
-       rm -f *.xml.tmp *.1 *.5 *.8 *(synopsis,opts).adoc docinfo.xml
        rm -rf build *.deb *.buildinfo *.changes
 
-.PHONY: distclean
-distclean: clean
-
-
 .PHONY: upload
 upload: $(DEB)
        tar cf - $(DEB) | ssh -X repoman@repo.proxmox.com -- upload --product pve --dist bullseye
diff --git a/PVE/API2/Disks.pm b/PVE/API2/Disks.pm
deleted file mode 100644 (file)
index bde6132..0000000
+++ /dev/null
@@ -1,318 +0,0 @@
-package PVE::API2::Disks;
-
-use strict;
-use warnings;
-
-use File::Basename;
-use HTTP::Status qw(:constants);
-
-use PVE::Diskmanage;
-use PVE::JSONSchema qw(get_standard_option);
-use PVE::SafeSyslog;
-use PVE::Tools qw(run_command);
-
-use PVE::API2::Disks::Directory;
-use PVE::API2::Disks::LVM;
-use PVE::API2::Disks::LVMThin;
-use PVE::API2::Disks::ZFS;
-
-use PVE::RESTHandler;
-use base qw(PVE::RESTHandler);
-
-__PACKAGE__->register_method ({
-   subclass => "PVE::API2::Disks::LVM",
-   path => 'lvm',
-});
-
-__PACKAGE__->register_method ({
-   subclass => "PVE::API2::Disks::LVMThin",
-   path => 'lvmthin',
-});
-
-__PACKAGE__->register_method ({
-   subclass => "PVE::API2::Disks::Directory",
-   path => 'directory',
-});
-
-__PACKAGE__->register_method ({
-   subclass => "PVE::API2::Disks::ZFS",
-   path => 'zfs',
-});
-
-__PACKAGE__->register_method ({
-    name => 'index',
-    path => '',
-    method => 'GET',
-    proxyto => 'node',
-    permissions => { user => 'all' },
-    description => "Node index.",
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-       },
-    },
-    returns => {
-       type => 'array',
-       items => {
-           type => "object",
-           properties => {},
-       },
-       links => [ { rel => 'child', href => "{name}" } ],
-    },
-    code => sub {
-       my ($param) = @_;
-
-       my $result = [
-           { name => 'list' },
-           { name => 'initgpt' },
-           { name => 'smart' },
-           { name => 'lvm' },
-           { name => 'lvmthin' },
-           { name => 'directory' },
-           { name => 'wipedisk' },
-           { name => 'zfs' },
-       ];
-
-       return $result;
-    }});
-
-__PACKAGE__->register_method ({
-    name => 'list',
-    path => 'list',
-    method => 'GET',
-    description => "List local disks.",
-    protected => 1,
-    proxyto => 'node',
-    permissions => {
-       check => ['or',
-           ['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1],
-           ['perm', '/nodes/{node}', ['Sys.Audit', 'Datastore.Audit'], any => 1],
-       ],
-    },
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           'include-partitions' => {
-               description => "Also include partitions.",
-               type => 'boolean',
-               optional => 1,
-               default => 0,
-           },
-           skipsmart => {
-               description => "Skip smart checks.",
-               type => 'boolean',
-               optional => 1,
-               default => 0,
-           },
-           type => {
-               description => "Only list specific types of disks.",
-               type => 'string',
-               enum => ['unused', 'journal_disks'],
-               optional => 1,
-           },
-       },
-    },
-    returns => {
-       type => 'array',
-       items => {
-           type => 'object',
-           properties => {
-               devpath => {
-                   type => 'string',
-                   description => 'The device path',
-               },
-               used => { type => 'string', optional => 1 },
-               gpt => { type => 'boolean' },
-               mounted => { type => 'boolean' },
-               size => { type => 'integer'},
-               osdid => { type => 'integer'},
-               vendor =>  { type => 'string', optional => 1 },
-               model =>  { type => 'string', optional => 1 },
-               serial =>  { type => 'string', optional => 1 },
-               wwn => { type => 'string', optional => 1},
-               health => { type => 'string', optional => 1},
-               parent => {
-                   type => 'string',
-                   description => 'For partitions only. The device path of ' .
-                       'the disk the partition resides on.',
-                   optional => 1
-               },
-           },
-       },
-    },
-    code => sub {
-       my ($param) = @_;
-
-       my $skipsmart = $param->{skipsmart} // 0;
-       my $include_partitions = $param->{'include-partitions'} // 0;
-
-       my $disks = PVE::Diskmanage::get_disks(
-           undef,
-           $skipsmart,
-           $include_partitions
-       );
-
-       my $type = $param->{type} // '';
-       my $result = [];
-
-       foreach my $disk (sort keys %$disks) {
-           my $entry = $disks->{$disk};
-           if ($type eq 'journal_disks') {
-               next if $entry->{osdid} >= 0;
-               if (my $usage = $entry->{used}) {
-                   next if !($usage eq 'partitions' && $entry->{gpt}
-                       || $usage eq 'LVM');
-               }
-           } elsif ($type eq 'unused') {
-               next if $entry->{used};
-           } elsif ($type ne '') {
-               die "internal error"; # should not happen
-           }
-           push @$result, $entry;
-       }
-       return $result;
-    }});
-
-__PACKAGE__->register_method ({
-    name => 'smart',
-    path => 'smart',
-    method => 'GET',
-    description => "Get SMART Health of a disk.",
-    protected => 1,
-    proxyto => "node",
-    permissions => {
-       check => ['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1],
-    },
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           disk => {
-               type => 'string',
-               pattern => '^/dev/[a-zA-Z0-9\/]+$',
-               description => "Block device name",
-           },
-           healthonly => {
-               type => 'boolean',
-               description => "If true returns only the health status",
-               optional => 1,
-           },
-       },
-    },
-    returns => {
-       type => 'object',
-       properties => {
-           health => { type => 'string' },
-           type => { type => 'string', optional => 1 },
-           attributes => { type => 'array', optional => 1},
-           text => { type => 'string', optional => 1 },
-       },
-    },
-    code => sub {
-       my ($param) = @_;
-
-       my $disk = PVE::Diskmanage::verify_blockdev_path($param->{disk});
-
-       my $result = PVE::Diskmanage::get_smart_data($disk, $param->{healthonly});
-
-       $result->{health} = 'UNKNOWN' if !defined $result->{health};
-       $result = { health => $result->{health} } if $param->{healthonly};
-
-       return $result;
-    }});
-
-__PACKAGE__->register_method ({
-    name => 'initgpt',
-    path => 'initgpt',
-    method => 'POST',
-    description => "Initialize Disk with GPT",
-    protected => 1,
-    proxyto => "node",
-    permissions => {
-       check => ['perm', '/', ['Sys.Modify']],
-    },
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           disk => {
-               type => 'string',
-               description => "Block device name",
-               pattern => '^/dev/[a-zA-Z0-9\/]+$',
-           },
-           uuid => {
-               type => 'string',
-               description => 'UUID for the GPT table',
-               pattern => '[a-fA-F0-9\-]+',
-               maxLength => 36,
-               optional => 1,
-           },
-       },
-    },
-    returns => { type => 'string' },
-    code => sub {
-       my ($param) = @_;
-
-       my $disk = PVE::Diskmanage::verify_blockdev_path($param->{disk});
-
-       my $rpcenv = PVE::RPCEnvironment::get();
-
-       my $authuser = $rpcenv->get_user();
-
-       die "$disk is a partition\n" if PVE::Diskmanage::is_partition($disk);
-       die "disk $disk already in use\n" if PVE::Diskmanage::disk_is_used($disk);
-       my $worker = sub {
-           PVE::Diskmanage::init_disk($disk, $param->{uuid});
-       };
-
-       my $diskid = $disk;
-       $diskid =~ s|^.*/||; # remove all up to the last slash
-       return $rpcenv->fork_worker('diskinit', $diskid, $authuser, $worker);
-    }});
-
-__PACKAGE__->register_method ({
-    name => 'wipe_disk',
-    path => 'wipedisk',
-    method => 'PUT',
-    description => "Wipe a disk or partition.",
-    proxyto => 'node',
-    protected => 1,
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           disk => {
-               type => 'string',
-               description => "Block device name",
-               pattern => '^/dev/[a-zA-Z0-9\/]+$',
-           },
-       },
-    },
-    returns => { type => 'string' },
-    code => sub {
-       my ($param) = @_;
-
-       my $disk = PVE::Diskmanage::verify_blockdev_path($param->{disk});
-
-       my $mounted = PVE::Diskmanage::is_mounted($disk);
-       die "disk/partition '${mounted}' is mounted\n" if $mounted;
-
-       my $held = PVE::Diskmanage::has_holder($disk);
-       die "disk/partition '${held}' has a holder\n" if $held;
-
-       my $rpcenv = PVE::RPCEnvironment::get();
-       my $authuser = $rpcenv->get_user();
-
-       my $worker = sub {
-           PVE::Diskmanage::wipe_blockdev($disk);
-           PVE::Diskmanage::udevadm_trigger($disk);
-       };
-
-       my $basename = basename($disk); # avoid '/' in the ID
-
-       return $rpcenv->fork_worker('wipedisk', $basename, $authuser, $worker);
-    }});
-
-1;
diff --git a/PVE/API2/Disks/Directory.pm b/PVE/API2/Disks/Directory.pm
deleted file mode 100644 (file)
index 4fdb068..0000000
+++ /dev/null
@@ -1,409 +0,0 @@
-package PVE::API2::Disks::Directory;
-
-use strict;
-use warnings;
-
-use POSIX;
-
-use PVE::Diskmanage;
-use PVE::JSONSchema qw(get_standard_option);
-use PVE::RESTHandler;
-use PVE::RPCEnvironment;
-use PVE::Systemd;
-use PVE::Tools qw(run_command trim file_set_contents file_get_contents dir_glob_foreach lock_file);
-
-use PVE::API2::Storage::Config;
-
-use base qw(PVE::RESTHandler);
-
-my $SGDISK = '/sbin/sgdisk';
-my $MKFS = '/sbin/mkfs';
-my $BLKID = '/sbin/blkid';
-
-my $read_ini = sub {
-    my ($filename) = @_;
-
-    my $content = file_get_contents($filename);
-    my @lines = split /\n/, $content;
-
-    my $result = {};
-    my $section;
-
-    foreach my $line (@lines) {
-       $line = trim($line);
-       if ($line =~ m/^\[([^\]]+)\]/) {
-           $section = $1;
-           if (!defined($result->{$section})) {
-               $result->{$section} = {};
-           }
-       } elsif ($line =~ m/^(.*?)=(.*)$/) {
-           my ($key, $val) = ($1, $2);
-           if (!$section) {
-               warn "key value pair found without section, skipping\n";
-               next;
-           }
-
-           if ($result->{$section}->{$key}) {
-               # make duplicate properties to arrays to keep the order
-               my $prop = $result->{$section}->{$key};
-               if (ref($prop) eq 'ARRAY') {
-                   push @$prop, $val;
-               } else {
-                   $result->{$section}->{$key} = [$prop, $val];
-               }
-           } else {
-               $result->{$section}->{$key} = $val;
-           }
-       }
-       # ignore everything else
-    }
-
-    return $result;
-};
-
-my $write_ini = sub {
-    my ($ini, $filename) = @_;
-
-    my $content = "";
-
-    foreach my $sname (sort keys %$ini) {
-       my $section = $ini->{$sname};
-
-       $content .= "[$sname]\n";
-
-       foreach my $pname (sort keys %$section) {
-           my $prop = $section->{$pname};
-
-           if (!ref($prop)) {
-               $content .= "$pname=$prop\n";
-           } elsif (ref($prop) eq 'ARRAY') {
-               foreach my $val (@$prop) {
-                   $content .= "$pname=$val\n";
-               }
-           } else {
-               die "invalid property '$pname'\n";
-           }
-       }
-       $content .= "\n";
-    }
-
-    file_set_contents($filename, $content);
-};
-
-__PACKAGE__->register_method ({
-    name => 'index',
-    path => '',
-    method => 'GET',
-    proxyto => 'node',
-    protected => 1,
-    permissions => {
-       check => ['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1],
-    },
-    description => "PVE Managed Directory storages.",
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-       },
-    },
-    returns => {
-       type => 'array',
-       items => {
-           type => 'object',
-           properties => {
-               unitfile => {
-                   type => 'string',
-                   description => 'The path of the mount unit.',
-               },
-               path => {
-                   type => 'string',
-                   description => 'The mount path.',
-               },
-               device => {
-                   type => 'string',
-                   description => 'The mounted device.',
-               },
-               type => {
-                   type => 'string',
-                   description => 'The filesystem type.',
-               },
-               options => {
-                   type => 'string',
-                   description => 'The mount options.',
-               },
-           },
-       },
-    },
-    code => sub {
-       my ($param) = @_;
-
-       my $result = [];
-
-       dir_glob_foreach('/etc/systemd/system', '^mnt-pve-(.+)\.mount$', sub {
-           my ($filename, $storid) = @_;
-           $storid = PVE::Systemd::unescape_unit($storid);
-
-           my $unitfile = "/etc/systemd/system/$filename";
-           my $unit = $read_ini->($unitfile);
-
-           push @$result, {
-               unitfile => $unitfile,
-               path => "/mnt/pve/$storid",
-               device => $unit->{'Mount'}->{'What'},
-               type => $unit->{'Mount'}->{'Type'},
-               options => $unit->{'Mount'}->{'Options'},
-           };
-       });
-
-       return $result;
-    }});
-
-__PACKAGE__->register_method ({
-    name => 'create',
-    path => '',
-    method => 'POST',
-    proxyto => 'node',
-    protected => 1,
-    permissions => {
-       check => ['perm', '/', ['Sys.Modify', 'Datastore.Allocate']],
-    },
-    description => "Create a Filesystem on an unused disk. Will be mounted under '/mnt/pve/NAME'.",
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           name => get_standard_option('pve-storage-id'),
-           device => {
-               type => 'string',
-               description => 'The block device you want to create the filesystem on.',
-           },
-           add_storage => {
-               description => "Configure storage using the directory.",
-               type => 'boolean',
-               optional => 1,
-               default => 0,
-           },
-           filesystem => {
-               description => "The desired filesystem.",
-               type => 'string',
-               enum => ['ext4', 'xfs'],
-               optional => 1,
-               default => 'ext4',
-           },
-       },
-    },
-    returns => { type => 'string' },
-    code => sub {
-       my ($param) = @_;
-
-       my $rpcenv = PVE::RPCEnvironment::get();
-       my $user = $rpcenv->get_user();
-
-       my $name = $param->{name};
-       my $dev = $param->{device};
-       my $node = $param->{node};
-       my $type = $param->{filesystem} // 'ext4';
-       my $path = "/mnt/pve/$name";
-       my $mountunitname = PVE::Systemd::escape_unit($path, 1) . ".mount";
-       my $mountunitpath = "/etc/systemd/system/$mountunitname";
-
-       $dev = PVE::Diskmanage::verify_blockdev_path($dev);
-       PVE::Diskmanage::assert_disk_unused($dev);
-
-       my $storage_params = {
-           type => 'dir',
-           storage => $name,
-           content => 'rootdir,images,iso,backup,vztmpl,snippets',
-           is_mountpoint => 1,
-           path => $path,
-           nodes => $node,
-       };
-       my $verify_params = [qw(path)];
-
-       if ($param->{add_storage}) {
-           PVE::API2::Storage::Config->create_or_update(
-               $name,
-               $node,
-               $storage_params,
-               $verify_params,
-               1,
-           );
-       }
-
-       my $mounted = PVE::Diskmanage::mounted_paths();
-       die "the path for '${name}' is already mounted: ${path} ($mounted->{$path})\n"
-           if $mounted->{$path};
-       die "a systemd mount unit already exists: ${mountunitpath}\n" if -e $mountunitpath;
-
-       my $worker = sub {
-           PVE::Diskmanage::locked_disk_action(sub {
-               PVE::Diskmanage::assert_disk_unused($dev);
-
-               my $part = $dev;
-
-               if (PVE::Diskmanage::is_partition($dev)) {
-                   eval { PVE::Diskmanage::change_parttype($dev, '8300'); };
-                   warn $@ if $@;
-               } else {
-                   # create partition
-                   my $cmd = [$SGDISK, '-n1', '-t1:8300', $dev];
-                   print "# ", join(' ', @$cmd), "\n";
-                   run_command($cmd);
-
-                   my ($devname) = $dev =~ m|^/dev/(.*)$|;
-                   $part = "/dev/";
-                   dir_glob_foreach("/sys/block/$devname", qr/\Q$devname\E.+/, sub {
-                       my ($partition) = @_;
-                       $part .= $partition;
-                   });
-               }
-
-               # create filesystem
-               my $cmd = [$MKFS, '-t', $type, $part];
-               print "# ", join(' ', @$cmd), "\n";
-               run_command($cmd);
-
-               # create systemd mount unit and enable & start it
-               my $ini = {
-                   'Unit' => {
-                       'Description' => "Mount storage '$name' under /mnt/pve",
-                   },
-                   'Install' => {
-                       'WantedBy' => 'multi-user.target',
-                   },
-               };
-
-               my $uuid_path;
-               my $uuid;
-
-               $cmd = [$BLKID, $part, '-o', 'export'];
-               print "# ", join(' ', @$cmd), "\n";
-               run_command($cmd, outfunc => sub {
-                       my ($line) = @_;
-
-                       if ($line =~ m/^UUID=(.*)$/) {
-                           $uuid = $1;
-                           $uuid_path = "/dev/disk/by-uuid/$uuid";
-                       }
-                   });
-
-               die "could not get UUID of device '$part'\n" if !$uuid;
-
-               $ini->{'Mount'} = {
-                   'What' => $uuid_path,
-                   'Where' => $path,
-                   'Type' => $type,
-                   'Options' => 'defaults',
-               };
-
-               $write_ini->($ini, $mountunitpath);
-
-               PVE::Diskmanage::udevadm_trigger($part);
-
-               run_command(['systemctl', 'daemon-reload']);
-               run_command(['systemctl', 'enable', $mountunitname]);
-               run_command(['systemctl', 'start', $mountunitname]);
-
-               if ($param->{add_storage}) {
-                   PVE::API2::Storage::Config->create_or_update(
-                       $name,
-                       $node,
-                       $storage_params,
-                       $verify_params,
-                   );
-               }
-           });
-       };
-
-       return $rpcenv->fork_worker('dircreate', $name, $user, $worker);
-    }});
-
-__PACKAGE__->register_method ({
-    name => 'delete',
-    path => '{name}',
-    method => 'DELETE',
-    proxyto => 'node',
-    protected => 1,
-    permissions => {
-       check => ['perm', '/', ['Sys.Modify', 'Datastore.Allocate']],
-    },
-    description => "Unmounts the storage and removes the mount unit.",
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           name => get_standard_option('pve-storage-id'),
-           'cleanup-config' => {
-               description => "Marks associated storage(s) as not available on this node anymore ".
-                   "or removes them from the configuration (if configured for this node only).",
-               type => 'boolean',
-               optional => 1,
-               default => 0,
-           },
-           'cleanup-disks' => {
-               description => "Also wipe disk so it can be repurposed afterwards.",
-               type => 'boolean',
-               optional => 1,
-               default => 0,
-           },
-       },
-    },
-    returns => { type => 'string' },
-    code => sub {
-       my ($param) = @_;
-
-       my $rpcenv = PVE::RPCEnvironment::get();
-       my $user = $rpcenv->get_user();
-
-       my $name = $param->{name};
-       my $node = $param->{node};
-
-       my $worker = sub {
-           my $path = "/mnt/pve/$name";
-           my $mountunitname = PVE::Systemd::escape_unit($path, 1) . ".mount";
-           my $mountunitpath = "/etc/systemd/system/$mountunitname";
-
-           PVE::Diskmanage::locked_disk_action(sub {
-               my $to_wipe;
-               if ($param->{'cleanup-disks'}) {
-                   my $unit = $read_ini->($mountunitpath);
-
-                   my $dev = PVE::Diskmanage::verify_blockdev_path($unit->{'Mount'}->{'What'});
-                   $to_wipe = $dev;
-
-                   # clean up whole device if this is the only partition
-                   $dev =~ s|^/dev/||;
-                   my $info = PVE::Diskmanage::get_disks($dev, 1, 1);
-                   die "unable to obtain information for disk '$dev'\n" if !$info->{$dev};
-                   $to_wipe = $info->{$dev}->{parent}
-                       if $info->{$dev}->{parent} && scalar(keys $info->%*) == 2;
-               }
-
-               run_command(['systemctl', 'stop', $mountunitname]);
-               run_command(['systemctl', 'disable', $mountunitname]);
-
-               unlink $mountunitpath or $! == ENOENT or die "cannot remove $mountunitpath - $!\n";
-
-               my $config_err;
-               if ($param->{'cleanup-config'}) {
-                   my $match = sub {
-                       my ($scfg) = @_;
-                       return $scfg->{type} eq 'dir' && $scfg->{path} eq $path;
-                   };
-                   eval { PVE::API2::Storage::Config->cleanup_storages_for_node($match, $node); };
-                   warn $config_err = $@ if $@;
-               }
-
-               if ($to_wipe) {
-                   PVE::Diskmanage::wipe_blockdev($to_wipe);
-                   PVE::Diskmanage::udevadm_trigger($to_wipe);
-               }
-
-               die "config cleanup failed - $config_err" if $config_err;
-           });
-       };
-
-       return $rpcenv->fork_worker('dirremove', $name, $user, $worker);
-    }});
-
-1;
diff --git a/PVE/API2/Disks/LVM.pm b/PVE/API2/Disks/LVM.pm
deleted file mode 100644 (file)
index fe87545..0000000
+++ /dev/null
@@ -1,281 +0,0 @@
-package PVE::API2::Disks::LVM;
-
-use strict;
-use warnings;
-
-use PVE::Storage::LVMPlugin;
-use PVE::Diskmanage;
-use PVE::JSONSchema qw(get_standard_option);
-use PVE::API2::Storage::Config;
-use PVE::Tools qw(lock_file run_command);
-
-use PVE::RPCEnvironment;
-use PVE::RESTHandler;
-
-use base qw(PVE::RESTHandler);
-
-__PACKAGE__->register_method ({
-    name => 'index',
-    path => '',
-    method => 'GET',
-    proxyto => 'node',
-    protected => 1,
-    permissions => {
-       check => ['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1],
-    },
-    description => "List LVM Volume Groups",
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-       },
-    },
-    returns => {
-       type => 'object',
-       properties => {
-           leaf => {
-               type => 'boolean',
-           },
-           children => {
-               type => 'array',
-               items => {
-                   type => "object",
-                   properties => {
-                       leaf => {
-                           type => 'boolean',
-                       },
-                       name => {
-                           type => 'string',
-                           description => 'The name of the volume group',
-                       },
-                       size => {
-                           type => 'integer',
-                           description => 'The size of the volume group in bytes',
-                       },
-                       free => {
-                           type => 'integer',
-                           description => 'The free bytes in the volume group',
-                       },
-                       children => {
-                           optional => 1,
-                           type => 'array',
-                           description => 'The underlying physical volumes',
-                           items =>  {
-                               type => 'object',
-                               properties => {
-                                   leaf => {
-                                       type => 'boolean',
-                                   },
-                                   name => {
-                                       type => 'string',
-                                       description => 'The name of the physical volume',
-                                   },
-                                   size => {
-                                       type => 'integer',
-                                       description => 'The size of the physical volume in bytes',
-                                   },
-                                   free => {
-                                       type => 'integer',
-                                       description => 'The free bytes in the physical volume',
-                                   },
-                               },
-                           },
-                       },
-                   },
-               },
-           },
-       },
-    },
-    code => sub {
-       my ($param) = @_;
-
-       my $result = [];
-
-       my $vgs = PVE::Storage::LVMPlugin::lvm_vgs(1);
-
-       foreach my $vg_name (sort keys %$vgs) {
-           my $vg = $vgs->{$vg_name};
-           $vg->{name} = $vg_name;
-           $vg->{leaf} = 0;
-           foreach my $pv (@{$vg->{pvs}}) {
-               $pv->{leaf} = 1;
-           }
-           $vg->{children} = delete $vg->{pvs};
-           push @$result, $vg;
-       }
-
-       return {
-           leaf => 0,
-           children => $result,
-       };
-    }});
-
-__PACKAGE__->register_method ({
-    name => 'create',
-    path => '',
-    method => 'POST',
-    proxyto => 'node',
-    protected => 1,
-    permissions => {
-       check => ['perm', '/', ['Sys.Modify', 'Datastore.Allocate']],
-    },
-    description => "Create an LVM Volume Group",
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           name => get_standard_option('pve-storage-id'),
-           device => {
-               type => 'string',
-               description => 'The block device you want to create the volume group on',
-           },
-           add_storage => {
-               description => "Configure storage using the Volume Group",
-               type => 'boolean',
-               optional => 1,
-               default => 0,
-           },
-       },
-    },
-    returns => { type => 'string' },
-    code => sub {
-       my ($param) = @_;
-
-       my $rpcenv = PVE::RPCEnvironment::get();
-       my $user = $rpcenv->get_user();
-
-       my $name = $param->{name};
-       my $dev = $param->{device};
-       my $node = $param->{node};
-
-       $dev = PVE::Diskmanage::verify_blockdev_path($dev);
-       PVE::Diskmanage::assert_disk_unused($dev);
-
-       my $storage_params = {
-           type => 'lvm',
-           vgname => $name,
-           storage => $name,
-           content => 'rootdir,images',
-           shared => 0,
-           nodes => $node,
-       };
-       my $verify_params = [qw(vgname)];
-
-       if ($param->{add_storage}) {
-           PVE::API2::Storage::Config->create_or_update(
-               $name,
-               $node,
-               $storage_params,
-               $verify_params,
-               1,
-           );
-       }
-
-       my $worker = sub {
-           PVE::Diskmanage::locked_disk_action(sub {
-               PVE::Diskmanage::assert_disk_unused($dev);
-               die "volume group with name '${name}' already exists on node '${node}'\n"
-                   if PVE::Storage::LVMPlugin::lvm_vgs()->{$name};
-
-               if (PVE::Diskmanage::is_partition($dev)) {
-                   eval { PVE::Diskmanage::change_parttype($dev, '8E00'); };
-                   warn $@ if $@;
-               }
-
-               PVE::Storage::LVMPlugin::lvm_create_volume_group($dev, $name);
-
-               PVE::Diskmanage::udevadm_trigger($dev);
-
-               if ($param->{add_storage}) {
-                   PVE::API2::Storage::Config->create_or_update(
-                       $name,
-                       $node,
-                       $storage_params,
-                       $verify_params,
-                   );
-               }
-           });
-       };
-
-       return $rpcenv->fork_worker('lvmcreate', $name, $user, $worker);
-    }});
-
-__PACKAGE__->register_method ({
-    name => 'delete',
-    path => '{name}',
-    method => 'DELETE',
-    proxyto => 'node',
-    protected => 1,
-    permissions => {
-       check => ['perm', '/', ['Sys.Modify', 'Datastore.Allocate']],
-    },
-    description => "Remove an LVM Volume Group.",
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           name => get_standard_option('pve-storage-id'),
-           'cleanup-config' => {
-               description => "Marks associated storage(s) as not available on this node anymore ".
-                   "or removes them from the configuration (if configured for this node only).",
-               type => 'boolean',
-               optional => 1,
-               default => 0,
-           },
-           'cleanup-disks' => {
-               description => "Also wipe disks so they can be repurposed afterwards.",
-               type => 'boolean',
-               optional => 1,
-               default => 0,
-           },
-       },
-    },
-    returns => { type => 'string' },
-    code => sub {
-       my ($param) = @_;
-
-       my $rpcenv = PVE::RPCEnvironment::get();
-       my $user = $rpcenv->get_user();
-
-       my $name = $param->{name};
-       my $node = $param->{node};
-
-       my $worker = sub {
-           PVE::Diskmanage::locked_disk_action(sub {
-               my $vgs = PVE::Storage::LVMPlugin::lvm_vgs(1);
-               die "no such volume group '$name'\n" if !$vgs->{$name};
-
-               PVE::Storage::LVMPlugin::lvm_destroy_volume_group($name);
-
-               my $config_err;
-               if ($param->{'cleanup-config'}) {
-                   my $match = sub {
-                       my ($scfg) = @_;
-                       return $scfg->{type} eq 'lvm' && $scfg->{vgname} eq $name;
-                   };
-                   eval { PVE::API2::Storage::Config->cleanup_storages_for_node($match, $node); };
-                   warn $config_err = $@ if $@;
-               }
-
-               if ($param->{'cleanup-disks'}) {
-                   my $wiped = [];
-                   eval {
-                       for my $pv ($vgs->{$name}->{pvs}->@*) {
-                           my $dev = PVE::Diskmanage::verify_blockdev_path($pv->{name});
-                           PVE::Diskmanage::wipe_blockdev($dev);
-                           push $wiped->@*, $dev;
-                       }
-                   };
-                   my $err = $@;
-                   PVE::Diskmanage::udevadm_trigger($wiped->@*);
-                   die "cleanup failed - $err" if $err;
-               }
-
-               die "config cleanup failed - $config_err" if $config_err;
-           });
-       };
-
-       return $rpcenv->fork_worker('lvmremove', $name, $user, $worker);
-    }});
-
-1;
diff --git a/PVE/API2/Disks/LVMThin.pm b/PVE/API2/Disks/LVMThin.pm
deleted file mode 100644 (file)
index 038310a..0000000
+++ /dev/null
@@ -1,271 +0,0 @@
-package PVE::API2::Disks::LVMThin;
-
-use strict;
-use warnings;
-
-use PVE::Storage::LvmThinPlugin;
-use PVE::Diskmanage;
-use PVE::JSONSchema qw(get_standard_option);
-use PVE::API2::Storage::Config;
-use PVE::Storage;
-use PVE::Tools qw(run_command lock_file);
-
-use PVE::RPCEnvironment;
-use PVE::RESTHandler;
-
-use base qw(PVE::RESTHandler);
-
-__PACKAGE__->register_method ({
-    name => 'index',
-    path => '',
-    method => 'GET',
-    proxyto => 'node',
-    protected => 1,
-    permissions => {
-       check => ['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1],
-    },
-    description => "List LVM thinpools",
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-       },
-    },
-    returns => {
-       type => 'array',
-       items => {
-           type => 'object',
-           properties => {
-               lv => {
-                   type => 'string',
-                   description => 'The name of the thinpool.',
-               },
-               vg => {
-                   type => 'string',
-                   description => 'The associated volume group.',
-               },
-               lv_size => {
-                   type => 'integer',
-                   description => 'The size of the thinpool in bytes.',
-               },
-               used => {
-                   type => 'integer',
-                   description => 'The used bytes of the thinpool.',
-               },
-               metadata_size => {
-                   type => 'integer',
-                   description => 'The size of the metadata lv in bytes.',
-               },
-               metadata_used => {
-                   type => 'integer',
-                   description => 'The used bytes of the metadata lv.',
-               },
-           },
-       },
-    },
-    code => sub {
-       my ($param) = @_;
-       return PVE::Storage::LvmThinPlugin::list_thinpools(undef);
-    }});
-
-__PACKAGE__->register_method ({
-    name => 'create',
-    path => '',
-    method => 'POST',
-    proxyto => 'node',
-    protected => 1,
-    permissions => {
-       check => ['perm', '/', ['Sys.Modify', 'Datastore.Allocate']],
-    },
-    description => "Create an LVM thinpool",
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           name => get_standard_option('pve-storage-id'),
-           device => {
-               type => 'string',
-               description => 'The block device you want to create the thinpool on.',
-           },
-           add_storage => {
-               description => "Configure storage using the thinpool.",
-               type => 'boolean',
-               optional => 1,
-               default => 0,
-           },
-       },
-    },
-    returns => { type => 'string' },
-    code => sub {
-       my ($param) = @_;
-
-       my $rpcenv = PVE::RPCEnvironment::get();
-       my $user = $rpcenv->get_user();
-
-       my $name = $param->{name};
-       my $dev = $param->{device};
-       my $node = $param->{node};
-
-       $dev = PVE::Diskmanage::verify_blockdev_path($dev);
-       PVE::Diskmanage::assert_disk_unused($dev);
-
-       my $storage_params = {
-           type => 'lvmthin',
-           vgname => $name,
-           thinpool => $name,
-           storage => $name,
-           content => 'rootdir,images',
-           nodes => $node,
-       };
-       my $verify_params = [qw(vgname thinpool)];
-
-       if ($param->{add_storage}) {
-           PVE::API2::Storage::Config->create_or_update(
-               $name,
-               $node,
-               $storage_params,
-               $verify_params,
-               1,
-           );
-       }
-
-       my $worker = sub {
-           PVE::Diskmanage::locked_disk_action(sub {
-               PVE::Diskmanage::assert_disk_unused($dev);
-
-               die "volume group with name '${name}' already exists on node '${node}'\n"
-                   if PVE::Storage::LVMPlugin::lvm_vgs()->{$name};
-
-               if (PVE::Diskmanage::is_partition($dev)) {
-                   eval { PVE::Diskmanage::change_parttype($dev, '8E00'); };
-                   warn $@ if $@;
-               }
-
-               PVE::Storage::LVMPlugin::lvm_create_volume_group($dev, $name);
-               my $pv = PVE::Storage::LVMPlugin::lvm_pv_info($dev);
-               # keep some free space just in case
-               my $datasize = $pv->{size} - 128*1024;
-               # default to 1% for metadata
-               my $metadatasize = $datasize/100;
-               # but at least 1G, as recommended in lvmthin man
-               $metadatasize = 1024*1024 if $metadatasize < 1024*1024;
-               # but at most 16G, which is the current lvm max
-               $metadatasize = 16*1024*1024 if $metadatasize > 16*1024*1024;
-               # shrink data by needed amount for metadata
-               $datasize -= 2*$metadatasize;
-
-               run_command([
-                   '/sbin/lvcreate',
-                   '--type', 'thin-pool',
-                   "-L${datasize}K",
-                   '--poolmetadatasize', "${metadatasize}K",
-                   '-n', $name,
-                   $name
-               ]);
-
-               PVE::Diskmanage::udevadm_trigger($dev);
-
-               if ($param->{add_storage}) {
-                   PVE::API2::Storage::Config->create_or_update(
-                       $name,
-                       $node,
-                       $storage_params,
-                       $verify_params,
-                   );
-               }
-           });
-       };
-
-       return $rpcenv->fork_worker('lvmthincreate', $name, $user, $worker);
-    }});
-
-__PACKAGE__->register_method ({
-    name => 'delete',
-    path => '{name}',
-    method => 'DELETE',
-    proxyto => 'node',
-    protected => 1,
-    permissions => {
-       check => ['perm', '/', ['Sys.Modify', 'Datastore.Allocate']],
-    },
-    description => "Remove an LVM thin pool.",
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           name => get_standard_option('pve-storage-id'),
-           'volume-group' => get_standard_option('pve-storage-id'),
-           'cleanup-config' => {
-               description => "Marks associated storage(s) as not available on this node anymore ".
-                   "or removes them from the configuration (if configured for this node only).",
-               type => 'boolean',
-               optional => 1,
-               default => 0,
-           },
-           'cleanup-disks' => {
-               description => "Also wipe disks so they can be repurposed afterwards.",
-               type => 'boolean',
-               optional => 1,
-               default => 0,
-           },
-       },
-    },
-    returns => { type => 'string' },
-    code => sub {
-       my ($param) = @_;
-
-       my $rpcenv = PVE::RPCEnvironment::get();
-       my $user = $rpcenv->get_user();
-
-       my $vg = $param->{'volume-group'};
-       my $lv = $param->{name};
-       my $node = $param->{node};
-
-       my $worker = sub {
-           PVE::Diskmanage::locked_disk_action(sub {
-               my $thinpools = PVE::Storage::LvmThinPlugin::list_thinpools();
-
-               die "no such thin pool ${vg}/${lv}\n"
-                   if !grep { $_->{lv} eq $lv && $_->{vg} eq $vg } $thinpools->@*;
-
-               run_command(['lvremove', '-y', "${vg}/${lv}"]);
-
-               my $config_err;
-               if ($param->{'cleanup-config'}) {
-                   my $match = sub {
-                       my ($scfg) = @_;
-                       return $scfg->{type} eq 'lvmthin'
-                           && $scfg->{vgname} eq $vg
-                           && $scfg->{thinpool} eq $lv;
-                   };
-                   eval { PVE::API2::Storage::Config->cleanup_storages_for_node($match, $node); };
-                   warn $config_err = $@ if $@;
-               }
-
-               if ($param->{'cleanup-disks'}) {
-                   my $vgs = PVE::Storage::LVMPlugin::lvm_vgs(1);
-
-                   die "no such volume group '$vg'\n" if !$vgs->{$vg};
-                   die "volume group '$vg' still in use\n" if $vgs->{$vg}->{lvcount} > 0;
-
-                   my $wiped = [];
-                   eval {
-                       for my $pv ($vgs->{$vg}->{pvs}->@*) {
-                           my $dev = PVE::Diskmanage::verify_blockdev_path($pv->{name});
-                           PVE::Diskmanage::wipe_blockdev($dev);
-                           push $wiped->@*, $dev;
-                       }
-                   };
-                   my $err = $@;
-                   PVE::Diskmanage::udevadm_trigger($wiped->@*);
-                   die "cleanup failed - $err" if $err;
-               }
-
-               die "config cleanup failed - $config_err" if $config_err;
-           });
-       };
-
-       return $rpcenv->fork_worker('lvmthinremove', "${vg}-${lv}", $user, $worker);
-    }});
-
-1;
diff --git a/PVE/API2/Disks/Makefile b/PVE/API2/Disks/Makefile
deleted file mode 100644 (file)
index 9152aed..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-
-SOURCES= LVM.pm\
-        LVMThin.pm\
-        ZFS.pm\
-        Directory.pm
-
-.PHONY: install
-install:
-       for i in ${SOURCES}; do install -D -m 0644 $$i ${DESTDIR}${PERLDIR}/PVE/API2/Disks/$$i; done
diff --git a/PVE/API2/Disks/ZFS.pm b/PVE/API2/Disks/ZFS.pm
deleted file mode 100644 (file)
index a9dc3a7..0000000
+++ /dev/null
@@ -1,612 +0,0 @@
-package PVE::API2::Disks::ZFS;
-
-use strict;
-use warnings;
-
-use PVE::Diskmanage;
-use PVE::JSONSchema qw(get_standard_option parse_property_string);
-use PVE::Systemd;
-use PVE::API2::Storage::Config;
-use PVE::Storage;
-use PVE::Tools qw(run_command lock_file trim);
-
-use PVE::RPCEnvironment;
-use PVE::RESTHandler;
-
-use base qw(PVE::RESTHandler);
-
-my $ZPOOL = '/sbin/zpool';
-my $ZFS = '/sbin/zfs';
-
-sub get_pool_data {
-    die "zfsutils-linux not installed\n" if ! -f $ZPOOL;
-
-    my $propnames = [qw(name size alloc free frag dedup health)];
-    my $numbers = {
-       size => 1,
-       alloc => 1,
-       free => 1,
-       frag => 1,
-       dedup => 1,
-    };
-
-    my $pools = [];
-    run_command([$ZPOOL, 'list', '-HpPLo', join(',', @$propnames)], outfunc => sub {
-       my ($line) = @_;
-
-       my @props = split('\s+', trim($line));
-       my $pool = {};
-       for (my $i = 0; $i < scalar(@$propnames); $i++) {
-           if ($numbers->{$propnames->[$i]}) {
-               $pool->{$propnames->[$i]} = $props[$i] + 0;
-           } else {
-               $pool->{$propnames->[$i]} = $props[$i];
-           }
-       }
-
-       push @$pools, $pool;
-    });
-
-    return $pools;
-}
-
-__PACKAGE__->register_method ({
-    name => 'index',
-    path => '',
-    method => 'GET',
-    proxyto => 'node',
-    protected => 1,
-    permissions => {
-       check => ['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1],
-    },
-    description => "List Zpools.",
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-       },
-    },
-    returns => {
-       type => 'array',
-       items => {
-           type => 'object',
-           properties => {
-               name => {
-                   type => 'string',
-                   description => "",
-               },
-               size => {
-                   type => 'integer',
-                   description => "",
-               },
-               alloc => {
-                   type => 'integer',
-                   description => "",
-               },
-               free => {
-                   type => 'integer',
-                   description => "",
-               },
-               frag => {
-                   type => 'integer',
-                   description => "",
-               },
-               dedup => {
-                   type => 'number',
-                   description => "",
-               },
-               health => {
-                   type => 'string',
-                   description => "",
-               },
-           },
-       },
-       links => [ { rel => 'child', href => "{name}" } ],
-    },
-    code => sub {
-       my ($param) = @_;
-
-       return get_pool_data();
-    }});
-
-sub preparetree {
-    my ($el) = @_;
-    delete $el->{lvl};
-    if ($el->{children} && scalar(@{$el->{children}})) {
-       $el->{leaf} = 0;
-       foreach my $child (@{$el->{children}}) {
-           preparetree($child);
-       }
-    } else {
-       $el->{leaf} = 1;
-    }
-}
-
-
-__PACKAGE__->register_method ({
-    name => 'detail',
-    path => '{name}',
-    method => 'GET',
-    proxyto => 'node',
-    protected => 1,
-    permissions => {
-       check => ['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1],
-    },
-    description => "Get details about a zpool.",
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           name => get_standard_option('pve-storage-id'),
-       },
-    },
-    returns => {
-       type => 'object',
-       properties => {
-           name => {
-               type => 'string',
-               description => 'The name of the zpool.',
-           },
-           state => {
-               type => 'string',
-               description => 'The state of the zpool.',
-           },
-           status => {
-               optional => 1,
-               type => 'string',
-               description => 'Information about the state of the zpool.',
-           },
-           action => {
-               optional => 1,
-               type => 'string',
-               description => 'Information about the recommended action to fix the state.',
-           },
-           scan => {
-               optional => 1,
-               type => 'string',
-               description => 'Information about the last/current scrub.',
-           },
-           errors => {
-               type => 'string',
-               description => 'Information about the errors on the zpool.',
-           },
-           children => {
-               type => 'array',
-               description => "The pool configuration information, including the vdevs for each section (e.g. spares, cache), may be nested.",
-               items => {
-                   type => 'object',
-                   properties => {
-                       name => {
-                           type => 'string',
-                           description => 'The name of the vdev or section.',
-                       },
-                       state => {
-                           optional => 1,
-                           type => 'string',
-                           description => 'The state of the vdev.',
-                       },
-                       read => {
-                           optional => 1,
-                           type => 'number',
-                       },
-                       write => {
-                           optional => 1,
-                           type => 'number',
-                       },
-                       cksum => {
-                           optional => 1,
-                           type => 'number',
-                       },
-                       msg => {
-                           type => 'string',
-                           description => 'An optional message about the vdev.'
-                       }
-                   },
-               },
-           },
-       },
-    },
-    code => sub {
-       my ($param) = @_;
-
-       if (!-f $ZPOOL) {
-           die "zfsutils-linux not installed\n";
-       }
-
-       my $cmd = [$ZPOOL, 'status', '-P', $param->{name}];
-
-       my $pool = {
-           lvl => 0,
-       };
-
-       my $curfield;
-       my $config = 0;
-
-       my $stack = [$pool];
-       my $curlvl = 0;
-
-       run_command($cmd, outfunc => sub {
-           my ($line) = @_;
-
-           if ($line =~ m/^\s*(\S+): (\S+.*)$/) {
-               $curfield = $1;
-               $pool->{$curfield} = $2;
-
-               $config = 0 if $curfield eq 'errors';
-           } elsif (!$config && $line =~ m/^\s+(\S+.*)$/) {
-               $pool->{$curfield} .= " " . $1;
-           } elsif (!$config && $line =~ m/^\s*config:/) {
-               $config = 1;
-           } elsif ($config && $line =~ m/^(\s+)(\S+)\s*(\S+)?(?:\s+(\S+)\s+(\S+)\s+(\S+))?\s*(.*)$/) {
-               my ($space, $name, $state, $read, $write, $cksum, $msg) = ($1, $2, $3, $4, $5, $6, $7);
-               if ($name ne "NAME") {
-                   my $lvl = int(length($space) / 2) + 1; # two spaces per level
-                   my $vdev = {
-                       name => $name,
-                       msg => $msg,
-                       lvl => $lvl,
-                   };
-
-                   $vdev->{state} = $state if defined($state);
-                   $vdev->{read} = $read + 0 if defined($read);
-                   $vdev->{write} = $write + 0 if defined($write);
-                   $vdev->{cksum} = $cksum + 0 if defined($cksum);
-
-                   my $cur = pop @$stack;
-
-                   if ($lvl > $curlvl) {
-                       $cur->{children} = [ $vdev ];
-                   } elsif ($lvl == $curlvl) {
-                       $cur = pop @$stack;
-                       push @{$cur->{children}}, $vdev;
-                   } else {
-                       while ($lvl <= $cur->{lvl} && $cur->{lvl} != 0) {
-                           $cur = pop @$stack;
-                       }
-                       push @{$cur->{children}}, $vdev;
-                   }
-
-                   push @$stack, $cur;
-                   push @$stack, $vdev;
-                   $curlvl = $lvl;
-               }
-           }
-       });
-
-       # change treenodes for extjs tree
-       $pool->{name} = delete $pool->{pool};
-       preparetree($pool);
-
-       return $pool;
-    }});
-
-my $draid_config_format = {
-    spares => {
-       type => 'integer',
-       minimum => 0,
-       description => 'Number of dRAID spares.',
-    },
-    data => {
-       type => 'integer',
-       minimum => 1,
-       description => 'The number of data devices per redundancy group. (dRAID)',
-    },
-};
-
-__PACKAGE__->register_method ({
-    name => 'create',
-    path => '',
-    method => 'POST',
-    proxyto => 'node',
-    protected => 1,
-    permissions => {
-       check => ['perm', '/', ['Sys.Modify', 'Datastore.Allocate']],
-    },
-    description => "Create a ZFS pool.",
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           name => get_standard_option('pve-storage-id'),
-           raidlevel => {
-               type => 'string',
-               description => 'The RAID level to use.',
-               enum => [
-                   'single', 'mirror',
-                   'raid10', 'raidz', 'raidz2', 'raidz3',
-                   'draid', 'draid2', 'draid3',
-               ],
-           },
-           devices => {
-               type => 'string', format => 'string-list',
-               description => 'The block devices you want to create the zpool on.',
-           },
-           'draid-config' => {
-               type => 'string',
-               format => $draid_config_format,
-               optional => 1,
-           },
-           ashift => {
-               type => 'integer',
-               minimum => 9,
-               maximum => 16,
-               optional => 1,
-               default => 12,
-               description => 'Pool sector size exponent.',
-           },
-           compression => {
-               type => 'string',
-               description => 'The compression algorithm to use.',
-               enum => ['on', 'off', 'gzip', 'lz4', 'lzjb', 'zle', 'zstd'],
-               optional => 1,
-               default => 'on',
-           },
-           add_storage => {
-               description => "Configure storage using the zpool.",
-               type => 'boolean',
-               optional => 1,
-               default => 0,
-           },
-       },
-    },
-    returns => { type => 'string' },
-    code => sub {
-       my ($param) = @_;
-
-       my $rpcenv = PVE::RPCEnvironment::get();
-       my $user = $rpcenv->get_user();
-
-       my $name = $param->{name};
-       my $node = $param->{node};
-       my $devs = [PVE::Tools::split_list($param->{devices})];
-       my $raidlevel = $param->{raidlevel};
-       my $compression = $param->{compression} // 'on';
-
-       my $draid_config;
-       if (exists $param->{'draid-config'}) {
-           die "draid-config set without using dRAID level\n" if $raidlevel !~ m/^draid/;
-           $draid_config = parse_property_string($draid_config_format, $param->{'draid-config'});
-       }
-
-       for my $dev (@$devs) {
-           $dev = PVE::Diskmanage::verify_blockdev_path($dev);
-           PVE::Diskmanage::assert_disk_unused($dev);
-
-       }
-       my $storage_params = {
-           type => 'zfspool',
-           pool => $name,
-           storage => $name,
-           content => 'rootdir,images',
-           nodes => $node,
-       };
-       my $verify_params = [qw(pool)];
-
-       if ($param->{add_storage}) {
-           PVE::API2::Storage::Config->create_or_update(
-               $name,
-               $node,
-               $storage_params,
-               $verify_params,
-               1,
-           );
-       }
-
-       my $pools = get_pool_data();
-       die "pool '${name}' already exists on node '${node}'\n"
-           if grep { $_->{name} eq $name } @{$pools};
-
-       my $numdisks = scalar(@$devs);
-       my $mindisks = {
-           single => 1,
-           mirror => 2,
-           raid10 => 4,
-           raidz => 3,
-           raidz2 => 4,
-           raidz3 => 5,
-           draid => 3,
-           draid2 => 4,
-           draid3 => 5,
-       };
-
-       # sanity checks
-       die "raid10 needs an even number of disks\n"
-           if $raidlevel eq 'raid10' && $numdisks % 2 != 0;
-
-       die "please give only one disk for single disk mode\n"
-           if $raidlevel eq 'single' && $numdisks > 1;
-
-       die "$raidlevel needs at least $mindisks->{$raidlevel} disks\n"
-           if $numdisks < $mindisks->{$raidlevel};
-
-       # draid checks
-       if ($raidlevel =~ m/^draid/) {
-           # bare minimum would be two drives: one for parity & one for data, but forbid that
-           # because it makes no sense in practice, at least one spare disk should be used
-           my $draid_min = $mindisks->{$raidlevel} - 2;
-           if ($draid_config) {
-               $draid_min += $draid_config->{data} || 0;
-               $draid_min += $draid_config->{spares} || 0;
-           }
-           die "At least $draid_min disks needed for current dRAID config\n"
-               if $numdisks < $draid_min;
-       }
-
-       my $code = sub {
-           for my $dev (@$devs) {
-               PVE::Diskmanage::assert_disk_unused($dev);
-
-               my $is_partition = PVE::Diskmanage::is_partition($dev);
-
-               if ($is_partition) {
-                   eval {
-                       PVE::Diskmanage::change_parttype($dev, '6a898cc3-1dd2-11b2-99a6-080020736631');
-                   };
-                   warn $@ if $@;
-               }
-
-               my $sysfsdev = $is_partition ? PVE::Diskmanage::get_blockdev($dev) : $dev;
-
-               $sysfsdev =~ s!^/dev/!/sys/block/!;
-               if ($is_partition) {
-                   my $part = $dev =~ s!^/dev/!!r;
-                   $sysfsdev .= "/${part}";
-               }
-
-               my $udevinfo = PVE::Diskmanage::get_udev_info($sysfsdev);
-               $dev = $udevinfo->{by_id_link} if defined($udevinfo->{by_id_link});
-           }
-
-           # create zpool with desired raidlevel
-           my $ashift = $param->{ashift} // 12;
-
-           my $cmd = [$ZPOOL, 'create', '-o', "ashift=$ashift", $name];
-
-           if ($raidlevel eq 'raid10') {
-               for (my $i = 0; $i < @$devs; $i+=2) {
-                   push @$cmd, 'mirror', $devs->[$i], $devs->[$i+1];
-               }
-           } elsif ($raidlevel eq 'single') {
-               push @$cmd, $devs->[0];
-           } elsif ($raidlevel =~ m/^draid/) {
-               my $draid_cmd = $raidlevel;
-               $draid_cmd .= ":$draid_config->{data}d" if $draid_config->{data};
-               $draid_cmd .= ":$draid_config->{spares}s" if $draid_config->{spares};
-               push @$cmd, $draid_cmd, @$devs;
-           } else {
-               push @$cmd, $raidlevel, @$devs;
-           }
-
-           print "# ", join(' ', @$cmd), "\n";
-           run_command($cmd);
-
-           $cmd = [$ZFS, 'set', "compression=$compression", $name];
-           print "# ", join(' ', @$cmd), "\n";
-           run_command($cmd);
-
-           if (-e '/lib/systemd/system/zfs-import@.service') {
-               my $importunit = 'zfs-import@'. PVE::Systemd::escape_unit($name, undef) . '.service';
-               $cmd = ['systemctl', 'enable', $importunit];
-               print "# ", join(' ', @$cmd), "\n";
-               run_command($cmd);
-           }
-
-           PVE::Diskmanage::udevadm_trigger($devs->@*);
-
-           if ($param->{add_storage}) {
-               PVE::API2::Storage::Config->create_or_update(
-                   $name,
-                   $node,
-                   $storage_params,
-                   $verify_params,
-               );
-           }
-       };
-
-       return $rpcenv->fork_worker('zfscreate', $name, $user, sub {
-           PVE::Diskmanage::locked_disk_action($code);
-       });
-    }});
-
-__PACKAGE__->register_method ({
-    name => 'delete',
-    path => '{name}',
-    method => 'DELETE',
-    proxyto => 'node',
-    protected => 1,
-    permissions => {
-       check => ['perm', '/', ['Sys.Modify', 'Datastore.Allocate']],
-    },
-    description => "Destroy a ZFS pool.",
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           name => get_standard_option('pve-storage-id'),
-           'cleanup-config' => {
-               description => "Marks associated storage(s) as not available on this node anymore ".
-                   "or removes them from the configuration (if configured for this node only).",
-               type => 'boolean',
-               optional => 1,
-               default => 0,
-           },
-           'cleanup-disks' => {
-               description => "Also wipe disks so they can be repurposed afterwards.",
-               type => 'boolean',
-               optional => 1,
-               default => 0,
-           },
-       },
-    },
-    returns => { type => 'string' },
-    code => sub {
-       my ($param) = @_;
-
-       my $rpcenv = PVE::RPCEnvironment::get();
-       my $user = $rpcenv->get_user();
-
-       my $name = $param->{name};
-       my $node = $param->{node};
-
-       my $worker = sub {
-           PVE::Diskmanage::locked_disk_action(sub {
-               my $to_wipe = [];
-               if ($param->{'cleanup-disks'}) {
-                   # Using -o name does not only output the name in combination with -v.
-                   run_command(['zpool', 'list', '-vHPL', $name], outfunc => sub {
-                       my ($line) = @_;
-
-                       my ($name) = PVE::Tools::split_list($line);
-                       return if $name !~ m|^/dev/.+|;
-
-                       my $dev = PVE::Diskmanage::verify_blockdev_path($name);
-                       my $wipe = $dev;
-
-                       $dev =~ s|^/dev/||;
-                       my $info = PVE::Diskmanage::get_disks($dev, 1, 1);
-                       die "unable to obtain information for disk '$dev'\n" if !$info->{$dev};
-
-                       # Wipe whole disk if usual ZFS layout with partition 9 as ZFS reserved.
-                       my $parent = $info->{$dev}->{parent};
-                       if ($parent && scalar(keys $info->%*) == 3) {
-                           $parent =~ s|^/dev/||;
-                           my $info9 = $info->{"${parent}9"};
-
-                           $wipe = $info->{$dev}->{parent} # need leading /dev/
-                               if $info9 && $info9->{used} && $info9->{used} =~ m/^ZFS reserved/;
-                       }
-
-                       push $to_wipe->@*, $wipe;
-                   });
-               }
-
-               if (-e '/lib/systemd/system/zfs-import@.service') {
-                   my $importunit = 'zfs-import@' . PVE::Systemd::escape_unit($name) . '.service';
-                   run_command(['systemctl', 'disable', $importunit]);
-               }
-
-               run_command(['zpool', 'destroy', $name]);
-
-               my $config_err;
-               if ($param->{'cleanup-config'}) {
-                   my $match = sub {
-                       my ($scfg) = @_;
-                       return $scfg->{type} eq 'zfspool' && $scfg->{pool} eq $name;
-                   };
-                   eval { PVE::API2::Storage::Config->cleanup_storages_for_node($match, $node); };
-                   warn $config_err = $@ if $@;
-               }
-
-               eval { PVE::Diskmanage::wipe_blockdev($_) for $to_wipe->@*; };
-               my $err = $@;
-               PVE::Diskmanage::udevadm_trigger($to_wipe->@*);
-               die "cleanup failed - $err" if $err;
-
-               die "config cleanup failed - $config_err" if $config_err;
-           });
-       };
-
-       return $rpcenv->fork_worker('zfsremove', $name, $user, $worker);
-    }});
-
-1;
diff --git a/PVE/API2/Makefile b/PVE/API2/Makefile
deleted file mode 100644 (file)
index fe316c5..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-.PHONY: install
-install:
-       install -D -m 0644 Disks.pm ${DESTDIR}${PERLDIR}/PVE/API2/Disks.pm
-       make -C Storage install
-       make -C Disks install
diff --git a/PVE/API2/Storage/Config.pm b/PVE/API2/Storage/Config.pm
deleted file mode 100755 (executable)
index 821db21..0000000
+++ /dev/null
@@ -1,424 +0,0 @@
-package PVE::API2::Storage::Config;
-
-use strict;
-use warnings;
-
-use PVE::SafeSyslog;
-use PVE::Tools qw(extract_param extract_sensitive_params);
-use PVE::Cluster qw(cfs_read_file cfs_write_file);
-use PVE::Storage;
-use PVE::Storage::Plugin;
-use PVE::Storage::LVMPlugin;
-use PVE::Storage::CIFSPlugin;
-use HTTP::Status qw(:constants);
-use Storable qw(dclone);
-use PVE::JSONSchema qw(get_standard_option);
-use PVE::RPCEnvironment;
-
-use PVE::RESTHandler;
-
-use base qw(PVE::RESTHandler);
-
-my @ctypes = qw(images vztmpl iso backup);
-
-my $storage_type_enum = PVE::Storage::Plugin->lookup_types();
-
-my $api_storage_config = sub {
-    my ($cfg, $storeid) = @_;
-
-    my $scfg = dclone(PVE::Storage::storage_config($cfg, $storeid));
-    $scfg->{storage} = $storeid;
-    $scfg->{digest} = $cfg->{digest};
-    $scfg->{content} = PVE::Storage::Plugin->encode_value($scfg->{type}, 'content', $scfg->{content});
-
-    if ($scfg->{nodes}) {
-       $scfg->{nodes} = PVE::Storage::Plugin->encode_value($scfg->{type}, 'nodes', $scfg->{nodes});
-    }
-
-    return $scfg;
-};
-
-# For storages that $match->($scfg), update node restrictions to not include $node anymore and
-# in case no node remains, remove the storage altogether.
-sub cleanup_storages_for_node {
-    my ($self, $match, $node) = @_;
-
-    my $config = PVE::Storage::config();
-    my $cluster_nodes = PVE::Cluster::get_nodelist();
-
-    for my $storeid (keys $config->{ids}->%*) {
-       my $scfg = PVE::Storage::storage_config($config, $storeid);
-       next if !$match->($scfg);
-
-       my $nodes = $scfg->{nodes} || { map { $_ => 1 } $cluster_nodes->@* };
-       next if !$nodes->{$node}; # not configured on $node, so nothing to do
-       delete $nodes->{$node};
-
-       if (scalar(keys $nodes->%*) > 0) {
-           $self->update({
-               nodes => join(',', sort keys $nodes->%*),
-               storage => $storeid,
-           });
-       } else {
-           $self->delete({storage => $storeid});
-       }
-    }
-}
-
-# Decides if a storage needs to be created or updated. An update is needed, if
-# the storage has a node list configured, then the current node will be added.
-# The verify_params parameter is an array of parameter names that need to match
-# if there already is a storage config of the same name present.  This is
-# mainly intended for local storage types as certain parameters need to be the
-# same.  For exmaple 'pool' for ZFS, 'vg_name' for LVM, ...
-# Set the dryrun parameter, to only verify the parameters without updating or
-# creating the storage.
-sub create_or_update {
-    my ($self, $sid, $node, $storage_params, $verify_params, $dryrun) = @_;
-
-    my $cfg = PVE::Storage::config();
-    my $scfg = PVE::Storage::storage_config($cfg, $sid, 1);
-
-    if ($scfg) {
-       die "storage config for '${sid}' exists but no parameters to verify were provided\n"
-           if !$verify_params;
-
-       $node = PVE::INotify::nodename() if !$node || ($node eq 'localhost');
-       die "Storage ID '${sid}' already exists on node ${node}\n"
-           if !defined($scfg->{nodes}) || $scfg->{nodes}->{$node};
-
-       push @$verify_params, 'type';
-       for my $key (@$verify_params) {
-           if (!defined($scfg->{$key})) {
-               die "Option '${key}' is not configured for storage '$sid', "
-                   ."expected it to be '$storage_params->{$key}'";
-           }
-           if ($storage_params->{$key} ne $scfg->{$key}) {
-               die "Option '${key}' ($storage_params->{$key}) does not match "
-                   ."existing storage configuration '$scfg->{$key}'\n";
-           }
-       }
-    }
-
-    if (!$dryrun) {
-       if ($scfg) {
-           if ($scfg->{nodes}) {
-               $scfg->{nodes}->{$node} = 1;
-               $self->update({
-                   nodes => join(',', sort keys $scfg->{nodes}->%*),
-                   storage => $sid,
-               });
-               print "Added '${node}' to nodes for storage '${sid}'\n";
-           }
-       } else {
-           $self->create($storage_params);
-       }
-    }
-}
-
-__PACKAGE__->register_method ({
-    name => 'index',
-    path => '',
-    method => 'GET',
-    description => "Storage index.",
-    permissions => {
-       description => "Only list entries where you have 'Datastore.Audit' or 'Datastore.AllocateSpace' permissions on '/storage/<storage>'",
-       user => 'all',
-    },
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           type => {
-               description => "Only list storage of specific type",
-               type => 'string',
-               enum => $storage_type_enum,
-               optional => 1,
-           },
-       },
-    },
-    returns => {
-       type => 'array',
-       items => {
-           type => "object",
-           properties => { storage => { type => 'string'} },
-       },
-       links => [ { rel => 'child', href => "{storage}" } ],
-    },
-    code => sub {
-       my ($param) = @_;
-
-       my $rpcenv = PVE::RPCEnvironment::get();
-       my $authuser = $rpcenv->get_user();
-
-       my $cfg = PVE::Storage::config();
-
-       my @sids = PVE::Storage::storage_ids($cfg);
-
-       my $res = [];
-       foreach my $storeid (@sids) {
-           my $privs = [ 'Datastore.Audit', 'Datastore.AllocateSpace' ];
-           next if !$rpcenv->check_any($authuser, "/storage/$storeid", $privs, 1);
-
-           my $scfg = &$api_storage_config($cfg, $storeid);
-           next if $param->{type} && $param->{type} ne $scfg->{type};
-           push @$res, $scfg;
-       }
-
-       return $res;
-    }});
-
-__PACKAGE__->register_method ({
-    name => 'read',
-    path => '{storage}',
-    method => 'GET',
-    description => "Read storage configuration.",
-    permissions => {
-       check => ['perm', '/storage/{storage}', ['Datastore.Allocate']],
-    },
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           storage => get_standard_option('pve-storage-id'),
-       },
-    },
-    returns => { type => 'object' },
-    code => sub {
-       my ($param) = @_;
-
-       my $cfg = PVE::Storage::config();
-
-       return &$api_storage_config($cfg, $param->{storage});
-    }});
-
-my $sensitive_params = [qw(password encryption-key master-pubkey keyring)];
-
-__PACKAGE__->register_method ({
-    name => 'create',
-    protected => 1,
-    path => '',
-    method => 'POST',
-    description => "Create a new storage.",
-    permissions => {
-       check => ['perm', '/storage', ['Datastore.Allocate']],
-    },
-    parameters => PVE::Storage::Plugin->createSchema(),
-    returns => {
-       type => 'object',
-       properties => {
-           storage => {
-               description => "The ID of the created storage.",
-               type => 'string',
-           },
-           type => {
-               description => "The type of the created storage.",
-               type => 'string',
-               enum => $storage_type_enum,
-           },
-           config => {
-               description => "Partial, possible server generated, configuration properties.",
-               type => 'object',
-               optional => 1,
-               additionalProperties => 1,
-               properties => {
-                   'encryption-key' => {
-                       description => "The, possible auto-generated, encryption-key.",
-                       optional => 1,
-                       type => 'string',
-                   },
-               },
-           },
-       },
-    },
-    code => sub {
-       my ($param) = @_;
-
-       my $type = extract_param($param, 'type');
-       my $storeid = extract_param($param, 'storage');
-
-       # revent an empty nodelist.
-       # fix me in section config create never need an empty entity.
-       delete $param->{nodes} if !$param->{nodes};
-
-       my $sensitive = extract_sensitive_params($param, $sensitive_params, []);
-
-       my $plugin = PVE::Storage::Plugin->lookup($type);
-       my $opts = $plugin->check_config($storeid, $param, 1, 1);
-
-       my $returned_config;
-       PVE::Storage::lock_storage_config(sub {
-           my $cfg = PVE::Storage::config();
-
-           if (my $scfg = PVE::Storage::storage_config($cfg, $storeid, 1)) {
-               die "storage ID '$storeid' already defined\n";
-           }
-
-           $cfg->{ids}->{$storeid} = $opts;
-
-           $returned_config = $plugin->on_add_hook($storeid, $opts, %$sensitive);
-
-           eval {
-               # try to activate if enabled on local node,
-               # we only do this to detect errors/problems sooner
-               if (PVE::Storage::storage_check_enabled($cfg, $storeid, undef, 1)) {
-                   PVE::Storage::activate_storage($cfg, $storeid);
-               }
-           };
-           if (my $err = $@) {
-               eval { $plugin->on_delete_hook($storeid, $opts) };
-               warn "$@\n" if $@;
-               die $err;
-           }
-
-           PVE::Storage::write_config($cfg);
-
-       }, "create storage failed");
-
-       my $res = {
-           storage => $storeid,
-           type => $type,
-       };
-       $res->{config} = $returned_config if $returned_config;
-       return $res;
-    }});
-
-__PACKAGE__->register_method ({
-    name => 'update',
-    protected => 1,
-    path => '{storage}',
-    method => 'PUT',
-    description => "Update storage configuration.",
-    permissions => {
-       check => ['perm', '/storage', ['Datastore.Allocate']],
-    },
-    parameters => PVE::Storage::Plugin->updateSchema(),
-    returns => {
-       type => 'object',
-       properties => {
-           storage => {
-               description => "The ID of the created storage.",
-               type => 'string',
-           },
-           type => {
-               description => "The type of the created storage.",
-               type => 'string',
-               enum => $storage_type_enum,
-           },
-           config => {
-               description => "Partial, possible server generated, configuration properties.",
-               type => 'object',
-               optional => 1,
-               additionalProperties => 1,
-               properties => {
-                   'encryption-key' => {
-                       description => "The, possible auto-generated, encryption-key.",
-                       optional => 1,
-                       type => 'string',
-                   },
-               },
-           },
-       },
-    },
-    code => sub {
-       my ($param) = @_;
-
-       my $storeid = extract_param($param, 'storage');
-       my $digest = extract_param($param, 'digest');
-       my $delete = extract_param($param, 'delete');
-       my $type;
-
-       if ($delete) {
-           $delete = [ PVE::Tools::split_list($delete) ];
-       }
-
-       my $returned_config;
-        PVE::Storage::lock_storage_config(sub {
-           my $cfg = PVE::Storage::config();
-
-           PVE::SectionConfig::assert_if_modified($cfg, $digest);
-
-           my $scfg = PVE::Storage::storage_config($cfg, $storeid);
-           $type = $scfg->{type};
-
-           my $sensitive = extract_sensitive_params($param, $sensitive_params, $delete);
-
-           my $plugin = PVE::Storage::Plugin->lookup($type);
-           my $opts = $plugin->check_config($storeid, $param, 0, 1);
-
-           if ($delete) {
-               my $options = $plugin->private()->{options}->{$type};
-               foreach my $k (@$delete) {
-                   my $d = $options->{$k} || die "no such option '$k'\n";
-                   die "unable to delete required option '$k'\n" if !$d->{optional};
-                   die "unable to delete fixed option '$k'\n" if $d->{fixed};
-                   die "cannot set and delete property '$k' at the same time!\n"
-                       if defined($opts->{$k});
-
-                   delete $scfg->{$k};
-               }
-           }
-
-           $returned_config = $plugin->on_update_hook($storeid, $opts, %$sensitive);
-
-           for my $k (keys %$opts) {
-               $scfg->{$k} = $opts->{$k};
-           }
-
-           PVE::Storage::write_config($cfg);
-
-       }, "update storage failed");
-
-       my $res = {
-           storage => $storeid,
-           type => $type,
-       };
-       $res->{config} = $returned_config if $returned_config;
-       return $res;
-    }});
-
-__PACKAGE__->register_method ({
-    name => 'delete',
-    protected => 1,
-    path => '{storage}', # /storage/config/{storage}
-    method => 'DELETE',
-    description => "Delete storage configuration.",
-    permissions => {
-       check => ['perm', '/storage', ['Datastore.Allocate']],
-    },
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           storage => get_standard_option('pve-storage-id', {
-               completion => \&PVE::Storage::complete_storage,
-           }),
-       },
-    },
-    returns => { type => 'null' },
-    code => sub {
-       my ($param) = @_;
-
-       my $storeid = extract_param($param, 'storage');
-
-        PVE::Storage::lock_storage_config(sub {
-           my $cfg = PVE::Storage::config();
-
-           my $scfg = PVE::Storage::storage_config($cfg, $storeid);
-
-           die "can't remove storage - storage is used as base of another storage\n"
-               if PVE::Storage::storage_is_used($cfg, $storeid);
-
-           my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-
-           $plugin->on_delete_hook($storeid, $scfg);
-
-           delete $cfg->{ids}->{$storeid};
-
-           PVE::Storage::write_config($cfg);
-
-       }, "delete storage failed");
-
-       PVE::AccessControl::remove_storage_access($storeid);
-
-       return undef;
-    }});
-
-1;
diff --git a/PVE/API2/Storage/Content.pm b/PVE/API2/Storage/Content.pm
deleted file mode 100644 (file)
index fe0ad4a..0000000
+++ /dev/null
@@ -1,560 +0,0 @@
-package PVE::API2::Storage::Content;
-
-use strict;
-use warnings;
-use Data::Dumper;
-
-use PVE::SafeSyslog;
-use PVE::Cluster;
-use PVE::Storage;
-use PVE::INotify;
-use PVE::Exception qw(raise_param_exc);
-use PVE::RPCEnvironment;
-use PVE::RESTHandler;
-use PVE::JSONSchema qw(get_standard_option);
-use PVE::SSHInfo;
-
-use base qw(PVE::RESTHandler);
-
-__PACKAGE__->register_method ({
-    name => 'index',
-    path => '',
-    method => 'GET',
-    description => "List storage content.",
-    permissions => {
-       check => ['perm', '/storage/{storage}', ['Datastore.Audit', 'Datastore.AllocateSpace'], any => 1],
-    },
-    protected => 1,
-    proxyto => 'node',
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           storage => get_standard_option('pve-storage-id', {
-               completion => \&PVE::Storage::complete_storage_enabled,
-            }),
-           content => {
-               description => "Only list content of this type.",
-               type => 'string', format => 'pve-storage-content',
-               optional => 1,
-               completion => \&PVE::Storage::complete_content_type,
-           },
-           vmid => get_standard_option('pve-vmid', {
-               description => "Only list images for this VM",
-               optional => 1,
-               completion => \&PVE::Cluster::complete_vmid,
-           }),
-       },
-    },
-    returns => {
-       type => 'array',
-       items => {
-           type => "object",
-           properties => {
-               volid => {
-                   description => "Volume identifier.",
-                   type => 'string',
-               },
-               vmid => {
-                   description => "Associated Owner VMID.",
-                   type => 'integer',
-                   optional => 1,
-               },
-               parent => {
-                   description => "Volume identifier of parent (for linked cloned).",
-                   type => 'string',
-                   optional => 1,
-               },
-               'format' => {
-                   description => "Format identifier ('raw', 'qcow2', 'subvol', 'iso', 'tgz' ...)",
-                   type => 'string',
-               },
-               size => {
-                   description => "Volume size in bytes.",
-                   type => 'integer',
-                   renderer => 'bytes',
-               },
-               used => {
-                   description => "Used space. Please note that most storage plugins " .
-                       "do not report anything useful here.",
-                   type => 'integer',
-                   renderer => 'bytes',
-                   optional => 1,
-               },
-               ctime => {
-                   description => "Creation time (seconds since the UNIX Epoch).",
-                   type => 'integer',
-                   minimum => 0,
-                   optional => 1,
-               },
-               notes => {
-                   description => "Optional notes. If they contain multiple lines, only the first one is returned here.",
-                   type => 'string',
-                   optional => 1,
-               },
-               encrypted => {
-                   description => "If whole backup is encrypted, value is the fingerprint or '1' "
-                       ." if encrypted. Only useful for the Proxmox Backup Server storage type.",
-                   type => 'string',
-                   optional => 1,
-               },
-               verification => {
-                   description => "Last backup verification result, only useful for PBS storages.",
-                   type => 'object',
-                   properties => {
-                       state => {
-                           description => "Last backup verification state.",
-                           type => 'string',
-                       },
-                       upid => {
-                           description => "Last backup verification UPID.",
-                           type => 'string',
-                       },
-                   },
-                   optional => 1,
-               },
-               protected => {
-                   description => "Protection status. Currently only supported for backups.",
-                   type => 'boolean',
-                   optional => 1,
-               },
-           },
-       },
-       links => [ { rel => 'child', href => "{volid}" } ],
-    },
-    code => sub {
-       my ($param) = @_;
-
-       my $rpcenv = PVE::RPCEnvironment::get();
-
-       my $authuser = $rpcenv->get_user();
-
-       my $storeid = $param->{storage};
-
-       my $cfg = PVE::Storage::config();
-
-       my $vollist = PVE::Storage::volume_list($cfg, $storeid, $param->{vmid}, $param->{content});
-
-       my $res = [];
-       foreach my $item (@$vollist) {
-           eval {  PVE::Storage::check_volume_access($rpcenv, $authuser, $cfg, undef, $item->{volid}); };
-           next if $@;
-           $item->{vmid} = int($item->{vmid}) if defined($item->{vmid});
-           $item->{size} = int($item->{size}) if defined($item->{size});
-           $item->{used} = int($item->{used}) if defined($item->{used});
-           push @$res, $item;
-       }
-
-       return $res;
-    }});
-
-__PACKAGE__->register_method ({
-    name => 'create',
-    path => '',
-    method => 'POST',
-    description => "Allocate disk images.",
-    permissions => {
-       check => ['perm', '/storage/{storage}', ['Datastore.AllocateSpace']],
-    },
-    protected => 1,
-    proxyto => 'node',
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           storage => get_standard_option('pve-storage-id', {
-               completion => \&PVE::Storage::complete_storage_enabled,
-           }),
-           filename => {
-               description => "The name of the file to create.",
-               type => 'string',
-           },
-           vmid => get_standard_option('pve-vmid', {
-               description => "Specify owner VM",
-               completion => \&PVE::Cluster::complete_vmid,
-           }),
-           size => {
-               description => "Size in kilobyte (1024 bytes). Optional suffixes 'M' (megabyte, 1024K) and 'G' (gigabyte, 1024M)",
-               type => 'string',
-               pattern => '\d+[MG]?',
-           },
-           'format' => {
-               type => 'string',
-               enum => ['raw', 'qcow2', 'subvol'],
-               requires => 'size',
-               optional => 1,
-           },
-       },
-    },
-    returns => {
-       description => "Volume identifier",
-       type => 'string',
-    },
-    code => sub {
-       my ($param) = @_;
-
-       my $storeid = $param->{storage};
-       my $name = $param->{filename};
-       my $sizestr = $param->{size};
-
-       my $size;
-       if ($sizestr =~ m/^\d+$/) {
-           $size = $sizestr;
-       } elsif ($sizestr =~ m/^(\d+)M$/) {
-           $size = $1 * 1024;
-       } elsif ($sizestr =~ m/^(\d+)G$/) {
-           $size = $1 * 1024 * 1024;
-       } else {
-           raise_param_exc({ size => "unable to parse size '$sizestr'" });
-       }
-
-       # extract FORMAT from name
-       if ($name =~ m/\.(raw|qcow2|vmdk)$/) {
-           my $fmt = $1;
-
-           raise_param_exc({ format => "different storage formats ($param->{format} != $fmt)" })
-               if $param->{format} && $param->{format} ne $fmt;
-
-           $param->{format} = $fmt;
-       }
-
-       my $cfg = PVE::Storage::config();
-
-       my $volid = PVE::Storage::vdisk_alloc ($cfg, $storeid, $param->{vmid},
-                                              $param->{format},
-                                              $name, $size);
-
-       return $volid;
-    }});
-
-# we allow to pass volume names (without storage prefix) if the storage
-# is specified as separate parameter.
-my $real_volume_id = sub {
-    my ($storeid, $volume) = @_;
-
-    my $volid;
-
-    if ($volume =~ m/:/) {
-       eval {
-           my ($sid, $volname) = PVE::Storage::parse_volume_id ($volume);
-           die "storage ID mismatch ($sid != $storeid)\n"
-               if $storeid && $sid ne $storeid;
-           $volid = $volume;
-           $storeid = $sid;
-       };
-       raise_param_exc({ volume => $@ }) if $@;
-
-    } else {
-       raise_param_exc({ volume => "no storage specified - incomplete volume ID" })
-           if !$storeid;
-
-       $volid = "$storeid:$volume";
-    }
-
-    return wantarray ? ($volid, $storeid) : $volid;
-};
-
-__PACKAGE__->register_method ({
-    name => 'info',
-    path => '{volume}',
-    method => 'GET',
-    description => "Get volume attributes",
-    permissions => {
-       description => "You need read access for the volume.",
-       user => 'all',
-    },
-    protected => 1,
-    proxyto => 'node',
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           storage => get_standard_option('pve-storage-id', { optional => 1 }),
-           volume => {
-               description => "Volume identifier",
-               type => 'string',
-           },
-       },
-    },
-    returns => {
-       type => 'object',
-       properties => {
-           path => {
-               description => "The Path",
-               type => 'string',
-           },
-           size => {
-               description => "Volume size in bytes.",
-               type => 'integer',
-               renderer => 'bytes',
-           },
-           used => {
-               description => "Used space. Please note that most storage plugins " .
-               "do not report anything useful here.",
-               type => 'integer',
-               renderer => 'bytes',
-           },
-           format => {
-               description => "Format identifier ('raw', 'qcow2', 'subvol', 'iso', 'tgz' ...)",
-               type => 'string',
-           },
-           notes => {
-               description => "Optional notes.",
-               optional => 1,
-               type => 'string',
-           },
-           protected => {
-               description => "Protection status. Currently only supported for backups.",
-               type => 'boolean',
-               optional => 1,
-           },
-       },
-    },
-    code => sub {
-       my ($param) = @_;
-
-       my $rpcenv = PVE::RPCEnvironment::get();
-       my $authuser = $rpcenv->get_user();
-
-       my ($volid, $storeid) = &$real_volume_id($param->{storage}, $param->{volume});
-
-       my $cfg = PVE::Storage::config();
-
-       PVE::Storage::check_volume_access($rpcenv, $authuser, $cfg, undef, $volid);
-
-       my $path = PVE::Storage::path($cfg, $volid);
-       my ($size, $format, $used, $parent) =  PVE::Storage::volume_size_info($cfg, $volid);
-       die "volume_size_info on '$volid' failed\n" if !($format && $size);
-
-       my $entry = {
-           path => $path,
-           size => int($size), # cast to integer in case it was changed to a string previously
-           used => int($used),
-           format => $format,
-       };
-
-       for my $attribute (qw(notes protected)) {
-           # keep going if fetching an optional attribute fails
-           eval {
-               my $value = PVE::Storage::get_volume_attribute($cfg, $volid, $attribute);
-               $entry->{$attribute} = $value if defined($value);
-           };
-           warn $@ if $@;
-       }
-
-       return $entry;
-    }});
-
-__PACKAGE__->register_method ({
-    name => 'updateattributes',
-    path => '{volume}',
-    method => 'PUT',
-    description => "Update volume attributes",
-    permissions => {
-       description => "You need read access for the volume.",
-       user => 'all',
-    },
-    protected => 1,
-    proxyto => 'node',
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           storage => get_standard_option('pve-storage-id', { optional => 1 }),
-           volume => {
-               description => "Volume identifier",
-               type => 'string',
-           },
-           notes => {
-               description => "The new notes.",
-               type => 'string',
-               optional => 1,
-           },
-           protected => {
-               description => "Protection status. Currently only supported for backups.",
-               type => 'boolean',
-               optional => 1,
-           },
-       },
-    },
-    returns => { type => 'null' },
-    code => sub {
-       my ($param) = @_;
-
-       my $rpcenv = PVE::RPCEnvironment::get();
-       my $authuser = $rpcenv->get_user();
-
-       my ($volid, $storeid) = &$real_volume_id($param->{storage}, $param->{volume});
-
-       my $cfg = PVE::Storage::config();
-
-       PVE::Storage::check_volume_access($rpcenv, $authuser, $cfg, undef, $volid);
-
-       for my $attr (qw(notes protected)) {
-           if (exists $param->{$attr}) {
-               PVE::Storage::update_volume_attribute($cfg, $volid, $attr, $param->{$attr});
-           }
-       }
-
-       return undef;
-    }});
-
-__PACKAGE__->register_method ({
-    name => 'delete',
-    path => '{volume}',
-    method => 'DELETE',
-    description => "Delete volume",
-    permissions => {
-       description => "You need 'Datastore.Allocate' privilege on the storage (or 'Datastore.AllocateSpace' for backup volumes if you have VM.Backup privilege on the VM).",
-       user => 'all',
-    },
-    protected => 1,
-    proxyto => 'node',
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           storage => get_standard_option('pve-storage-id', {
-               optional => 1,
-               completion => \&PVE::Storage::complete_storage,
-           }),
-           volume => {
-               description => "Volume identifier",
-               type => 'string',
-               completion => \&PVE::Storage::complete_volume,
-           },
-           delay => {
-               type => 'integer',
-               description => "Time to wait for the task to finish. We return 'null' if the task finish within that time.",
-               minimum => 1,
-               maximum => 30,
-               optional => 1,
-           },
-       },
-    },
-    returns => { type => 'string', optional => 1, },
-    code => sub {
-       my ($param) = @_;
-
-       my $rpcenv = PVE::RPCEnvironment::get();
-       my $authuser = $rpcenv->get_user();
-
-       my $cfg = PVE::Storage::config();
-
-       my ($volid, $storeid) = &$real_volume_id($param->{storage}, $param->{volume});
-
-       my ($path, $ownervm, $vtype) = PVE::Storage::path($cfg, $volid);
-       if ($vtype eq 'backup' && $ownervm) {
-           $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
-           $rpcenv->check($authuser, "/vms/$ownervm", ['VM.Backup']);
-       } else {
-           $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.Allocate']);
-       }
-
-       my $worker = sub {
-           PVE::Storage::vdisk_free ($cfg, $volid);
-           print "Removed volume '$volid'\n";
-           if ($vtype eq 'backup'
-               && $path =~ /(.*\/vzdump-\w+-\d+-\d{4}_\d{2}_\d{2}-\d{2}_\d{2}_\d{2})[^\/]+$/) {
-               # Remove log file #318 and notes file #3972 if they still exist
-               PVE::Storage::archive_auxiliaries_remove($path);
-           }
-       };
-
-       my $id = (defined $ownervm ? "$ownervm@" : '') . $storeid;
-       my $upid = $rpcenv->fork_worker('imgdel', $id, $authuser, $worker);
-       my $background_delay = $param->{delay};
-       if ($background_delay) {
-           my $end_time = time() + $background_delay;
-           my $currently_deleting; # not necessarily true, e.g. sequential api call from cli
-           do {
-               my $task = PVE::Tools::upid_decode($upid);
-               $currently_deleting = PVE::ProcFSTools::check_process_running($task->{pid}, $task->{pstart});
-               sleep 1 if $currently_deleting;
-           } while (time() < $end_time && $currently_deleting);
-
-           if (!$currently_deleting) {
-               my $status = PVE::Tools::upid_read_status($upid);
-               chomp $status;
-               return undef if !PVE::Tools::upid_status_is_error($status);
-               die "$status\n";
-           }
-       }
-       return $upid;
-    }});
-
-__PACKAGE__->register_method ({
-    name => 'copy',
-    path => '{volume}',
-    method => 'POST',
-    description => "Copy a volume. This is experimental code - do not use.",
-    protected => 1,
-    proxyto => 'node',
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           storage => get_standard_option('pve-storage-id', { optional => 1}),
-           volume => {
-               description => "Source volume identifier",
-               type => 'string',
-           },
-           target => {
-               description => "Target volume identifier",
-               type => 'string',
-           },
-           target_node => get_standard_option('pve-node',  {
-               description => "Target node. Default is local node.",
-               optional => 1,
-           }),
-       },
-    },
-    returns => {
-       type => 'string',
-    },
-    code => sub {
-       my ($param) = @_;
-
-       my $rpcenv = PVE::RPCEnvironment::get();
-
-       my $user = $rpcenv->get_user();
-
-       my $target_node = $param->{target_node} || PVE::INotify::nodename();
-       # pvesh examples
-       # cd /nodes/localhost/storage/local/content
-       # pve:/> create local:103/vm-103-disk-1.raw -target local:103/vm-103-disk-2.raw
-       # pve:/> create 103/vm-103-disk-1.raw -target 103/vm-103-disk-3.raw
-
-       my $src_volid = &$real_volume_id($param->{storage}, $param->{volume});
-       my $dst_volid = &$real_volume_id($param->{storage}, $param->{target});
-
-       print "DEBUG: COPY $src_volid TO $dst_volid\n";
-
-       my $cfg = PVE::Storage::config();
-
-       # do all parameter checks first
-
-       # then do all short running task (to raise errors before we go to background)
-
-       # then start the worker task
-       my $worker = sub  {
-           my $upid = shift;
-
-           print "DEBUG: starting worker $upid\n";
-
-           my ($target_sid, $target_volname) = PVE::Storage::parse_volume_id($dst_volid);
-           #my $target_ip = PVE::Cluster::remote_node_ip($target_node);
-
-           # you need to get this working (fails currently, because storage_migrate() uses
-           # ssh to connect to local host (which is not needed
-           my $sshinfo = PVE::SSHInfo::get_ssh_info($target_node);
-           PVE::Storage::storage_migrate($cfg, $src_volid, $sshinfo, $target_sid, {'target_volname' => $target_volname});
-
-           print "DEBUG: end worker $upid\n";
-
-       };
-
-       return $rpcenv->fork_worker('imgcopy', undef, $user, $worker);
-    }});
-
-1;
diff --git a/PVE/API2/Storage/FileRestore.pm b/PVE/API2/Storage/FileRestore.pm
deleted file mode 100644 (file)
index 764ebfb..0000000
+++ /dev/null
@@ -1,215 +0,0 @@
-package PVE::API2::Storage::FileRestore;
-
-use strict;
-use warnings;
-
-use MIME::Base64;
-use PVE::Exception qw(raise_param_exc);
-use PVE::JSONSchema qw(get_standard_option);
-use PVE::PBSClient;
-use PVE::Storage;
-use PVE::Tools qw(extract_param);
-
-use PVE::RESTHandler;
-use base qw(PVE::RESTHandler);
-
-my $parse_volname_or_id = sub {
-    my ($storeid, $volume) = @_;
-
-    my $volid;
-    my ($sid, $volname) = PVE::Storage::parse_volume_id($volume, 1);
-
-    if (defined($sid)) {
-       raise_param_exc({ volume => "storage ID mismatch ($sid != $storeid)." })
-           if $sid ne $storeid;
-
-       $volid = $volume;
-    } elsif ($volume =~ m/^backup\//) {
-       $volid = "$storeid:$volume";
-    } else {
-       $volid = "$storeid:backup/$volume";
-    }
-
-    return $volid;
-};
-
-__PACKAGE__->register_method ({
-    name => 'list',
-    path => 'list',
-    method => 'GET',
-    proxyto => 'node',
-    permissions => {
-       description => "You need read access for the volume.",
-       user => 'all',
-    },
-    description => "List files and directories for single file restore under the given path.",
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           storage => get_standard_option('pve-storage-id', {
-               completion => \&PVE::Storage::complete_storage_enabled,
-           }),
-           volume => {
-               description => "Backup volume ID or name. Currently only PBS snapshots are supported.",
-               type => 'string',
-               completion => \&PVE::Storage::complete_volume,
-           },
-           filepath => {
-               description => 'base64-path to the directory or file being listed, or "/".',
-               type => 'string',
-           },
-       },
-    },
-    returns => {
-       type => 'array',
-       items => {
-           type => "object",
-           properties => {
-               filepath => {
-                   description => "base64 path of the current entry",
-                   type => 'string',
-               },
-               type => {
-                   description => "Entry type.",
-                   type => 'string',
-               },
-               text => {
-                   description => "Entry display text.",
-                   type => 'string',
-               },
-               leaf => {
-                   description => "If this entry is a leaf in the directory graph.",
-                   type => 'boolean',
-               },
-               size => {
-                   description => "Entry file size.",
-                   type => 'integer',
-                   optional => 1,
-               },
-               mtime => {
-                   description => "Entry last-modified time (unix timestamp).",
-                   type => 'integer',
-                   optional => 1,
-               },
-           },
-       },
-    },
-    protected => 1,
-    code => sub {
-       my ($param) = @_;
-
-       my $rpcenv = PVE::RPCEnvironment::get();
-       my $user = $rpcenv->get_user();
-
-       my $path = extract_param($param, 'filepath') || "/";
-       my $base64 = $path ne "/";
-
-       my $storeid = extract_param($param, 'storage');
-
-       my $volid = $parse_volname_or_id->($storeid, $param->{volume});
-       my $cfg = PVE::Storage::config();
-       my $scfg = PVE::Storage::storage_config($cfg, $storeid);
-
-       PVE::Storage::check_volume_access($rpcenv, $user, $cfg, undef, $volid, 'backup');
-
-       raise_param_exc({'storage' => "Only PBS storages supported for file-restore."})
-           if $scfg->{type} ne 'pbs';
-
-       my (undef, $snap) = PVE::Storage::parse_volname($cfg, $volid);
-
-       my $client = PVE::PBSClient->new($scfg, $storeid);
-       my $ret = $client->file_restore_list($snap, $path, $base64, { timeout => 25 });
-
-       if (ref($ret) eq "HASH") {
-           my $msg = $ret->{message};
-           if (my $code = $ret->{code}) {
-               die PVE::Exception->new("$msg\n", code => $code);
-           } else {
-               die "$msg\n";
-           }
-       } elsif (ref($ret) eq "ARRAY") {
-           # 'leaf' is a proper JSON boolean, map to perl-y bool
-           # TODO: make PBSClient decode all bools always as 1/0?
-           foreach my $item (@$ret) {
-               $item->{leaf} = $item->{leaf} ? 1 : 0;
-           }
-
-           return $ret;
-       }
-
-       die "invalid proxmox-file-restore output";
-    }});
-
-__PACKAGE__->register_method ({
-    name => 'download',
-    path => 'download',
-    method => 'GET',
-    proxyto => 'node',
-    permissions => {
-       description => "You need read access for the volume.",
-       user => 'all',
-    },
-    description => "Extract a file or directory (as zip archive) from a PBS backup.",
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           storage => get_standard_option('pve-storage-id', {
-               completion => \&PVE::Storage::complete_storage_enabled,
-           }),
-           volume => {
-               description => "Backup volume ID or name. Currently only PBS snapshots are supported.",
-               type => 'string',
-               completion => \&PVE::Storage::complete_volume,
-           },
-           filepath => {
-               description => 'base64-path to the directory or file to download.',
-               type => 'string',
-           },
-       },
-    },
-    returns => {
-       type => 'any', # download
-    },
-    protected => 1,
-    code => sub {
-       my ($param) = @_;
-
-       my $rpcenv = PVE::RPCEnvironment::get();
-       my $user = $rpcenv->get_user();
-
-       my $path = extract_param($param, 'filepath');
-       my $storeid = extract_param($param, 'storage');
-       my $volid = $parse_volname_or_id->($storeid, $param->{volume});
-
-       my $cfg = PVE::Storage::config();
-       my $scfg = PVE::Storage::storage_config($cfg, $storeid);
-
-       PVE::Storage::check_volume_access($rpcenv, $user, $cfg, undef, $volid, 'backup');
-
-       raise_param_exc({'storage' => "Only PBS storages supported for file-restore."})
-           if $scfg->{type} ne 'pbs';
-
-       my (undef, $snap) = PVE::Storage::parse_volname($cfg, $volid);
-
-       my $client = PVE::PBSClient->new($scfg, $storeid);
-       my $fifo = $client->file_restore_extract_prepare();
-
-       $rpcenv->fork_worker('pbs-download', undef, $user, sub {
-           my $name = decode_base64($path);
-           print "Starting download of file: $name\n";
-           $client->file_restore_extract($fifo, $snap, $path, 1);
-       });
-
-       my $ret = {
-           download => {
-               path => $fifo,
-               stream => 1,
-               'content-type' => 'application/octet-stream',
-           },
-       };
-       return $ret;
-    }});
-
-1;
diff --git a/PVE/API2/Storage/Makefile b/PVE/API2/Storage/Makefile
deleted file mode 100644 (file)
index 1705080..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-
-SOURCES= Content.pm Status.pm Config.pm PruneBackups.pm Scan.pm FileRestore.pm
-
-.PHONY: install
-install:
-       for i in ${SOURCES}; do install -D -m 0644 $$i ${DESTDIR}${PERLDIR}/PVE/API2/Storage/$$i; done
diff --git a/PVE/API2/Storage/PruneBackups.pm b/PVE/API2/Storage/PruneBackups.pm
deleted file mode 100644 (file)
index e6ab276..0000000
+++ /dev/null
@@ -1,164 +0,0 @@
-package PVE::API2::Storage::PruneBackups;
-
-use strict;
-use warnings;
-
-use PVE::Cluster;
-use PVE::JSONSchema qw(get_standard_option);
-use PVE::RESTHandler;
-use PVE::RPCEnvironment;
-use PVE::Storage;
-use PVE::Tools qw(extract_param);
-
-use base qw(PVE::RESTHandler);
-
-__PACKAGE__->register_method ({
-    name => 'dryrun',
-    path => '',
-    method => 'GET',
-    description => "Get prune information for backups. NOTE: this is only a preview and might not be " .
-                  "what a subsequent prune call does if backups are removed/added in the meantime.",
-    permissions => {
-       check => ['perm', '/storage/{storage}', ['Datastore.Audit', 'Datastore.AllocateSpace'], any => 1],
-    },
-    protected => 1,
-    proxyto => 'node',
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           storage => get_standard_option('pve-storage-id', {
-               completion => \&PVE::Storage::complete_storage_enabled,
-            }),
-           'prune-backups' => get_standard_option('prune-backups', {
-               description => "Use these retention options instead of those from the storage configuration.",
-               optional => 1,
-           }),
-           type => {
-               description => "Either 'qemu' or 'lxc'. Only consider backups for guests of this type.",
-               type => 'string',
-               optional => 1,
-               enum => ['qemu', 'lxc'],
-           },
-           vmid => get_standard_option('pve-vmid', {
-               description => "Only consider backups for this guest.",
-               optional => 1,
-               completion => \&PVE::Cluster::complete_vmid,
-           }),
-       },
-    },
-    returns => {
-       type => 'array',
-       items => {
-           type => 'object',
-           properties => {
-               volid => {
-                   description => "Backup volume ID.",
-                   type => 'string',
-               },
-               'ctime' => {
-                   description => "Creation time of the backup (seconds since the UNIX epoch).",
-                   type => 'integer',
-               },
-               'mark' => {
-                   description => "Whether the backup would be kept or removed. Backups that are" .
-                       " protected or don't use the standard naming scheme are not removed.",
-                   type => 'string',
-                   enum => ['keep', 'remove', 'protected', 'renamed'],
-               },
-               type => {
-                   description => "One of 'qemu', 'lxc', 'openvz' or 'unknown'.",
-                   type => 'string',
-               },
-               'vmid' => {
-                   description => "The VM the backup belongs to.",
-                   type => 'integer',
-                   optional => 1,
-               },
-           },
-       },
-    },
-    code => sub {
-       my ($param) = @_;
-
-       my $cfg = PVE::Storage::config();
-
-       my $vmid = extract_param($param, 'vmid');
-       my $type = extract_param($param, 'type');
-       my $storeid = extract_param($param, 'storage');
-
-       my $prune_backups = extract_param($param, 'prune-backups');
-       $prune_backups = PVE::JSONSchema::parse_property_string('prune-backups', $prune_backups)
-           if defined($prune_backups);
-
-       return PVE::Storage::prune_backups($cfg, $storeid, $prune_backups, $vmid, $type, 1);
-    }});
-
-__PACKAGE__->register_method ({
-    name => 'delete',
-    path => '',
-    method => 'DELETE',
-    description => "Prune backups. Only those using the standard naming scheme are considered.",
-    permissions => {
-       description => "You need the 'Datastore.Allocate' privilege on the storage " .
-                      "(or if a VM ID is specified, 'Datastore.AllocateSpace' and 'VM.Backup' for the VM).",
-       user => 'all',
-    },
-    protected => 1,
-    proxyto => 'node',
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           storage => get_standard_option('pve-storage-id', {
-                completion => \&PVE::Storage::complete_storage,
-            }),
-           'prune-backups' => get_standard_option('prune-backups', {
-               description => "Use these retention options instead of those from the storage configuration.",
-           }),
-           type => {
-               description => "Either 'qemu' or 'lxc'. Only consider backups for guests of this type.",
-               type => 'string',
-               optional => 1,
-               enum => ['qemu', 'lxc'],
-           },
-           vmid => get_standard_option('pve-vmid', {
-               description => "Only prune backups for this VM.",
-               completion => \&PVE::Cluster::complete_vmid,
-               optional => 1,
-           }),
-       },
-    },
-    returns => { type => 'string' },
-    code => sub {
-       my ($param) = @_;
-
-       my $rpcenv = PVE::RPCEnvironment::get();
-       my $authuser = $rpcenv->get_user();
-
-       my $cfg = PVE::Storage::config();
-
-       my $vmid = extract_param($param, 'vmid');
-       my $type = extract_param($param, 'type');
-       my $storeid = extract_param($param, 'storage');
-
-       my $prune_backups = extract_param($param, 'prune-backups');
-       $prune_backups = PVE::JSONSchema::parse_property_string('prune-backups', $prune_backups)
-           if defined($prune_backups);
-
-       if (defined($vmid)) {
-           $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
-           $rpcenv->check($authuser, "/vms/$vmid", ['VM.Backup']);
-       } else {
-           $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.Allocate']);
-       }
-
-       my $id = (defined($vmid) ? "$vmid@" : '') . $storeid;
-       my $worker = sub {
-           PVE::Storage::prune_backups($cfg, $storeid, $prune_backups, $vmid, $type, 0);
-       };
-
-       return $rpcenv->fork_worker('prunebackups', $id, $authuser, $worker);
-    }});
-
-1;
diff --git a/PVE/API2/Storage/Scan.pm b/PVE/API2/Storage/Scan.pm
deleted file mode 100644 (file)
index d7a8743..0000000
+++ /dev/null
@@ -1,449 +0,0 @@
-package PVE::API2::Storage::Scan;
-
-use strict;
-use warnings;
-
-# NOTE: This API endpoints are mounted by pve-manager's API2::Node module and pvesm CLI
-
-use PVE::JSONSchema qw(get_standard_option);
-use PVE::RESTHandler;
-use PVE::SafeSyslog;
-use PVE::Storage::LVMPlugin;
-use PVE::Storage;
-use PVE::SysFSTools;
-
-use base qw(PVE::RESTHandler);
-
-__PACKAGE__->register_method({
-    name => 'index',
-    path => '',
-    method => 'GET',
-    description => "Index of available scan methods",
-    permissions => {
-       user => 'all',
-    },
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-       },
-    },
-    returns => {
-       type => 'array',
-       items => {
-           type => "object",
-           properties => {
-               method => { type => 'string'},
-           },
-       },
-       links => [ { rel => 'child', href => "{method}" } ],
-    },
-    code => sub {
-       my ($param) = @_;
-
-       my $res = [
-           { method => 'cifs' },
-           { method => 'glusterfs' },
-           { method => 'iscsi' },
-           { method => 'lvm' },
-           { method => 'nfs' },
-           { method => 'pbs' },
-           { method => 'zfs' },
-       ];
-
-       return $res;
-    }});
-
-__PACKAGE__->register_method({
-    name => 'nfsscan',
-    path => 'nfs',
-    method => 'GET',
-    description => "Scan remote NFS server.",
-    protected => 1,
-    proxyto => "node",
-    permissions => {
-       check => ['perm', '/storage', ['Datastore.Allocate']],
-    },
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           server => {
-               description => "The server address (name or IP).",
-               type => 'string', format => 'pve-storage-server',
-           },
-       },
-    },
-    returns => {
-       type => 'array',
-       items => {
-           type => "object",
-           properties => {
-               path => {
-                   description => "The exported path.",
-                   type => 'string',
-               },
-               options => {
-                   description => "NFS export options.",
-                   type => 'string',
-               },
-           },
-       },
-    },
-    code => sub {
-       my ($param) = @_;
-
-       my $server = $param->{server};
-       my $res = PVE::Storage::scan_nfs($server);
-
-       my $data = [];
-       foreach my $k (sort keys %$res) {
-           push @$data, { path => $k, options => $res->{$k} };
-       }
-       return $data;
-    }});
-
-__PACKAGE__->register_method({
-    name => 'cifsscan',
-    path => 'cifs',
-    method => 'GET',
-    description => "Scan remote CIFS server.",
-    protected => 1,
-    proxyto => "node",
-    permissions => {
-       check => ['perm', '/storage', ['Datastore.Allocate']],
-    },
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           server => {
-               description => "The server address (name or IP).",
-               type => 'string', format => 'pve-storage-server',
-           },
-           username => {
-               description => "User name.",
-               type => 'string',
-               optional => 1,
-           },
-           password => {
-               description => "User password.",
-               type => 'string',
-               optional => 1,
-           },
-           domain => {
-               description => "SMB domain (Workgroup).",
-               type => 'string',
-               optional => 1,
-           },
-       },
-    },
-    returns => {
-       type => 'array',
-       items => {
-           type => "object",
-           properties => {
-               share => {
-                   description => "The cifs share name.",
-                   type => 'string',
-               },
-               description => {
-                   description => "Descriptive text from server.",
-                   type => 'string',
-               },
-           },
-       },
-    },
-    code => sub {
-       my ($param) = @_;
-
-       my $server = $param->{server};
-
-       my $username = $param->{username};
-       my $password = $param->{password};
-       my $domain = $param->{domain};
-
-       my $res = PVE::Storage::scan_cifs($server, $username, $password, $domain);
-
-       my $data = [];
-       foreach my $k (sort keys %$res) {
-           next if $k =~ m/NT_STATUS_/;
-           push @$data, { share => $k, description => $res->{$k} };
-       }
-
-       return $data;
-    }});
-
-__PACKAGE__->register_method({
-    name => 'pbsscan',
-    path => 'pbs',
-    method => 'GET',
-    description => "Scan remote Proxmox Backup Server.",
-    protected => 1,
-    proxyto => "node",
-    permissions => {
-       check => ['perm', '/storage', ['Datastore.Allocate']],
-    },
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           server => {
-               description => "The server address (name or IP).",
-               type => 'string', format => 'pve-storage-server',
-           },
-           username => {
-               description => "User-name or API token-ID.",
-               type => 'string',
-           },
-           password => {
-               description => "User password or API token secret.",
-               type => 'string',
-           },
-           fingerprint => get_standard_option('fingerprint-sha256', {
-               optional => 1,
-           }),
-           port => {
-               description => "Optional port.",
-               type => 'integer',
-               minimum => 1,
-               maximum => 65535,
-               default => 8007,
-               optional => 1,
-           },
-       },
-    },
-    returns => {
-       type => 'array',
-       items => {
-           type => "object",
-           properties => {
-               store => {
-                   description => "The datastore name.",
-                   type => 'string',
-               },
-               comment => {
-                   description => "Comment from server.",
-                   type => 'string',
-                   optional => 1,
-               },
-           },
-       },
-    },
-    code => sub {
-       my ($param) = @_;
-
-       my $password = delete $param->{password};
-
-       return PVE::Storage::PBSPlugin::scan_datastores($param, $password);
-    }
-});
-
-# Note: GlusterFS currently does not have an equivalent of showmount.
-# As workaround, we simply use nfs showmount.
-# see http://www.gluster.org/category/volumes/
-__PACKAGE__->register_method({
-    name => 'glusterfsscan',
-    path => 'glusterfs',
-    method => 'GET',
-    description => "Scan remote GlusterFS server.",
-    protected => 1,
-    proxyto => "node",
-    permissions => {
-       check => ['perm', '/storage', ['Datastore.Allocate']],
-    },
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           server => {
-               description => "The server address (name or IP).",
-               type => 'string', format => 'pve-storage-server',
-           },
-       },
-    },
-    returns => {
-       type => 'array',
-       items => {
-           type => "object",
-           properties => {
-               volname => {
-                   description => "The volume name.",
-                   type => 'string',
-               },
-           },
-       },
-    },
-    code => sub {
-       my ($param) = @_;
-
-       my $server = $param->{server};
-       my $res = PVE::Storage::scan_nfs($server);
-
-       my $data = [];
-       foreach my $path (sort keys %$res) {
-           if ($path =~ m!^/([^\s/]+)$!) {
-               push @$data, { volname => $1 };
-           }
-       }
-       return $data;
-    }});
-
-__PACKAGE__->register_method({
-    name => 'iscsiscan',
-    path => 'iscsi',
-    method => 'GET',
-    description => "Scan remote iSCSI server.",
-    protected => 1,
-    proxyto => "node",
-    permissions => {
-       check => ['perm', '/storage', ['Datastore.Allocate']],
-    },
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           portal => {
-               description => "The iSCSI portal (IP or DNS name with optional port).",
-               type => 'string', format => 'pve-storage-portal-dns',
-           },
-       },
-    },
-    returns => {
-       type => 'array',
-       items => {
-           type => "object",
-           properties => {
-               target => {
-                   description => "The iSCSI target name.",
-                   type => 'string',
-               },
-               portal => {
-                   description => "The iSCSI portal name.",
-                   type => 'string',
-               },
-           },
-       },
-    },
-    code => sub {
-       my ($param) = @_;
-
-       my $res = PVE::Storage::scan_iscsi($param->{portal});
-
-       my $data = [];
-       foreach my $k (sort keys %$res) {
-           push @$data, { target => $k, portal => join(',', @{$res->{$k}}) };
-       }
-
-       return $data;
-    }});
-
-__PACKAGE__->register_method({
-    name => 'lvmscan',
-    path => 'lvm',
-    method => 'GET',
-    description => "List local LVM volume groups.",
-    protected => 1,
-    proxyto => "node",
-    permissions => {
-       check => ['perm', '/storage', ['Datastore.Allocate']],
-    },
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-       },
-    },
-    returns => {
-       type => 'array',
-       items => {
-           type => "object",
-           properties => {
-               vg => {
-                   description => "The LVM logical volume group name.",
-                   type => 'string',
-               },
-           },
-       },
-    },
-    code => sub {
-       my ($param) = @_;
-
-       my $res = PVE::Storage::LVMPlugin::lvm_vgs();
-       return PVE::RESTHandler::hash_to_array($res, 'vg');
-    }});
-
-__PACKAGE__->register_method({
-    name => 'lvmthinscan',
-    path => 'lvmthin',
-    method => 'GET',
-    description => "List local LVM Thin Pools.",
-    protected => 1,
-    proxyto => "node",
-    permissions => {
-       check => ['perm', '/storage', ['Datastore.Allocate']],
-    },
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           vg => {
-               type => 'string',
-               pattern => '[a-zA-Z0-9\.\+\_][a-zA-Z0-9\.\+\_\-]+', # see lvm(8) manpage
-               maxLength => 100,
-           },
-       },
-    },
-    returns => {
-       type => 'array',
-       items => {
-           type => "object",
-           properties => {
-               lv => {
-                   description => "The LVM Thin Pool name (LVM logical volume).",
-                   type => 'string',
-               },
-           },
-       },
-    },
-    code => sub {
-       my ($param) = @_;
-
-       return PVE::Storage::LvmThinPlugin::list_thinpools($param->{vg});
-    }});
-
-__PACKAGE__->register_method({
-    name => 'zfsscan',
-    path => 'zfs',
-    method => 'GET',
-    description => "Scan zfs pool list on local node.",
-    protected => 1,
-    proxyto => "node",
-    permissions => {
-       check => ['perm', '/storage', ['Datastore.Allocate']],
-    },
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-       },
-    },
-    returns => {
-       type => 'array',
-       items => {
-           type => "object",
-           properties => {
-               pool => {
-                   description => "ZFS pool name.",
-                   type => 'string',
-               },
-           },
-       },
-    },
-    code => sub {
-       my ($param) = @_;
-
-       return PVE::Storage::scan_zfs();
-    }});
-
-1;
diff --git a/PVE/API2/Storage/Status.pm b/PVE/API2/Storage/Status.pm
deleted file mode 100644 (file)
index e4ce698..0000000
+++ /dev/null
@@ -1,660 +0,0 @@
-package PVE::API2::Storage::Status;
-
-use strict;
-use warnings;
-
-use File::Basename;
-use File::Path;
-use POSIX qw(ENOENT);
-
-use PVE::Cluster;
-use PVE::Exception qw(raise_param_exc);
-use PVE::INotify;
-use PVE::JSONSchema qw(get_standard_option);
-use PVE::RESTHandler;
-use PVE::RPCEnvironment;
-use PVE::RRD;
-use PVE::Tools qw(run_command);
-
-use PVE::API2::Storage::Content;
-use PVE::API2::Storage::FileRestore;
-use PVE::API2::Storage::PruneBackups;
-use PVE::Storage;
-
-use base qw(PVE::RESTHandler);
-
-__PACKAGE__->register_method ({
-    subclass => "PVE::API2::Storage::PruneBackups",
-    path => '{storage}/prunebackups',
-});
-
-__PACKAGE__->register_method ({
-    subclass => "PVE::API2::Storage::Content",
-    # set fragment delimiter (no subdirs) - we need that, because volume
-    # IDs may contain a slash '/'
-    fragmentDelimiter => '',
-    path => '{storage}/content',
-});
-
-__PACKAGE__->register_method ({
-   subclass => "PVE::API2::Storage::FileRestore",
-   path => '{storage}/file-restore',
-});
-
-__PACKAGE__->register_method ({
-    name => 'index',
-    path => '',
-    method => 'GET',
-    description => "Get status for all datastores.",
-    permissions => {
-       description => "Only list entries where you have 'Datastore.Audit' or 'Datastore.AllocateSpace' permissions on '/storage/<storage>'",
-       user => 'all',
-    },
-    protected => 1,
-    proxyto => 'node',
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           storage => get_standard_option('pve-storage-id', {
-               description => "Only list status for  specified storage",
-               optional => 1,
-               completion => \&PVE::Storage::complete_storage_enabled,
-           }),
-           content => {
-               description => "Only list stores which support this content type.",
-               type => 'string', format => 'pve-storage-content-list',
-               optional => 1,
-               completion => \&PVE::Storage::complete_content_type,
-           },
-           enabled => {
-               description => "Only list stores which are enabled (not disabled in config).",
-               type => 'boolean',
-               optional => 1,
-               default => 0,
-           },
-           target => get_standard_option('pve-node', {
-               description => "If target is different to 'node', we only lists shared storages which " .
-                   "content is accessible on this 'node' and the specified 'target' node.",
-               optional => 1,
-               completion => \&PVE::Cluster::get_nodelist,
-           }),
-           'format' => {
-               description => "Include information about formats",
-               type => 'boolean',
-               optional => 1,
-               default => 0,
-           },
-       },
-    },
-    returns => {
-       type => 'array',
-       items => {
-           type => "object",
-           properties => {
-               storage => get_standard_option('pve-storage-id'),
-               type => {
-                   description => "Storage type.",
-                   type => 'string',
-               },
-               content => {
-                   description => "Allowed storage content types.",
-                   type => 'string', format => 'pve-storage-content-list',
-               },
-               enabled => {
-                   description => "Set when storage is enabled (not disabled).",
-                   type => 'boolean',
-                   optional => 1,
-               },
-               active => {
-                   description => "Set when storage is accessible.",
-                   type => 'boolean',
-                   optional => 1,
-               },
-               shared => {
-                   description => "Shared flag from storage configuration.",
-                   type => 'boolean',
-                   optional => 1,
-               },
-               total => {
-                   description => "Total storage space in bytes.",
-                   type => 'integer',
-                   renderer => 'bytes',
-                   optional => 1,
-               },
-               used => {
-                   description => "Used storage space in bytes.",
-                   type => 'integer',
-                   renderer => 'bytes',
-                   optional => 1,
-               },
-               avail => {
-                   description => "Available storage space in bytes.",
-                   type => 'integer',
-                   renderer => 'bytes',
-                   optional => 1,
-               },
-               used_fraction => {
-                   description => "Used fraction (used/total).",
-                   type => 'number',
-                   renderer => 'fraction_as_percentage',
-                   optional => 1,
-               },
-           },
-       },
-       links => [ { rel => 'child', href => "{storage}" } ],
-    },
-    code => sub {
-       my ($param) = @_;
-
-       my $rpcenv = PVE::RPCEnvironment::get();
-       my $authuser = $rpcenv->get_user();
-
-       my $localnode = PVE::INotify::nodename();
-
-       my $target = $param->{target};
-
-       undef $target if $target && ($target eq $localnode || $target eq 'localhost');
-
-       my $cfg = PVE::Storage::config();
-
-       my $info = PVE::Storage::storage_info($cfg, $param->{content}, $param->{format});
-
-       raise_param_exc({ storage => "No such storage." })
-           if $param->{storage} && !defined($info->{$param->{storage}});
-
-       my $res = {};
-       my @sids = PVE::Storage::storage_ids($cfg);
-       foreach my $storeid (@sids) {
-           my $data = $info->{$storeid};
-           next if !$data;
-           my $privs = [ 'Datastore.Audit', 'Datastore.AllocateSpace' ];
-           next if !$rpcenv->check_any($authuser, "/storage/$storeid", $privs, 1);
-           next if $param->{storage} && $param->{storage} ne $storeid;
-
-           my $scfg = PVE::Storage::storage_config($cfg, $storeid);
-
-           next if $param->{enabled} && $scfg->{disable};
-
-           if ($target) {
-               # check if storage content is accessible on local node and specified target node
-               # we use this on the Clone GUI
-
-               next if !$scfg->{shared};
-               next if !PVE::Storage::storage_check_node($cfg, $storeid, undef, 1);
-               next if !PVE::Storage::storage_check_node($cfg, $storeid, $target, 1);
-           }
-
-           if ($data->{total}) {
-               $data->{used_fraction} = ($data->{used} // 0) / $data->{total};
-           }
-
-           $res->{$storeid} = $data;
-       }
-
-       return PVE::RESTHandler::hash_to_array($res, 'storage');
-    }});
-
-__PACKAGE__->register_method ({
-    name => 'diridx',
-    path => '{storage}',
-    method => 'GET',
-    description => "",
-    permissions => {
-       check => ['perm', '/storage/{storage}', ['Datastore.Audit', 'Datastore.AllocateSpace'], any => 1],
-    },
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           storage => get_standard_option('pve-storage-id'),
-       },
-    },
-    returns => {
-       type => 'array',
-       items => {
-           type => "object",
-           properties => {
-               subdir => { type => 'string' },
-           },
-       },
-       links => [ { rel => 'child', href => "{subdir}" } ],
-    },
-    code => sub {
-       my ($param) = @_;
-
-       my $res = [
-           { subdir => 'content' },
-           { subdir => 'download-url' },
-           { subdir => 'file-restore' },
-           { subdir => 'prunebackups' },
-           { subdir => 'rrd' },
-           { subdir => 'rrddata' },
-           { subdir => 'status' },
-           { subdir => 'upload' },
-       ];
-
-       return $res;
-    }});
-
-__PACKAGE__->register_method ({
-    name => 'read_status',
-    path => '{storage}/status',
-    method => 'GET',
-    description => "Read storage status.",
-    permissions => {
-       check => ['perm', '/storage/{storage}', ['Datastore.Audit', 'Datastore.AllocateSpace'], any => 1],
-    },
-    protected => 1,
-    proxyto => 'node',
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           storage => get_standard_option('pve-storage-id'),
-       },
-    },
-    returns => {
-       type => "object",
-       properties => {},
-    },
-    code => sub {
-       my ($param) = @_;
-
-       my $cfg = PVE::Storage::config();
-
-       my $info = PVE::Storage::storage_info($cfg, $param->{content});
-
-       my $data = $info->{$param->{storage}};
-
-       raise_param_exc({ storage => "No such storage." })
-           if !defined($data);
-
-       return $data;
-    }});
-
-__PACKAGE__->register_method ({
-    name => 'rrd',
-    path => '{storage}/rrd',
-    method => 'GET',
-    description => "Read storage RRD statistics (returns PNG).",
-    permissions => {
-       check => ['perm', '/storage/{storage}', ['Datastore.Audit', 'Datastore.AllocateSpace'], any => 1],
-    },
-    protected => 1,
-    proxyto => 'node',
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           storage => get_standard_option('pve-storage-id'),
-           timeframe => {
-               description => "Specify the time frame you are interested in.",
-               type => 'string',
-               enum => [ 'hour', 'day', 'week', 'month', 'year' ],
-           },
-           ds => {
-               description => "The list of datasources you want to display.",
-               type => 'string', format => 'pve-configid-list',
-           },
-           cf => {
-               description => "The RRD consolidation function",
-               type => 'string',
-               enum => [ 'AVERAGE', 'MAX' ],
-               optional => 1,
-           },
-       },
-    },
-    returns => {
-       type => "object",
-       properties => {
-           filename => { type => 'string' },
-       },
-    },
-    code => sub {
-       my ($param) = @_;
-
-       return PVE::RRD::create_rrd_graph(
-           "pve2-storage/$param->{node}/$param->{storage}",
-           $param->{timeframe}, $param->{ds}, $param->{cf});
-    }});
-
-__PACKAGE__->register_method ({
-    name => 'rrddata',
-    path => '{storage}/rrddata',
-    method => 'GET',
-    description => "Read storage RRD statistics.",
-    permissions => {
-       check => ['perm', '/storage/{storage}', ['Datastore.Audit', 'Datastore.AllocateSpace'], any => 1],
-    },
-    protected => 1,
-    proxyto => 'node',
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           storage => get_standard_option('pve-storage-id'),
-           timeframe => {
-               description => "Specify the time frame you are interested in.",
-               type => 'string',
-               enum => [ 'hour', 'day', 'week', 'month', 'year' ],
-           },
-           cf => {
-               description => "The RRD consolidation function",
-               type => 'string',
-               enum => [ 'AVERAGE', 'MAX' ],
-               optional => 1,
-           },
-       },
-    },
-    returns => {
-       type => "array",
-       items => {
-           type => "object",
-           properties => {},
-       },
-    },
-    code => sub {
-       my ($param) = @_;
-
-       return PVE::RRD::create_rrd_data(
-           "pve2-storage/$param->{node}/$param->{storage}",
-           $param->{timeframe}, $param->{cf});
-    }});
-
-# makes no sense for big images and backup files (because it
-# create a copy of the file).
-__PACKAGE__->register_method ({
-    name => 'upload',
-    path => '{storage}/upload',
-    method => 'POST',
-    description => "Upload templates and ISO images.",
-    permissions => {
-       check => ['perm', '/storage/{storage}', ['Datastore.AllocateTemplate']],
-    },
-    protected => 1,
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           storage => get_standard_option('pve-storage-id'),
-           content => {
-               description => "Content type.",
-               type => 'string', format => 'pve-storage-content',
-               enum => ['iso', 'vztmpl'],
-           },
-           filename => {
-               description => "The name of the file to create. Caution: This will be normalized!",
-               maxLength => 255,
-               type => 'string',
-           },
-           checksum => {
-               description => "The expected checksum of the file.",
-               type => 'string',
-               requires => 'checksum-algorithm',
-               optional => 1,
-           },
-           'checksum-algorithm' => {
-               description => "The algorithm to calculate the checksum of the file.",
-               type => 'string',
-               enum => ['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512'],
-               requires => 'checksum',
-               optional => 1,
-           },
-           tmpfilename => {
-               description => "The source file name. This parameter is usually set by the REST handler. You can only overwrite it when connecting to the trusted port on localhost.",
-               type => 'string',
-               optional => 1,
-               pattern => '/var/tmp/pveupload-[0-9a-f]+',
-           },
-       },
-    },
-    returns => { type => "string" },
-    code => sub {
-       my ($param) = @_;
-
-       my $rpcenv = PVE::RPCEnvironment::get();
-
-       my $user = $rpcenv->get_user();
-
-       my $cfg = PVE::Storage::config();
-
-       my $node = $param->{node};
-       my $scfg = PVE::Storage::storage_check_enabled($cfg, $param->{storage}, $node);
-
-       die "can't upload to storage type '$scfg->{type}'\n"
-           if !defined($scfg->{path});
-
-       my $content = $param->{content};
-
-       my $tmpfilename = $param->{tmpfilename};
-       die "missing temporary file name\n" if !$tmpfilename;
-
-       my $size = -s $tmpfilename;
-       die "temporary file '$tmpfilename' does not exist\n" if !defined($size);
-
-       my $filename = PVE::Storage::normalize_content_filename($param->{filename});
-
-       my $path;
-
-       if ($content eq 'iso') {
-           if ($filename !~ m![^/]+$PVE::Storage::ISO_EXT_RE_0$!) {
-               raise_param_exc({ filename => "wrong file extension" });
-           }
-           $path = PVE::Storage::get_iso_dir($cfg, $param->{storage});
-       } elsif ($content eq 'vztmpl') {
-           if ($filename !~ m![^/]+$PVE::Storage::VZTMPL_EXT_RE_1$!) {
-               raise_param_exc({ filename => "wrong file extension" });
-           }
-           $path = PVE::Storage::get_vztmpl_dir($cfg, $param->{storage});
-       } else {
-           raise_param_exc({ content => "upload content type '$content' not allowed" });
-       }
-
-       die "storage '$param->{storage}' does not support '$content' content\n"
-           if !$scfg->{content}->{$content};
-
-       my $dest = "$path/$filename";
-       my $dirname = dirname($dest);
-
-       # best effort to match apl_download behaviour
-       chmod 0644, $tmpfilename;
-
-       my $err_cleanup = sub { unlink $dest; die "cleanup failed: $!\n" if $! && $! != ENOENT };
-
-       my $cmd;
-       if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) {
-           my $remip = PVE::Cluster::remote_node_ip($node);
-
-           my @ssh_options = ('-o', 'BatchMode=yes');
-
-           my @remcmd = ('/usr/bin/ssh', @ssh_options, $remip, '--');
-
-           eval { # activate remote storage
-               run_command([@remcmd, '/usr/sbin/pvesm', 'status', '--storage', $param->{storage}]);
-           };
-           die "can't activate storage '$param->{storage}' on node '$node': $@\n" if $@;
-
-           run_command(
-               [@remcmd, '/bin/mkdir', '-p', '--', PVE::Tools::shell_quote($dirname)],
-               errmsg => "mkdir failed",
-           );
-           $cmd = ['/usr/bin/scp', @ssh_options, '-p', '--', $tmpfilename, "[$remip]:" . PVE::Tools::shell_quote($dest)];
-
-           $err_cleanup = sub { run_command([@remcmd, 'rm', '-f', '--', $dest]) };
-       } else {
-           PVE::Storage::activate_storage($cfg, $param->{storage});
-           File::Path::make_path($dirname);
-           $cmd = ['cp', '--', $tmpfilename, $dest];
-       }
-
-       # NOTE: we simply overwrite the destination file if it already exists
-       my $worker = sub {
-           my $upid = shift;
-
-           print "starting file import from: $tmpfilename\n";
-
-           eval {
-               my ($checksum, $checksum_algorithm) = $param->@{'checksum', 'checksum-algorithm'};
-               if ($checksum_algorithm) {
-                   print "calculating checksum...";
-
-                   my $checksum_got = PVE::Tools::get_file_hash($checksum_algorithm, $tmpfilename);
-
-                   if (lc($checksum_got) eq lc($checksum)) {
-                       print "OK, checksum verified\n";
-                   } else {
-                       print "\n";  # the front end expects the error to reside at the last line without any noise
-                       die "checksum mismatch: got '$checksum_got' != expect '$checksum'\n";
-                   }
-               }
-           };
-           if (my $err = $@) {
-               # unlinks only the temporary file from the http server
-               unlink $tmpfilename;
-               warn "unable to clean up temporory file '$tmpfilename' - $!\n"
-                   if $! && $! != ENOENT;
-               die $err;
-           }
-
-           print "target node: $node\n";
-           print "target file: $dest\n";
-           print "file size is: $size\n";
-           print "command: " . join(' ', @$cmd) . "\n";
-
-           eval { run_command($cmd, errmsg => 'import failed'); };
-
-           unlink $tmpfilename; # the temporary file got only uploaded locally, no need to rm remote
-           warn "unable to clean up temporary file '$tmpfilename' - $!\n" if $! && $! != ENOENT;
-
-           if (my $err = $@) {
-               eval { $err_cleanup->() };
-               warn "$@" if $@;
-               die $err;
-           }
-           print "finished file import successfully\n";
-       };
-
-       return $rpcenv->fork_worker('imgcopy', undef, $user, $worker);
-   }});
-
-__PACKAGE__->register_method({
-    name => 'download_url',
-    path => '{storage}/download-url',
-    method => 'POST',
-    description => "Download templates and ISO images by using an URL.",
-    proxyto => 'node',
-    permissions => {
-       check => [ 'and',
-           ['perm', '/storage/{storage}', [ 'Datastore.AllocateTemplate' ]],
-           ['perm', '/', [ 'Sys.Audit', 'Sys.Modify' ]],
-       ],
-    },
-    protected => 1,
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => get_standard_option('pve-node'),
-           storage => get_standard_option('pve-storage-id'),
-           url => {
-               description => "The URL to download the file from.",
-               type => 'string',
-               pattern => 'https?://.*',
-           },
-           content => {
-               description => "Content type.", # TODO: could be optional & detected in most cases
-               type => 'string', format => 'pve-storage-content',
-               enum => ['iso', 'vztmpl'],
-           },
-           filename => {
-               description => "The name of the file to create. Caution: This will be normalized!",
-               maxLength => 255,
-               type => 'string',
-           },
-           checksum => {
-               description => "The expected checksum of the file.",
-               type => 'string',
-               requires => 'checksum-algorithm',
-               optional => 1,
-           },
-           'checksum-algorithm' => {
-               description => "The algorithm to calculate the checksum of the file.",
-               type => 'string',
-               enum => ['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512'],
-               requires => 'checksum',
-               optional => 1,
-           },
-           'verify-certificates' => {
-               description => "If false, no SSL/TLS certificates will be verified.",
-               type => 'boolean',
-               optional => 1,
-               default => 1,
-           },
-       },
-    },
-    returns => {
-       type => "string"
-    },
-    code => sub {
-       my ($param) = @_;
-
-       my $rpcenv = PVE::RPCEnvironment::get();
-       my $user = $rpcenv->get_user();
-
-       my $cfg = PVE::Storage::config();
-
-       my ($node, $storage) = $param->@{'node', 'storage'};
-       my $scfg = PVE::Storage::storage_check_enabled($cfg, $storage, $node);
-
-       die "can't upload to storage type '$scfg->{type}', not a file based storage!\n"
-           if !defined($scfg->{path});
-
-       my ($content, $url) = $param->@{'content', 'url'};
-
-       die "storage '$storage' is not configured for content-type '$content'\n"
-           if !$scfg->{content}->{$content};
-
-       my $filename = PVE::Storage::normalize_content_filename($param->{filename});
-
-       my $path;
-       if ($content eq 'iso') {
-           if ($filename !~ m![^/]+$PVE::Storage::ISO_EXT_RE_0$!) {
-               raise_param_exc({ filename => "wrong file extension" });
-           }
-           $path = PVE::Storage::get_iso_dir($cfg, $storage);
-       } elsif ($content eq 'vztmpl') {
-           if ($filename !~ m![^/]+$PVE::Storage::VZTMPL_EXT_RE_1$!) {
-               raise_param_exc({ filename => "wrong file extension" });
-           }
-           $path = PVE::Storage::get_vztmpl_dir($cfg, $storage);
-       } else {
-           raise_param_exc({ content => "upload content-type '$content' is not allowed" });
-       }
-
-       PVE::Storage::activate_storage($cfg, $storage);
-       File::Path::make_path($path);
-
-       my $dccfg = PVE::Cluster::cfs_read_file('datacenter.cfg');
-       my $opts = {
-           hash_required => 0,
-           verify_certificates => $param->{'verify-certificates'} // 1,
-           http_proxy => $dccfg->{http_proxy},
-       };
-
-       my ($checksum, $checksum_algorithm) = $param->@{'checksum', 'checksum-algorithm'};
-       if ($checksum) {
-           $opts->{"${checksum_algorithm}sum"} = $checksum;
-           $opts->{hash_required} = 1;
-       }
-
-       my $worker = sub {
-           PVE::Tools::download_file_from_url("$path/$filename", $url, $opts);
-       };
-
-       my $worker_id = PVE::Tools::encode_text($filename); # must not pass : or the like as w-ID
-
-       return $rpcenv->fork_worker('download', $worker_id, $user, $worker);
-    }});
-
-1;
diff --git a/PVE/CLI/Makefile b/PVE/CLI/Makefile
deleted file mode 100644 (file)
index 6c6e258..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-SOURCES=pvesm.pm
-
-.PHONY: install
-install: ${SOURCES}
-       install -d -m 0755 ${DESTDIR}${PERLDIR}/PVE/CLI
-       for i in ${SOURCES}; do install -D -m 0644 $$i ${DESTDIR}${PERLDIR}/PVE/CLI/$$i; done
-
-
-clean:
diff --git a/PVE/CLI/pvesm.pm b/PVE/CLI/pvesm.pm
deleted file mode 100755 (executable)
index 9b9676b..0000000
+++ /dev/null
@@ -1,731 +0,0 @@
-package PVE::CLI::pvesm;
-
-use strict;
-use warnings;
-
-use POSIX qw(O_RDONLY O_WRONLY O_CREAT O_TRUNC);
-use Fcntl ':flock';
-use File::Path;
-use MIME::Base64 qw(encode_base64);
-
-use IO::Socket::IP;
-use IO::Socket::UNIX;
-use Socket qw(SOCK_STREAM);
-
-use PVE::SafeSyslog;
-use PVE::Cluster;
-use PVE::INotify;
-use PVE::RPCEnvironment;
-use PVE::Storage;
-use PVE::Tools qw(extract_param);
-use PVE::API2::Storage::Config;
-use PVE::API2::Storage::Content;
-use PVE::API2::Storage::PruneBackups;
-use PVE::API2::Storage::Scan;
-use PVE::API2::Storage::Status;
-use PVE::JSONSchema qw(get_standard_option);
-use PVE::PTY;
-
-use PVE::CLIHandler;
-
-use base qw(PVE::CLIHandler);
-
-my $nodename = PVE::INotify::nodename();
-
-sub param_mapping {
-    my ($name) = @_;
-
-    my $password_map = PVE::CLIHandler::get_standard_mapping('pve-password', {
-       func => sub {
-           my ($value) = @_;
-           return $value if $value;
-           return PVE::PTY::read_password("Enter Password: ");
-       },
-    });
-
-    my $enc_key_map = {
-       name => 'encryption-key',
-       desc => 'a file containing an encryption key, or the special value "autogen"',
-       func => sub {
-           my ($value) = @_;
-           return $value if $value eq 'autogen';
-           return PVE::Tools::file_get_contents($value);
-       }
-    };
-
-    my $master_key_map = {
-       name => 'master-pubkey',
-       desc => 'a file containing a PEM-formatted master public key',
-       func => sub {
-           my ($value) = @_;
-           return encode_base64(PVE::Tools::file_get_contents($value), '');
-       }
-    };
-
-    my $keyring_map = {
-       name => 'keyring',
-       desc => 'file containing the keyring to authenticate in the Ceph cluster',
-       func => sub {
-           my ($value) = @_;
-           return PVE::Tools::file_get_contents($value);
-       },
-    };
-
-    my $mapping = {
-       'cifsscan' => [ $password_map ],
-       'cifs' => [ $password_map ],
-       'pbs' => [ $password_map ],
-       'create' => [ $password_map, $enc_key_map, $master_key_map, $keyring_map ],
-       'update' => [ $password_map, $enc_key_map, $master_key_map, $keyring_map ],
-    };
-    return $mapping->{$name};
-}
-
-sub setup_environment {
-    PVE::RPCEnvironment->setup_default_cli_env();
-}
-
-__PACKAGE__->register_method ({
-    name => 'apiinfo',
-    path => 'apiinfo',
-    method => 'GET',
-    description => "Returns APIVER and APIAGE.",
-    parameters => {
-       additionalProperties => 0,
-       properties => {},
-    },
-    returns => {
-       type => 'object',
-       properties => {
-           apiver => { type => 'integer' },
-           apiage => { type => 'integer' },
-       },
-    },
-    code => sub {
-       return {
-           apiver => PVE::Storage::APIVER,
-           apiage => PVE::Storage::APIAGE,
-       };
-    }
-});
-
-__PACKAGE__->register_method ({
-    name => 'path',
-    path => 'path',
-    method => 'GET',
-    description => "Get filesystem path for specified volume",
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           volume => {
-               description => "Volume identifier",
-               type => 'string', format => 'pve-volume-id',
-               completion => \&PVE::Storage::complete_volume,
-           },
-       },
-    },
-    returns => { type => 'null' },
-
-    code => sub {
-       my ($param) = @_;
-
-       my $cfg = PVE::Storage::config();
-
-       my $path = PVE::Storage::path ($cfg, $param->{volume});
-
-       print "$path\n";
-
-       return undef;
-
-    }});
-
-__PACKAGE__->register_method ({
-    name => 'extractconfig',
-    path => 'extractconfig',
-    method => 'GET',
-    description => "Extract configuration from vzdump backup archive.",
-    permissions => {
-       description => "The user needs 'VM.Backup' permissions on the backed up guest ID, and 'Datastore.AllocateSpace' on the backup storage.",
-       user => 'all',
-    },
-    protected => 1,
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           volume => {
-               description => "Volume identifier",
-               type => 'string',
-               completion => \&PVE::Storage::complete_volume,
-           },
-       },
-    },
-    returns => { type => 'null' },
-    code => sub {
-       my ($param) = @_;
-       my $volume = $param->{volume};
-
-       my $rpcenv = PVE::RPCEnvironment::get();
-       my $authuser = $rpcenv->get_user();
-
-       my $storage_cfg = PVE::Storage::config();
-       PVE::Storage::check_volume_access(
-           $rpcenv,
-           $authuser,
-           $storage_cfg,
-           undef,
-           $volume,
-           'backup',
-       );
-
-       if (PVE::Storage::parse_volume_id($volume, 1)) {
-           my (undef, undef, $ownervm) = PVE::Storage::parse_volname($storage_cfg, $volume);
-           $rpcenv->check($authuser, "/vms/$ownervm", ['VM.Backup']);
-       }
-
-       my $config_raw = PVE::Storage::extract_vzdump_config($storage_cfg, $volume);
-
-       print "$config_raw\n";
-       return;
-    }});
-
-my $print_content = sub {
-    my ($list) = @_;
-
-    my ($maxlenname, $maxsize) = (0, 0);
-    foreach my $info (@$list) {
-       my $volid = $info->{volid};
-       my $sidlen =  length ($volid);
-       $maxlenname = $sidlen if $sidlen > $maxlenname;
-       $maxsize = $info->{size} if ($info->{size} // 0) > $maxsize;
-    }
-    my $sizemaxdigits = length($maxsize);
-
-    my $basefmt = "%-${maxlenname}s %-7s %-9s %${sizemaxdigits}s";
-    printf "$basefmt %s\n", "Volid", "Format", "Type", "Size", "VMID";
-
-    foreach my $info (@$list) {
-       next if !$info->{vmid};
-       my $volid = $info->{volid};
-
-       printf "$basefmt %d\n", $volid, $info->{format}, $info->{content}, $info->{size}, $info->{vmid};
-    }
-
-    foreach my $info (sort { $a->{format} cmp $b->{format} } @$list) {
-       next if $info->{vmid};
-       my $volid = $info->{volid};
-
-       printf "$basefmt\n", $volid, $info->{format}, $info->{content}, $info->{size};
-    }
-};
-
-my $print_status = sub {
-    my $res = shift;
-
-    my $maxlen = 0;
-    foreach my $res (@$res) {
-       my $storeid = $res->{storage};
-       $maxlen = length ($storeid) if length ($storeid) > $maxlen;
-    }
-    $maxlen+=1;
-
-    printf "%-${maxlen}s %10s %10s %15s %15s %15s %8s\n", 'Name', 'Type',
-       'Status', 'Total', 'Used', 'Available', '%';
-
-    foreach my $res (sort { $a->{storage} cmp $b->{storage} } @$res) {
-       my $storeid = $res->{storage};
-
-       my $active = $res->{active} ? 'active' : 'inactive';
-       my ($per, $per_fmt) = (0, '% 7.2f%%');
-       $per = ($res->{used}*100)/$res->{total} if $res->{total} > 0;
-
-       if (!$res->{enabled}) {
-           $per = 'N/A';
-           $per_fmt = '% 8s';
-           $active = 'disabled';
-       }
-
-       printf "%-${maxlen}s %10s %10s %15d %15d %15d $per_fmt\n", $storeid,
-           $res->{type}, $active, $res->{total}/1024, $res->{used}/1024,
-           $res->{avail}/1024, $per;
-    }
-};
-
-__PACKAGE__->register_method ({
-    name => 'export',
-    path => 'export',
-    method => 'GET',
-    description => "Used internally to export a volume.",
-    protected => 1,
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           volume => {
-               description => "Volume identifier",
-               type => 'string',
-               completion => \&PVE::Storage::complete_volume,
-           },
-           format => {
-               description => "Export stream format",
-               type => 'string',
-               enum => $PVE::Storage::KNOWN_EXPORT_FORMATS,
-           },
-           filename => {
-               description => "Destination file name",
-               type => 'string',
-           },
-           base => {
-               description => "Snapshot to start an incremental stream from",
-               type => 'string',
-               pattern => qr/[a-z0-9_\-]{1,40}/i,
-               maxLength => 40,
-               optional => 1,
-           },
-           snapshot => {
-               description => "Snapshot to export",
-               type => 'string',
-               pattern => qr/[a-z0-9_\-]{1,40}/i,
-               maxLength => 40,
-               optional => 1,
-           },
-           'with-snapshots' => {
-               description =>
-                   "Whether to include intermediate snapshots in the stream",
-               type => 'boolean',
-               optional => 1,
-               default => 0,
-           },
-           'snapshot-list' => {
-               description => "Ordered list of snapshots to transfer",
-               type => 'string',
-               format => 'string-list',
-               optional => 1,
-           },
-       },
-    },
-    returns => { type => 'null' },
-    code => sub {
-       my ($param) = @_;
-
-       my $with_snapshots = $param->{'with-snapshots'};
-       if (defined(my $list = $param->{'snapshot-list'})) {
-           $with_snapshots = PVE::Tools::split_list($list);
-       }
-
-       my $filename = $param->{filename};
-
-       my $outfh;
-       if ($filename eq '-') {
-           $outfh = \*STDOUT;
-       } else {
-           sysopen($outfh, $filename, O_CREAT|O_WRONLY|O_TRUNC)
-               or die "open($filename): $!\n";
-       }
-
-       eval {
-           my $cfg = PVE::Storage::config();
-           PVE::Storage::volume_export($cfg, $outfh, $param->{volume}, $param->{format},
-               $param->{snapshot}, $param->{base}, $with_snapshots);
-       };
-       my $err = $@;
-       if ($filename ne '-') {
-           close($outfh);
-           unlink($filename) if $err;
-       }
-       die $err if $err;
-       return;
-    }
-});
-
-__PACKAGE__->register_method ({
-    name => 'import',
-    path => 'import',
-    method => 'PUT',
-    description => "Used internally to import a volume.",
-    protected => 1,
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           volume => {
-               description => "Volume identifier",
-               type => 'string',
-               completion => \&PVE::Storage::complete_volume,
-           },
-           format => {
-               description => "Import stream format",
-               type => 'string',
-               enum => $PVE::Storage::KNOWN_EXPORT_FORMATS,
-           },
-           filename => {
-               description => "Source file name. For '-' stdin is used, the " .
-                 "tcp://<IP-or-CIDR> format allows to use a TCP connection, " .
-                 "the unix://PATH-TO-SOCKET format a UNIX socket as input." .
-                 "Else, the file is treated as common file.",
-               type => 'string',
-           },
-           base => {
-               description => "Base snapshot of an incremental stream",
-               type => 'string',
-               pattern => qr/[a-z0-9_\-]{1,40}/i,
-               maxLength => 40,
-               optional => 1,
-           },
-           'with-snapshots' => {
-               description =>
-                   "Whether the stream includes intermediate snapshots",
-               type => 'boolean',
-               optional => 1,
-               default => 0,
-           },
-           'delete-snapshot' => {
-               description => "A snapshot to delete on success",
-               type => 'string',
-               pattern => qr/[a-z0-9_\-]{1,80}/i,
-               maxLength => 80,
-               optional => 1,
-           },
-           'allow-rename' => {
-               description => "Choose a new volume ID if the requested " .
-                 "volume ID already exists, instead of throwing an error.",
-               type => 'boolean',
-               optional => 1,
-               default => 0,
-           },
-           snapshot => {
-               description => "The current-state snapshot if the stream contains snapshots",
-               type => 'string',
-               pattern => qr/[a-z0-9_\-]{1,40}/i,
-               maxLength => 40,
-               optional => 1,
-           },
-       },
-    },
-    returns => { type => 'string' },
-    code => sub {
-       my ($param) = @_;
-
-       my $filename = $param->{filename};
-
-       my $infh;
-       if ($filename eq '-') {
-           $infh = \*STDIN;
-       } elsif ($filename =~ m!^tcp://(([^/]+)(/\d+)?)$!) {
-           my ($cidr, $ip, $subnet) = ($1, $2, $3);
-           if ($subnet) { # got real CIDR notation, not just IP
-               my $ips = PVE::Network::get_local_ip_from_cidr($cidr);
-               die "Unable to get any local IP address in network '$cidr'\n"
-                   if scalar(@$ips) < 1;
-               die "Got multiple local IP address in network '$cidr'\n"
-                   if scalar(@$ips) > 1;
-
-               $ip = $ips->[0];
-           }
-           my $family = PVE::Tools::get_host_address_family($ip);
-           my $port = PVE::Tools::next_migrate_port($family, $ip);
-
-           my $sock_params = {
-               Listen => 1,
-               ReuseAddr => 1,
-               Proto => &Socket::IPPROTO_TCP,
-               GetAddrInfoFlags => 0,
-               LocalAddr => $ip,
-               LocalPort => $port,
-           };
-           my $socket = IO::Socket::IP->new(%$sock_params)
-               or die "failed to open socket: $!\n";
-
-           print "$ip\n$port\n"; # tell remote where to connect
-           *STDOUT->flush();
-
-           my $prev_alarm = alarm 0;
-           local $SIG{ALRM} = sub { die "timed out waiting for client\n" };
-           alarm 30;
-           my $client = $socket->accept; # Wait for a client
-           alarm $prev_alarm;
-           close($socket);
-
-           $infh = \*$client;
-       } elsif ($filename =~ m!^unix://(.*)$!) {
-           my $socket_path = $1;
-           my $socket = IO::Socket::UNIX->new(
-               Type => SOCK_STREAM(),
-               Local => $socket_path,
-               Listen => 1,
-           ) or die "failed to open socket: $!\n";
-
-           print "ready\n";
-           *STDOUT->flush();
-
-           my $prev_alarm = alarm 0;
-           local $SIG{ALRM} = sub { die "timed out waiting for client\n" };
-           alarm 30;
-           my $client = $socket->accept; # Wait for a client
-           alarm $prev_alarm;
-           close($socket);
-
-           $infh = \*$client;
-       } else {
-           sysopen($infh, $filename, O_RDONLY)
-               or die "open($filename): $!\n";
-       }
-
-       my $cfg = PVE::Storage::config();
-       my $volume = $param->{volume};
-       my $delete = $param->{'delete-snapshot'};
-       my $imported_volid = PVE::Storage::volume_import($cfg, $infh, $volume, $param->{format},
-           $param->{snapshot}, $param->{base}, $param->{'with-snapshots'},
-           $param->{'allow-rename'});
-       PVE::Storage::volume_snapshot_delete($cfg, $imported_volid, $delete)
-           if defined($delete);
-       return $imported_volid;
-    }
-});
-
-__PACKAGE__->register_method ({
-    name => 'prunebackups',
-    path => 'prunebackups',
-    method => 'GET',
-    description => "Prune backups. Only those using the standard naming scheme are considered. " .
-                  "If no keep options are specified, those from the storage configuration are used.",
-    protected => 1,
-    proxyto => 'node',
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           'dry-run' => {
-               description => "Only show what would be pruned, don't delete anything.",
-               type => 'boolean',
-               optional => 1,
-           },
-           node => get_standard_option('pve-node'),
-           storage => get_standard_option('pve-storage-id', {
-               completion => \&PVE::Storage::complete_storage_enabled,
-            }),
-           %{$PVE::Storage::Plugin::prune_backups_format},
-           type => {
-               description => "Either 'qemu' or 'lxc'. Only consider backups for guests of this type.",
-               type => 'string',
-               optional => 1,
-               enum => ['qemu', 'lxc'],
-           },
-           vmid => get_standard_option('pve-vmid', {
-               description => "Only consider backups for this guest.",
-               optional => 1,
-               completion => \&PVE::Cluster::complete_vmid,
-           }),
-       },
-    },
-    returns => {
-       type => 'object',
-       properties => {
-           dryrun => {
-               description => 'If it was a dry run or not. The list will only be defined in that case.',
-               type => 'boolean',
-           },
-           list => {
-               type => 'array',
-               items => {
-                   type => 'object',
-                   properties => {
-                       volid => {
-                           description => "Backup volume ID.",
-                           type => 'string',
-                       },
-                       'ctime' => {
-                           description => "Creation time of the backup (seconds since the UNIX epoch).",
-                           type => 'integer',
-                       },
-                       'mark' => {
-                           description => "Whether the backup would be kept or removed. For backups that don't " .
-                                          "use the standard naming scheme, it's 'protected'.",
-                           type => 'string',
-                       },
-                       type => {
-                           description => "One of 'qemu', 'lxc', 'openvz' or 'unknown'.",
-                           type => 'string',
-                       },
-                       'vmid' => {
-                           description => "The VM the backup belongs to.",
-                           type => 'integer',
-                           optional => 1,
-                       },
-                   },
-               },
-           },
-       },
-    },
-    code => sub {
-       my ($param) = @_;
-
-       my $dryrun = extract_param($param, 'dry-run') ? 1 : 0;
-
-       my $keep_opts;
-       foreach my $keep (keys %{$PVE::Storage::Plugin::prune_backups_format}) {
-           $keep_opts->{$keep} = extract_param($param, $keep) if defined($param->{$keep});
-       }
-       $param->{'prune-backups'} = PVE::JSONSchema::print_property_string(
-           $keep_opts, $PVE::Storage::Plugin::prune_backups_format) if $keep_opts;
-
-       my $list = [];
-       if ($dryrun) {
-           $list = PVE::API2::Storage::PruneBackups->dryrun($param);
-       } else {
-           PVE::API2::Storage::PruneBackups->delete($param);
-       }
-
-       return {
-           dryrun => $dryrun,
-           list => $list,
-       };
-    }});
-
-my $print_api_result = sub {
-    my ($data, $schema, $options) = @_;
-    PVE::CLIFormatter::print_api_result($data, $schema, undef, $options);
-};
-
-our $cmddef = {
-    add => [ "PVE::API2::Storage::Config", 'create', ['type', 'storage'] ],
-    set => [ "PVE::API2::Storage::Config", 'update', ['storage'] ],
-    remove => [ "PVE::API2::Storage::Config", 'delete', ['storage'] ],
-    status => [ "PVE::API2::Storage::Status", 'index', [],
-               { node => $nodename }, $print_status ],
-    list => [ "PVE::API2::Storage::Content", 'index', ['storage'],
-             { node => $nodename }, $print_content ],
-    alloc => [ "PVE::API2::Storage::Content", 'create', ['storage', 'vmid', 'filename', 'size'],
-              { node => $nodename }, sub {
-                  my $volid = shift;
-                  print "successfully created '$volid'\n";
-              }],
-    free => [ "PVE::API2::Storage::Content", 'delete', ['volume'],
-             { node => $nodename } ],
-    scan => {
-       nfs => [ "PVE::API2::Storage::Scan", 'nfsscan', ['server'], { node => $nodename }, sub  {
-           my $res = shift;
-
-           my $maxlen = 0;
-           foreach my $rec (@$res) {
-               my $len = length ($rec->{path});
-               $maxlen = $len if $len > $maxlen;
-           }
-           foreach my $rec (@$res) {
-               printf "%-${maxlen}s %s\n", $rec->{path}, $rec->{options};
-           }
-       }],
-       cifs => [ "PVE::API2::Storage::Scan", 'cifsscan', ['server'], { node => $nodename }, sub  {
-           my $res = shift;
-
-           my $maxlen = 0;
-           foreach my $rec (@$res) {
-               my $len = length ($rec->{share});
-               $maxlen = $len if $len > $maxlen;
-           }
-           foreach my $rec (@$res) {
-               printf "%-${maxlen}s %s\n", $rec->{share}, $rec->{description};
-           }
-       }],
-       glusterfs => [ "PVE::API2::Storage::Scan", 'glusterfsscan', ['server'], { node => $nodename }, sub  {
-           my $res = shift;
-
-           foreach my $rec (@$res) {
-               printf "%s\n", $rec->{volname};
-           }
-       }],
-       iscsi => [ "PVE::API2::Storage::Scan", 'iscsiscan', ['portal'], { node => $nodename }, sub  {
-           my $res = shift;
-
-           my $maxlen = 0;
-           foreach my $rec (@$res) {
-               my $len = length ($rec->{target});
-               $maxlen = $len if $len > $maxlen;
-           }
-           foreach my $rec (@$res) {
-               printf "%-${maxlen}s %s\n", $rec->{target}, $rec->{portal};
-           }
-       }],
-       lvm => [ "PVE::API2::Storage::Scan", 'lvmscan', [], { node => $nodename }, sub  {
-           my $res = shift;
-           foreach my $rec (@$res) {
-               printf "$rec->{vg}\n";
-           }
-       }],
-       lvmthin => [ "PVE::API2::Storage::Scan", 'lvmthinscan', ['vg'], { node => $nodename }, sub  {
-           my $res = shift;
-           foreach my $rec (@$res) {
-               printf "$rec->{lv}\n";
-           }
-       }],
-       pbs => [
-           "PVE::API2::Storage::Scan",
-           'pbsscan',
-           ['server', 'username'],
-           { node => $nodename },
-           $print_api_result,
-           $PVE::RESTHandler::standard_output_options,
-       ],
-       zfs => [ "PVE::API2::Storage::Scan", 'zfsscan', [], { node => $nodename }, sub  {
-           my $res = shift;
-
-           foreach my $rec (@$res) {
-                printf "$rec->{pool}\n";
-           }
-       }],
-    },
-    nfsscan => { alias => 'scan nfs' },
-    cifsscan => { alias => 'scan cifs' },
-    glusterfsscan => { alias => 'scan glusterfs' },
-    iscsiscan => { alias => 'scan iscsi' },
-    lvmscan => { alias => 'scan lvm' },
-    lvmthinscan => { alias => 'scan lvmthin' },
-    zfsscan => { alias => 'scan zfs' },
-    path => [ __PACKAGE__, 'path', ['volume']],
-    extractconfig => [__PACKAGE__, 'extractconfig', ['volume']],
-    export => [ __PACKAGE__, 'export', ['volume', 'format', 'filename']],
-    import => [ __PACKAGE__, 'import', ['volume', 'format', 'filename'], {}, sub  {
-       my $volid = shift;
-       print PVE::Storage::volume_imported_message($volid);
-    }],
-    apiinfo => [ __PACKAGE__, 'apiinfo', [], {}, sub {
-       my $res = shift;
-
-       print "APIVER $res->{apiver}\n";
-       print "APIAGE $res->{apiage}\n";
-    }],
-    'prune-backups' => [ __PACKAGE__, 'prunebackups', ['storage'], { node => $nodename }, sub {
-       my $res = shift;
-
-       my ($dryrun, $list) = ($res->{dryrun}, $res->{list});
-
-       return if !$dryrun;
-
-       if (!scalar(@{$list})) {
-           print "No backups found\n";
-           return;
-       }
-
-       print "NOTE: this is only a preview and might not be what a subsequent\n" .
-             "prune call does if backups are removed/added in the meantime.\n\n";
-
-       my @sorted = sort {
-           my $vmcmp = PVE::Tools::safe_compare($a->{vmid}, $b->{vmid}, sub { $_[0] <=> $_[1] });
-           return $vmcmp if $vmcmp ne 0;
-           return $a->{ctime} <=> $b->{ctime};
-       } @{$list};
-
-       my $maxlen = 0;
-       foreach my $backup (@sorted) {
-           my $volid = $backup->{volid};
-           $maxlen = length($volid) if length($volid) > $maxlen;
-       }
-       $maxlen+=1;
-
-       printf("%-${maxlen}s %15s %10s\n", 'Backup', 'Backup-ID', 'Prune-Mark');
-       foreach my $backup (@sorted) {
-           my $type = $backup->{type};
-           my $vmid = $backup->{vmid};
-           my $backup_id = defined($vmid) ? "$type/$vmid" : "$type";
-           printf("%-${maxlen}s %15s %10s\n", $backup->{volid}, $backup_id, $backup->{mark});
-       }
-    }],
-};
-
-1;
diff --git a/PVE/CephConfig.pm b/PVE/CephConfig.pm
deleted file mode 100644 (file)
index 6b10d46..0000000
+++ /dev/null
@@ -1,291 +0,0 @@
-package PVE::CephConfig;
-
-use strict;
-use warnings;
-use Net::IP;
-use PVE::Tools qw(run_command);
-use PVE::Cluster qw(cfs_register_file);
-
-cfs_register_file('ceph.conf',
-                 \&parse_ceph_config,
-                 \&write_ceph_config);
-
-sub parse_ceph_config {
-    my ($filename, $raw) = @_;
-
-    my $cfg = {};
-    return $cfg if !defined($raw);
-
-    my @lines = split /\n/, $raw;
-
-    my $section;
-
-    foreach my $line (@lines) {
-       $line =~ s/#.*$//;
-       $line =~ s/^\s+//;
-       $line =~ s/^;.*$//;
-       $line =~ s/\s+$//;
-       next if !$line;
-
-       $section = $1 if $line =~ m/^\[(\S+)\]$/;
-       if (!$section) {
-           warn "no section - skip: $line\n";
-           next;
-       }
-
-       if ($line =~ m/^(.*?\S)\s*=\s*(\S.*)$/) {
-           my ($key, $val) = ($1, $2);
-           # ceph treats ' ', '_' and '-' in keys the same, so lets do too
-           $key =~ s/[-\ ]/_/g;
-           $cfg->{$section}->{$key} = $val;
-       }
-
-    }
-
-    return $cfg;
-}
-
-my $parse_ceph_file = sub {
-    my ($filename) = @_;
-
-    my $cfg = {};
-
-    return $cfg if ! -f $filename;
-
-    my $content = PVE::Tools::file_get_contents($filename);
-
-    return parse_ceph_config($filename, $content);
-};
-
-sub write_ceph_config {
-    my ($filename, $cfg) = @_;
-
-    my $out = '';
-
-    my $cond_write_sec = sub {
-       my $re = shift;
-
-       foreach my $section (sort keys %$cfg) {
-           next if $section !~ m/^$re$/;
-           $out .= "[$section]\n";
-           foreach my $key (sort keys %{$cfg->{$section}}) {
-               $out .= "\t $key = $cfg->{$section}->{$key}\n";
-           }
-           $out .= "\n";
-       }
-    };
-
-    &$cond_write_sec('global');
-    &$cond_write_sec('client');
-
-    &$cond_write_sec('mds');
-    &$cond_write_sec('mon');
-    &$cond_write_sec('osd');
-    &$cond_write_sec('mgr');
-
-    &$cond_write_sec('mds\..*');
-    &$cond_write_sec('mon\..*');
-    &$cond_write_sec('osd\..*');
-    &$cond_write_sec('mgr\..*');
-
-    return $out;
-}
-
-my $ceph_get_key = sub {
-    my ($keyfile, $username) = @_;
-
-    my $key = $parse_ceph_file->($keyfile);
-    my $secret = $key->{"client.$username"}->{key};
-
-    return $secret;
-};
-
-my $get_host = sub {
-    my ($hostport) = @_;
-    my ($host, $port) = PVE::Tools::parse_host_and_port($hostport);
-    if (!defined($host)) {
-       return "";
-    }
-    $port = defined($port) ? ":$port" : '';
-    $host = "[$host]" if Net::IP::ip_is_ipv6($host);
-    return "${host}${port}";
-};
-
-sub get_monaddr_list {
-    my ($configfile) = shift;
-
-    if (!defined($configfile)) {
-       warn "No ceph config specified\n";
-       return;
-    }
-
-    my $config = $parse_ceph_file->($configfile);
-
-    my $monhostlist = {};
-
-    # get all ip addresses from mon_host
-    my $monhosts = [ split (/[ ,;]+/, $config->{global}->{mon_host} // "") ];
-
-    foreach my $monhost (@$monhosts) {
-       $monhost =~ s/^\[?v\d\://; # remove beginning of vector
-       $monhost =~ s|/\d+\]?||; # remove end of vector
-       my $host = $get_host->($monhost);
-       if ($host ne "") {
-           $monhostlist->{$host} = 1;
-       }
-    }
-
-    # then get all addrs from mon. sections
-    for my $section ( keys %$config ) {
-       next if $section !~ m/^mon\./;
-
-       if (my $addr = $config->{$section}->{mon_addr}) {
-           $monhostlist->{$addr} = 1;
-       }
-    }
-
-    return join(',', sort keys %$monhostlist);
-}
-
-sub hostlist {
-    my ($list_text, $separator) = @_;
-
-    my @monhostlist = PVE::Tools::split_list($list_text);
-    return join($separator, map { $get_host->($_) } @monhostlist);
-}
-
-my $ceph_check_keyfile = sub {
-    my ($filename, $type) = @_;
-
-    return if ! -f $filename;
-
-    my $content = PVE::Tools::file_get_contents($filename);
-    eval {
-       die if !$content;
-
-       if ($type eq 'rbd') {
-           die if $content !~ /\s*\[\S+\]\s*key\s*=\s*\S+==\s*$/m;
-       } elsif ($type eq 'cephfs') {
-           die if $content !~ /\S+==\s*$/;
-       }
-    };
-    die "Not a proper $type authentication file: $filename\n" if $@;
-
-    return undef;
-};
-
-sub ceph_connect_option {
-    my ($scfg, $storeid, %options) = @_;
-
-    my $cmd_option = {};
-    my $keyfile = "/etc/pve/priv/ceph/${storeid}.keyring";
-    $keyfile = "/etc/pve/priv/ceph/${storeid}.secret" if ($scfg->{type} eq 'cephfs');
-    my $pveceph_managed = !defined($scfg->{monhost});
-
-    $cmd_option->{ceph_conf} = '/etc/pve/ceph.conf' if $pveceph_managed;
-
-    $ceph_check_keyfile->($keyfile, $scfg->{type});
-
-    if (-e "/etc/pve/priv/ceph/${storeid}.conf") {
-       # allow custom ceph configuration for external clusters
-       if ($pveceph_managed) {
-           warn "ignoring custom ceph config for storage '$storeid', 'monhost' is not set (assuming pveceph managed cluster)!\n";
-       } else {
-           $cmd_option->{ceph_conf} = "/etc/pve/priv/ceph/${storeid}.conf";
-       }
-    }
-
-    $cmd_option->{keyring} = $keyfile if (-e $keyfile);
-    $cmd_option->{auth_supported} = (defined $cmd_option->{keyring}) ? 'cephx' : 'none';
-    $cmd_option->{userid} =  $scfg->{username} ? $scfg->{username} : 'admin';
-    $cmd_option->{mon_host} = hostlist($scfg->{monhost}, ',') if (defined($scfg->{monhost}));
-
-    if (%options) {
-       foreach my $k (keys %options) {
-           $cmd_option->{$k} = $options{$k};
-       }
-    }
-
-    return $cmd_option;
-
-}
-
-sub ceph_create_keyfile {
-    my ($type, $storeid, $secret) = @_;
-
-    my $extension = 'keyring';
-    $extension = 'secret' if ($type eq 'cephfs');
-
-    my $ceph_admin_keyring = '/etc/pve/priv/ceph.client.admin.keyring';
-    my $ceph_storage_keyring = "/etc/pve/priv/ceph/${storeid}.$extension";
-
-    die "ceph authx keyring file for storage '$storeid' already exists!\n"
-       if -e $ceph_storage_keyring && !defined($secret);
-
-    if (-e $ceph_admin_keyring || defined($secret)) {
-       eval {
-           if (defined($secret)) {
-               mkdir '/etc/pve/priv/ceph';
-               chomp $secret;
-               PVE::Tools::file_set_contents($ceph_storage_keyring, "${secret}\n", 0400);
-           } elsif ($type eq 'rbd') {
-               mkdir '/etc/pve/priv/ceph';
-               PVE::Tools::file_copy($ceph_admin_keyring, $ceph_storage_keyring);
-           } elsif ($type eq 'cephfs') {
-               my $cephfs_secret = $ceph_get_key->($ceph_admin_keyring, 'admin');
-               mkdir '/etc/pve/priv/ceph';
-               chomp $cephfs_secret;
-               PVE::Tools::file_set_contents($ceph_storage_keyring, "${cephfs_secret}\n", 0400);
-          }
-       };
-       if (my $err = $@) {
-          unlink $ceph_storage_keyring;
-          die "failed to copy ceph authx $extension for storage '$storeid': $err\n";
-       }
-    } else {
-       warn "$ceph_admin_keyring not found, authentication is disabled.\n";
-    }
-}
-
-sub ceph_remove_keyfile {
-    my ($type, $storeid) = @_;
-
-    my $extension = 'keyring';
-    $extension = 'secret' if ($type eq 'cephfs');
-    my $ceph_storage_keyring = "/etc/pve/priv/ceph/${storeid}.$extension";
-
-    if (-f $ceph_storage_keyring) {
-       unlink($ceph_storage_keyring) or warn "removing keyring of storage failed: $!\n";
-    }
-}
-
-my $ceph_version_parser = sub {
-    my $ceph_version = shift;
-    # FIXME this is the same as pve-manager PVE::Ceph::Tools get_local_version
-    if ($ceph_version =~ /^ceph.*\sv?(\d+(?:\.\d+)+(?:-pve\d+)?)\s+(?:\(([a-zA-Z0-9]+)\))?/) {
-       my ($version, $buildcommit) = ($1, $2);
-       my $subversions = [ split(/\.|-/, $version) ];
-
-       return ($subversions, $version, $buildcommit);
-    }
-    warn "Could not parse Ceph version: '$ceph_version'\n";
-};
-
-sub local_ceph_version {
-    my ($cache) = @_;
-
-    my $version_string = $cache;
-    if (!defined($version_string)) {
-       run_command('ceph --version', outfunc => sub {
-           $version_string = shift;
-       });
-    }
-    return undef if !defined($version_string);
-    # subversion is an array ref. with the version parts from major to minor
-    # version is the filtered version string
-    my ($subversions, $version) = $ceph_version_parser->($version_string);
-
-    return wantarray ? ($subversions, $version) : $version;
-}
-
-1;
diff --git a/PVE/Diskmanage.pm b/PVE/Diskmanage.pm
deleted file mode 100644 (file)
index a311ffd..0000000
+++ /dev/null
@@ -1,913 +0,0 @@
-package PVE::Diskmanage;
-
-use strict;
-use warnings;
-
-use PVE::ProcFSTools;
-use Data::Dumper;
-use Cwd qw(abs_path);
-use Fcntl ':mode';
-use File::Basename;
-use File::stat;
-use JSON;
-
-use PVE::Tools qw(extract_param run_command file_get_contents file_read_firstline dir_glob_regex dir_glob_foreach trim);
-
-my $SMARTCTL = "/usr/sbin/smartctl";
-my $ZPOOL = "/sbin/zpool";
-my $SGDISK = "/sbin/sgdisk";
-my $PVS = "/sbin/pvs";
-my $LVS = "/sbin/lvs";
-my $LSBLK = "/bin/lsblk";
-
-my sub strip_dev :prototype($) {
-    my ($devpath) = @_;
-    $devpath =~ s|^/dev/||;
-    return $devpath;
-}
-
-sub check_bin {
-    my ($path) = @_;
-    return -x $path;
-}
-
-sub verify_blockdev_path {
-    my ($rel_path) = @_;
-
-    die "missing path" if !$rel_path;
-    my $path = abs_path($rel_path);
-    die "failed to get absolute path to $rel_path\n" if !$path;
-
-    die "got unusual device path '$path'\n" if $path !~  m|^/dev/(.*)$|;
-
-    $path = "/dev/$1"; # untaint
-
-    assert_blockdev($path);
-
-    return $path;
-}
-
-sub assert_blockdev {
-    my ($dev, $noerr) = @_;
-
-    if ($dev !~ m|^/dev/| || !(-b $dev)) {
-       return if $noerr;
-       die "not a valid block device\n";
-    }
-
-    return 1;
-}
-
-sub init_disk {
-    my ($disk, $uuid) = @_;
-
-    assert_blockdev($disk);
-
-    # we should already have checked these in the api call, but we check again for safety
-    die "$disk is a partition\n" if is_partition($disk);
-    die "disk $disk is already in use\n" if disk_is_used($disk);
-
-    my $id = $uuid || 'R';
-    run_command([$SGDISK, $disk, '-U', $id]);
-    return 1;
-}
-
-sub disk_is_used {
-    my ($disk) = @_;
-
-    my $dev = $disk;
-    $dev =~ s|^/dev/||;
-
-    my $disklist = get_disks($dev, 1, 1);
-
-    die "'$disk' is not a valid local disk\n" if !defined($disklist->{$dev});
-    return 1 if $disklist->{$dev}->{used};
-
-    return 0;
-}
-
-sub get_smart_data {
-    my ($disk, $healthonly) = @_;
-
-    assert_blockdev($disk);
-    my $smartdata = {};
-    my $type;
-
-    my $cmd = [$SMARTCTL, '-H'];
-    push @$cmd, '-A', '-f', 'brief' if !$healthonly;
-    push @$cmd, $disk;
-
-    my $returncode = eval {
-       run_command($cmd, noerr => 1, outfunc => sub {
-           my ($line) = @_;
-
-# ATA SMART attributes, e.g.:
-# ID# ATTRIBUTE_NAME          FLAGS    VALUE WORST THRESH FAIL RAW_VALUE
-#   1 Raw_Read_Error_Rate     POSR-K   100   100   000    -    0
-#
-# SAS and NVME disks, e.g.:
-# Data Units Written:                 5,584,952 [2.85 TB]
-# Accumulated start-stop cycles:  34
-
-           if (defined($type) && $type eq 'ata' && $line =~ m/^([ \d]{2}\d)\s+(\S+)\s+(\S{6})\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(.*)$/) {
-               my $entry = {};
-
-               $entry->{name} = $2 if defined $2;
-               $entry->{flags} = $3 if defined $3;
-               # the +0 makes a number out of the strings
-               # FIXME: 'value' is depreacated by 'normalized'; remove with PVE 7.0
-               $entry->{value} = $4+0 if defined $4;
-               $entry->{normalized} = $4+0 if defined $4;
-               $entry->{worst} = $5+0 if defined $5;
-               # some disks report the default threshold as --- instead of 000
-               if (defined($6) && $6 eq '---') {
-                   $entry->{threshold} = 0;
-               } else {
-                   $entry->{threshold} = $6+0 if defined $6;
-               }
-               $entry->{fail} = $7 if defined $7;
-               $entry->{raw} = $8 if defined $8;
-               $entry->{id} = $1 if defined $1;
-               push @{$smartdata->{attributes}}, $entry;
-           } elsif ($line =~ m/(?:Health Status|self\-assessment test result): (.*)$/ ) {
-               $smartdata->{health} = $1;
-           } elsif ($line =~ m/Vendor Specific SMART Attributes with Thresholds:/) {
-               $type = 'ata';
-               delete $smartdata->{text};
-           } elsif ($line =~ m/=== START OF (READ )?SMART DATA SECTION ===/) {
-               $type = 'text';
-           } elsif (defined($type) && $type eq 'text') {
-               $smartdata->{text} = '' if !defined $smartdata->{text};
-               $smartdata->{text} .= "$line\n";
-               # extract wearout from nvme/sas text, allow for decimal values
-               if ($line =~ m/Percentage Used(?: endurance indicator)?:\s*(\d+(?:\.\d+)?)\%/i) {
-                   $smartdata->{wearout} = 100 - $1;
-               }
-           } elsif ($line =~ m/SMART Disabled/) {
-               $smartdata->{health} = "SMART Disabled";
-           }
-       })
-    };
-    my $err = $@;
-
-    # bit 0 and 1 mark a fatal error, other bits are for disk status -> ignore (see man 8 smartctl)
-    if ((defined($returncode) && ($returncode & 0b00000011)) || $err) {
-       die "Error getting S.M.A.R.T. data: Exit code: $returncode\n";
-    }
-
-    $smartdata->{type} = $type;
-
-    return $smartdata;
-}
-
-sub get_lsblk_info {
-    my $cmd = [$LSBLK, '--json', '-o', 'path,parttype,fstype'];
-    my $output = "";
-    eval { run_command($cmd, outfunc => sub { $output .= "$_[0]\n"; }) };
-    warn "$@\n" if $@;
-    return {} if $output eq '';
-
-    my $parsed = eval { decode_json($output) } // {};
-    warn "$@\n" if $@;
-    my $list = $parsed->{blockdevices} // [];
-
-    return {
-       map {
-           $_->{path} => {
-               parttype => $_->{parttype},
-               fstype => $_->{fstype}
-           }
-       } @{$list}
-    };
-}
-
-my sub get_devices_by_partuuid {
-    my ($lsblk_info, $uuids, $res) = @_;
-
-    $res = {} if !defined($res);
-
-    foreach my $dev (sort keys %{$lsblk_info}) {
-       my $uuid = $lsblk_info->{$dev}->{parttype};
-       next if !defined($uuid) || !defined($uuids->{$uuid});
-       $res->{$dev} = $uuids->{$uuid};
-    }
-
-    return $res;
-}
-
-sub get_zfs_devices {
-    my ($lsblk_info) = @_;
-    my $res = {};
-
-    return {} if !check_bin($ZPOOL);
-
-    # use zpool and parttype uuid, because log and cache do not have zfs type uuid
-    eval {
-       run_command([$ZPOOL, 'list', '-HPLv'], outfunc => sub {
-            my ($line) = @_;
-            if ($line =~ m|^\t([^\t]+)\t|) {
-               $res->{$1} = 1;
-            }
-       });
-    };
-
-    # only warn here, because maybe zfs tools are not installed
-    warn "$@\n" if $@;
-
-    my $uuids = {
-       "6a898cc3-1dd2-11b2-99a6-080020736631" => 1, # apple
-       "516e7cba-6ecf-11d6-8ff8-00022d09712b" => 1, # bsd
-    };
-
-
-    $res = get_devices_by_partuuid($lsblk_info, $uuids, $res);
-
-    return $res;
-}
-
-sub get_lvm_devices {
-    my ($lsblk_info) = @_;
-    my $res = {};
-    eval {
-       run_command([$PVS, '--noheadings', '--readonly', '-o', 'pv_name'], outfunc => sub{
-           my ($line) = @_;
-           $line = trim($line);
-           if ($line =~ m|^/dev/|) {
-               $res->{$line} = 1;
-           }
-       });
-    };
-
-    # if something goes wrong, we do not want to give up, but indicate an error has occurred
-    warn "$@\n" if $@;
-
-    my $uuids = {
-       "e6d6d379-f507-44c2-a23c-238f2a3df928" => 1,
-    };
-
-    $res = get_devices_by_partuuid($lsblk_info, $uuids, $res);
-
-    return $res;
-}
-
-sub get_ceph_journals {
-    my ($lsblk_info) = @_;
-    my $res = {};
-
-    my $uuids = {
-       '45b0969e-9b03-4f30-b4c6-b4b80ceff106' => 1, # journal
-       '30cd0809-c2b2-499c-8879-2d6b78529876' => 2, # db
-       '5ce17fce-4087-4169-b7ff-056cc58473f9' => 3, # wal
-       'cafecafe-9b03-4f30-b4c6-b4b80ceff106' => 4, # block
-    };
-
-    $res = get_devices_by_partuuid($lsblk_info, $uuids, $res);
-
-    return $res;
-}
-
-# reads the lv_tags and matches them with the devices
-sub get_ceph_volume_infos {
-    my $result = {};
-
-    my $cmd = [ $LVS, '-S', 'lv_name=~^osd-', '-o', 'devices,lv_name,lv_tags',
-              '--noheadings', '--readonly', '--separator', ';' ];
-
-    run_command($cmd, outfunc => sub {
-       my $line = shift;
-       $line =~ s/(?:^\s+)|(?:\s+$)//g; # trim whitespaces
-
-       my $fields = [ split(';', $line) ];
-
-       # lvs syntax is /dev/sdX(Y) where Y is the start (which we do not need)
-       my ($dev) = $fields->[0] =~ m|^(/dev/[a-z]+[^(]*)|;
-       if ($fields->[1] =~ m|^osd-([^-]+)-|) {
-           my $type = $1;
-           # $result autovivification is wanted, to not creating empty hashes
-           if (($type eq 'block' || $type eq 'data') && $fields->[2] =~ m/ceph.osd_id=([^,]+)/) {
-               $result->{$dev}->{osdid} = $1;
-               $result->{$dev}->{bluestore} = ($type eq 'block');
-               if ($fields->[2] =~ m/ceph\.encrypted=1/) {
-                   $result->{$dev}->{encrypted} = 1;
-               }
-           } else {
-               # undef++ becomes '1' (see `perldoc perlop`: Auto-increment)
-               $result->{$dev}->{$type}++;
-           }
-       }
-    });
-
-    return $result;
-}
-
-sub get_udev_info {
-    my ($dev) = @_;
-
-    my $info = "";
-    my $data = {};
-    eval {
-       run_command(['udevadm', 'info', '-p', $dev, '--query', 'all'], outfunc => sub {
-           my ($line) = @_;
-           $info .= "$line\n";
-       });
-    };
-    warn $@ if $@;
-    return if !$info;
-
-    return if $info !~ m/^E: DEVTYPE=(disk|partition)$/m;
-    return if $info =~ m/^E: ID_CDROM/m;
-
-    # we use this, because some disks are not simply in /dev e.g. /dev/cciss/c0d0
-    if ($info =~ m/^E: DEVNAME=(\S+)$/m) {
-       $data->{devpath} = $1;
-    }
-    return if !defined($data->{devpath});
-
-    $data->{serial} = 'unknown';
-    $data->{serial} = $1 if $info =~ m/^E: ID_SERIAL_SHORT=(\S+)$/m;
-
-    $data->{gpt} = $info =~ m/^E: ID_PART_TABLE_TYPE=gpt$/m ? 1 : 0;
-
-    $data->{rpm} = -1;
-    $data->{rpm} = $1 if $info =~ m/^E: ID_ATA_ROTATION_RATE_RPM=(\d+)$/m; # detects SSD implicit
-
-    $data->{usb} = 1 if $info =~ m/^E: ID_BUS=usb$/m;
-
-    $data->{model} = $1 if $info =~ m/^E: ID_MODEL=(.+)$/m;
-
-    $data->{wwn} = 'unknown';
-    $data->{wwn} = $1 if $info =~ m/^E: ID_WWN=(.*)$/m;
-
-    if ($info =~ m/^E: DEVLINKS=(.+)$/m) {
-       my @devlinks = grep(m#^/dev/disk/by-id/(ata|scsi|nvme(?!-eui))#, split (/ /, $1));
-       $data->{by_id_link} = $devlinks[0] if defined($devlinks[0]);
-    }
-
-    return $data;
-}
-
-sub get_sysdir_size {
-    my ($sysdir) = @_;
-
-    my $size = file_read_firstline("$sysdir/size");
-    return if !$size;
-
-    # linux always considers sectors to be 512 bytes, independently of real block size
-    return $size * 512;
-}
-
-sub get_sysdir_info {
-    my ($sysdir) = @_;
-
-    return if ! -d "$sysdir/device";
-
-    my $data = {};
-
-    $data->{size} = get_sysdir_size($sysdir) or return;
-
-    # dir/queue/rotational should be 1 for hdd, 0 for ssd
-    $data->{rotational} = file_read_firstline("$sysdir/queue/rotational") // -1;
-
-    $data->{vendor} = file_read_firstline("$sysdir/device/vendor") || 'unknown';
-    $data->{model} = file_read_firstline("$sysdir/device/model") || 'unknown';
-
-    return $data;
-}
-
-sub get_wear_leveling_info {
-    my ($smartdata) = @_;
-    my $attributes = $smartdata->{attributes};
-
-    if (defined($smartdata->{wearout})) {
-       return $smartdata->{wearout};
-    }
-
-    my $wearout;
-
-    # Common register names that represent percentage values of potential failure indicators used
-    # in drivedb.h of smartmontool's. Order matters, as some drives may have multiple definitions
-    my @wearoutregisters = (
-       "Media_Wearout_Indicator",
-       "SSD_Life_Left",
-       "Wear_Leveling_Count",
-       "Perc_Write\/Erase_Ct_BC",
-       "Perc_Rated_Life_Remain",
-       "Remaining_Lifetime_Perc",
-       "Percent_Lifetime_Remain",
-       "Lifetime_Left",
-       "PCT_Life_Remaining",
-       "Lifetime_Remaining",
-       "Percent_Life_Remaining",
-       "Percent_Lifetime_Used",
-       "Perc_Rated_Life_Used"
-    );
-
-    # Search for S.M.A.R.T. attributes for known register
-    foreach my $register (@wearoutregisters) {
-       last if defined $wearout;
-       foreach my $attr (@$attributes) {
-          next if $attr->{name} !~ m/$register/;
-          $wearout = $attr->{value};
-          last;
-       }
-    }
-
-    return $wearout;
-}
-
-sub dir_is_empty {
-    my ($dir) = @_;
-
-    my $dh = IO::Dir->new ($dir);
-    return 1 if !$dh;
-
-    while (defined(my $tmp = $dh->read)) {
-       next if $tmp eq '.' || $tmp eq '..';
-       $dh->close;
-       return 0;
-    }
-    $dh->close;
-    return 1;
-}
-
-sub is_iscsi {
-    my ($sysdir) = @_;
-
-    if (-l $sysdir && readlink($sysdir) =~ m|host[^/]*/session[^/]*|) {
-       return 1;
-    }
-
-    return 0;
-}
-
-my sub is_ssdlike {
-    my ($type) = @_;
-    return $type eq 'ssd' || $type eq 'nvme';
-}
-
-sub mounted_blockdevs {
-    my $mounted = {};
-
-    my $mounts = PVE::ProcFSTools::parse_proc_mounts();
-
-    foreach my $mount (@$mounts) {
-       next if $mount->[0] !~ m|^/dev/|;
-       $mounted->{abs_path($mount->[0])} = $mount->[1];
-    };
-
-    return $mounted;
-}
-
-# returns hashmap of abs mount path -> first part of /proc/mounts (what)
-sub mounted_paths {
-    my $mounted = {};
-
-    my $mounts = PVE::ProcFSTools::parse_proc_mounts();
-
-    foreach my $mount (@$mounts) {
-       $mounted->{abs_path($mount->[1])} = $mount->[0];
-    };
-
-    return $mounted;
-}
-
-sub get_disks {
-    my ($disks, $nosmart, $include_partitions) = @_;
-    my $disklist = {};
-
-    my $mounted = mounted_blockdevs();
-
-    my $lsblk_info = get_lsblk_info();
-
-    my $journalhash = get_ceph_journals($lsblk_info);
-    my $ceph_volume_infos = get_ceph_volume_infos();
-
-    my $zfshash = get_zfs_devices($lsblk_info);
-
-    my $lvmhash = get_lvm_devices($lsblk_info);
-
-    my $disk_regex = ".*";
-    if (defined($disks)) {
-       if (!ref($disks)) {
-           $disks = [ $disks ];
-       } elsif (ref($disks) ne 'ARRAY') {
-           die "disks is not a string or array reference\n";
-       }
-       # we get cciss/c0d0 but need cciss!c0d0
-       $_ =~ s|cciss/|cciss!| for @$disks;
-
-       if ($include_partitions) {
-           # Proper blockdevice is needed for the regex, use parent for partitions.
-           for my $disk ($disks->@*) {
-               next if !is_partition("/dev/$disk");
-               $disk = strip_dev(get_blockdev("/dev/$disk"));
-           }
-       }
-
-       $disk_regex = "(?:" . join('|', @$disks) . ")";
-    }
-
-    dir_glob_foreach('/sys/block', $disk_regex, sub {
-       my ($dev) = @_;
-       # whitelisting following devices
-       # - hdX         ide block device
-       # - sdX         scsi/sata block device
-       # - vdX         virtIO block device
-       # - xvdX:       xen virtual block device
-       # - nvmeXnY:    nvme devices
-       # - cciss!cXnY  cciss devices
-       return if $dev !~ m/^(h|s|x?v)d[a-z]+$/ &&
-                 $dev !~ m/^nvme\d+n\d+$/ &&
-                 $dev !~ m/^cciss\!c\d+d\d+$/;
-
-       my $data = get_udev_info("/sys/block/$dev") // return;
-       my $devpath = $data->{devpath};
-
-       my $sysdir = "/sys/block/$dev";
-
-       # we do not want iscsi devices
-       return if is_iscsi($sysdir);
-
-       my $sysdata = get_sysdir_info($sysdir);
-       return if !defined($sysdata);
-
-       my $type = 'unknown';
-
-       if ($sysdata->{rotational} == 0) {
-           $type = 'ssd';
-           $type = 'nvme' if $dev =~ m/^nvme\d+n\d+$/;
-           $data->{rpm} = 0;
-       } elsif ($sysdata->{rotational} == 1) {
-           if ($data->{rpm} != -1) {
-               $type = 'hdd';
-           } elsif ($data->{usb}) {
-               $type = 'usb';
-               $data->{rpm} = 0;
-           }
-       }
-
-       my ($health, $wearout) = ('UNKNOWN', 'N/A');
-       if (!$nosmart) {
-           eval {
-               my $smartdata = get_smart_data($devpath, !is_ssdlike($type));
-               $health = $smartdata->{health} if $smartdata->{health};
-
-               if (is_ssdlike($type)) { # if we have an ssd we try to get the wearout indicator
-                   my $wear_level = get_wear_leveling_info($smartdata);
-                   $wearout = $wear_level if defined($wear_level);
-               }
-           };
-       }
-
-       # we replaced cciss/ with cciss! above, but in the result we need cciss/ again because the
-       # caller might want to check the result again with the original parameter
-       if ($dev =~ m|^cciss!|) {
-           $dev =~ s|^cciss!|cciss/|;
-       }
-
-       $disklist->{$dev} = {
-           vendor => $sysdata->{vendor},
-           model => $data->{model} || $sysdata->{model},
-           size => $sysdata->{size},
-           serial => $data->{serial},
-           gpt => $data->{gpt},
-           rpm => $data->{rpm},
-           type =>  $type,
-           wwn => $data->{wwn},
-           health => $health,
-           devpath => $devpath,
-           wearout => $wearout,
-       };
-       $disklist->{$dev}->{mounted} = 1 if exists $mounted->{$devpath};
-
-       my $by_id_link = $data->{by_id_link};
-       $disklist->{$dev}->{by_id_link} = $by_id_link if defined($by_id_link);
-
-       my ($osdid, $bluestore, $osdencrypted) = (-1, 0, 0);
-       my ($journal_count, $db_count, $wal_count) = (0, 0, 0);
-
-       my $partpath = $devpath;
-       # remove trailing part to get the partition base path, e.g. /dev/cciss/c0d0 -> /dev/cciss
-       $partpath =~ s/\/[^\/]+$//;
-
-       my $determine_usage = sub {
-           my ($devpath, $sysdir, $is_partition) = @_;
-
-           return 'LVM' if $lvmhash->{$devpath};
-           return 'ZFS' if $zfshash->{$devpath};
-
-           my $info = $lsblk_info->{$devpath} // {};
-
-           if (defined(my $parttype = $info->{parttype})) {
-               return 'BIOS boot'if $parttype eq '21686148-6449-6e6f-744e-656564454649';
-               return 'EFI' if $parttype eq 'c12a7328-f81f-11d2-ba4b-00a0c93ec93b';
-               return 'ZFS reserved' if $parttype eq '6a945a3b-1dd2-11b2-99a6-080020736631';
-           }
-
-           return "$info->{fstype}" if defined($info->{fstype});
-           return 'mounted' if $mounted->{$devpath};
-
-           return if !$is_partition;
-
-           # for devices, this check is done explicitly later
-           return 'Device Mapper' if !dir_is_empty("$sysdir/holders");
-
-           return; # unused partition
-       };
-
-       my $collect_ceph_info = sub {
-           my ($devpath) = @_;
-
-           my $ceph_volume = $ceph_volume_infos->{$devpath} or return;
-           $journal_count += $ceph_volume->{journal} // 0;
-           $db_count += $ceph_volume->{db} // 0;
-           $wal_count += $ceph_volume->{wal} // 0;
-           if (defined($ceph_volume->{osdid})) {
-               $osdid = $ceph_volume->{osdid};
-               $bluestore = 1 if $ceph_volume->{bluestore};
-               $osdencrypted = 1 if $ceph_volume->{encrypted};
-           }
-
-           my $result = { %{$ceph_volume} };
-           $result->{journals} = delete $result->{journal} if $result->{journal};
-           return $result;
-       };
-
-       my $partitions = {};
-       dir_glob_foreach("$sysdir", "$dev.+", sub {
-           my ($part) = @_;
-
-           $partitions->{$part} = $collect_ceph_info->("$partpath/$part");
-           my $lvm_based_osd = defined($partitions->{$part});
-
-           $partitions->{$part}->{devpath} = "$partpath/$part";
-           $partitions->{$part}->{parent} = "$devpath";
-           $partitions->{$part}->{mounted} = 1 if exists $mounted->{"$partpath/$part"};
-           $partitions->{$part}->{gpt} = $data->{gpt};
-           $partitions->{$part}->{type} = 'partition';
-           $partitions->{$part}->{size} = get_sysdir_size("$sysdir/$part") // 0;
-           $partitions->{$part}->{used} = $determine_usage->("$partpath/$part", "$sysdir/$part", 1);
-           $partitions->{$part}->{osdid} //= -1;
-
-           # avoid counting twice (e.g. partition with the LVM for the DB OSD is in $journalhash)
-           return if $lvm_based_osd;
-
-           # Legacy handling for non-LVM based OSDs
-           if (my $mp = $mounted->{"$partpath/$part"}) {
-               if ($mp =~ m|^/var/lib/ceph/osd/ceph-(\d+)$|) {
-                   $osdid = $1;
-                   $partitions->{$part}->{osdid} = $osdid;
-               }
-           }
-
-           if (my $journal_part = $journalhash->{"$partpath/$part"}) {
-               $journal_count++ if $journal_part == 1;
-               $db_count++ if $journal_part == 2;
-               $wal_count++ if $journal_part == 3;
-               $bluestore = 1 if $journal_part == 4;
-
-               $partitions->{$part}->{journals} = 1 if $journal_part == 1;
-               $partitions->{$part}->{db} = 1 if $journal_part == 2;
-               $partitions->{$part}->{wal} = 1 if $journal_part == 3;
-               $partitions->{$part}->{bluestore} = 1 if $journal_part == 4;
-           }
-       });
-
-       my $used = $determine_usage->($devpath, $sysdir, 0);
-       if (!$include_partitions) {
-           foreach my $part (sort keys %{$partitions}) {
-               $used //= $partitions->{$part}->{used};
-           }
-       } else {
-           # fstype might be set even if there are partitions, but showing that is confusing
-           $used = 'partitions' if scalar(keys %{$partitions});
-       }
-       $used //= 'partitions' if scalar(keys %{$partitions});
-       # multipath, software raid, etc.
-       # this check comes in last, to show more specific info
-       # if we have it
-       $used //= 'Device Mapper' if !dir_is_empty("$sysdir/holders");
-
-       $disklist->{$dev}->{used} = $used if $used;
-
-       $collect_ceph_info->($devpath);
-
-       $disklist->{$dev}->{osdid} = $osdid;
-       $disklist->{$dev}->{journals} = $journal_count if $journal_count;
-       $disklist->{$dev}->{bluestore} = $bluestore if $osdid != -1;
-       $disklist->{$dev}->{osdencrypted} = $osdencrypted if $osdid != -1;
-       $disklist->{$dev}->{db} = $db_count if $db_count;
-       $disklist->{$dev}->{wal} = $wal_count if $wal_count;
-
-       if ($include_partitions) {
-           $disklist->{$_} = $partitions->{$_} for keys %{$partitions};
-       }
-    });
-
-    return $disklist;
-}
-
-sub get_partnum {
-    my ($part_path) = @_;
-
-    my $st = stat($part_path);
-
-    die "error detecting block device '$part_path'\n"
-       if !$st || !$st->mode || !S_ISBLK($st->mode) || !$st->rdev;
-
-    my $major = PVE::Tools::dev_t_major($st->rdev);
-    my $minor = PVE::Tools::dev_t_minor($st->rdev);
-    my $partnum_path = "/sys/dev/block/$major:$minor/";
-
-    my $partnum = file_read_firstline("${partnum_path}partition");
-    die "Partition does not exist\n" if !defined($partnum);
-    die "Failed to get partition number\n" if $partnum !~ m/(\d+)/; # untaint
-    $partnum = $1;
-    die "Partition number $partnum is invalid\n" if $partnum > 128;
-
-    return $partnum;
-}
-
-sub get_blockdev {
-    my ($part_path) = @_;
-
-    my ($dev, $block_dev);
-    if ($part_path =~ m|^/dev/(.*)$|) {
-       $dev = $1;
-       my $link = readlink "/sys/class/block/$dev";
-       $block_dev = $1 if $link =~ m|([^/]*)/$dev$|;
-    }
-
-    die "Can't parse parent device\n" if !defined($block_dev);
-    die "No valid block device\n" if index($dev, $block_dev) == -1;
-
-    $block_dev = "/dev/$block_dev";
-    die "Block device does not exists\n" if !(-b $block_dev);
-
-    return $block_dev;
-}
-
-sub is_partition {
-    my ($dev_path) = @_;
-
-    return defined(eval { get_partnum($dev_path) });
-}
-
-sub locked_disk_action {
-    my ($sub) = @_;
-    my $res = PVE::Tools::lock_file('/run/lock/pve-diskmanage.lck', undef, $sub);
-    die $@ if $@;
-    return $res;
-}
-
-sub assert_disk_unused {
-    my ($dev) = @_;
-    die "device '$dev' is already in use\n" if disk_is_used($dev);
-    return;
-}
-
-sub append_partition {
-    my ($dev, $size) = @_;
-
-    my $devname = $dev;
-    $devname =~ s|^/dev/||;
-
-    my $newpartid = 1;
-    dir_glob_foreach("/sys/block/$devname", qr/\Q$devname\E.*?(\d+)/, sub {
-       my ($part, $partid) = @_;
-
-       if ($partid >= $newpartid) {
-           $newpartid = $partid + 1;
-       }
-    });
-
-    $size = PVE::Tools::convert_size($size, 'b' => 'mb');
-
-    run_command([ $SGDISK, '-n', "$newpartid:0:+${size}M", $dev ],
-               errmsg => "error creating partition '$newpartid' on '$dev'");
-
-    my $partition;
-
-    # loop again to detect the real partition device which does not always follow
-    # a strict $devname$partition scheme like /dev/nvme0n1 -> /dev/nvme0n1p1
-    dir_glob_foreach("/sys/block/$devname", qr/\Q$devname\E.*$newpartid/, sub {
-       my ($part) = @_;
-
-       $partition = "/dev/$part";
-    });
-
-    return $partition;
-}
-
-# Check if a disk or any of its partitions has a holder.
-# Can also be called with a partition.
-# Expected to be called with a result of verify_blockdev_path().
-sub has_holder {
-    my ($devpath) = @_;
-
-    my $dev = strip_dev($devpath);
-
-    return $devpath if !dir_is_empty("/sys/class/block/${dev}/holders");
-
-    my $found;
-    dir_glob_foreach("/sys/block/${dev}", "${dev}.+", sub {
-       my ($part) = @_;
-       $found = "/dev/${part}" if !dir_is_empty("/sys/class/block/${part}/holders");
-    });
-
-    return $found;
-}
-
-# Basic check if a disk or any of its partitions is mounted.
-# Can also be called with a partition.
-# Expected to be called with a result of verify_blockdev_path().
-sub is_mounted {
-    my ($devpath) = @_;
-
-    my $mounted = mounted_blockdevs();
-
-    return $devpath if $mounted->{$devpath};
-
-    my $dev = strip_dev($devpath);
-
-    my $found;
-    dir_glob_foreach("/sys/block/${dev}", "${dev}.+", sub {
-       my ($part) = @_;
-       my $partpath = "/dev/${part}";
-
-       $found = $partpath if $mounted->{$partpath};
-    });
-
-    return $found;
-}
-
-# Currently only supports GPT-partitioned disks.
-sub change_parttype {
-    my ($partpath, $parttype) = @_;
-
-    my $err = "unable to change partition type for $partpath";
-
-    my $partnum = get_partnum($partpath);
-    my $blockdev = get_blockdev($partpath);
-    my $dev = strip_dev($blockdev);
-
-    my $info = get_disks($dev, 1);
-    die "$err - unable to get disk info for '$blockdev'\n" if !defined($info->{$dev});
-    die "$err - disk '$blockdev' is not GPT partitioned\n" if !$info->{$dev}->{gpt};
-
-    run_command(['sgdisk', "-t${partnum}:${parttype}", $blockdev], errmsg => $err);
-}
-
-# Wipes all labels and the first 200 MiB of a disk/partition (or the whole if it is smaller).
-# If called with a partition, also sets the partition type to 0x83 'Linux filesystem'.
-# Expected to be called with a result of verify_blockdev_path().
-sub wipe_blockdev {
-    my ($devpath) = @_;
-
-    my $devname = basename($devpath);
-    my $dev_size = PVE::Tools::file_get_contents("/sys/class/block/$devname/size");
-
-    ($dev_size) = $dev_size =~ m|(\d+)|; # untaint $dev_size
-    die "Couldn't get the size of the device $devname\n" if !defined($dev_size);
-
-    my $size = ($dev_size * 512 / 1024 / 1024);
-    my $count = ($size < 200) ? $size : 200;
-
-    my $to_wipe = [];
-    dir_glob_foreach("/sys/class/block/${devname}", "${devname}.+", sub {
-       my ($part) = @_;
-       push $to_wipe->@*, "/dev/${part}" if -b "/dev/${part}";
-    });
-
-    if (scalar($to_wipe->@*) > 0) {
-       print "found child partitions to wipe: ". join(', ', $to_wipe->@*) ."\n";
-    }
-    push $to_wipe->@*, $devpath; # put actual device last
-
-    print "wiping block device ${devpath}\n";
-
-    run_command(['wipefs', '--all', $to_wipe->@*], errmsg => "error wiping '${devpath}'");
-
-    run_command(
-       ['dd', 'if=/dev/zero', "of=${devpath}", 'bs=1M', 'conv=fdatasync', "count=${count}"],
-       errmsg => "error wiping '${devpath}'",
-    );
-
-    if (is_partition($devpath)) {
-       eval { change_parttype($devpath, '8300'); };
-       warn $@ if $@;
-    }
-}
-
-# FIXME: Remove once we depend on systemd >= v249.
-# Work around udev bug https://github.com/systemd/systemd/issues/18525 ensuring database is updated.
-sub udevadm_trigger {
-    my @devs = @_;
-
-    return if scalar(@devs) == 0;
-
-    eval { run_command(['udevadm', 'trigger', @devs]); };
-    warn $@ if $@;
-}
-
-1;
diff --git a/PVE/Makefile b/PVE/Makefile
deleted file mode 100644 (file)
index ec7818e..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-.PHONY: install
-install:
-       install -D -m 0644 Storage.pm ${DESTDIR}${PERLDIR}/PVE/Storage.pm
-       install -D -m 0644 Diskmanage.pm ${DESTDIR}${PERLDIR}/PVE/Diskmanage.pm
-       install -D -m 0644 CephConfig.pm ${DESTDIR}${PERLDIR}/PVE/CephConfig.pm
-       make -C Storage install
-       make -C API2 install
-       make -C CLI install
diff --git a/PVE/Storage.pm b/PVE/Storage.pm
deleted file mode 100755 (executable)
index cec3996..0000000
+++ /dev/null
@@ -1,2151 +0,0 @@
-package PVE::Storage;
-
-use strict;
-use warnings;
-use Data::Dumper;
-
-use POSIX;
-use IO::Select;
-use IO::File;
-use IO::Socket::IP;
-use IPC::Open3;
-use File::Basename;
-use File::Path;
-use Cwd 'abs_path';
-use Socket;
-use Time::Local qw(timelocal);
-
-use PVE::Tools qw(run_command file_read_firstline dir_glob_foreach $IPV6RE);
-use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_lock_file);
-use PVE::DataCenterConfig;
-use PVE::Exception qw(raise_param_exc raise);
-use PVE::JSONSchema;
-use PVE::INotify;
-use PVE::RPCEnvironment;
-use PVE::SSHInfo;
-use PVE::RESTEnvironment qw(log_warn);
-
-use PVE::Storage::Plugin;
-use PVE::Storage::DirPlugin;
-use PVE::Storage::LVMPlugin;
-use PVE::Storage::LvmThinPlugin;
-use PVE::Storage::NFSPlugin;
-use PVE::Storage::CIFSPlugin;
-use PVE::Storage::ISCSIPlugin;
-use PVE::Storage::RBDPlugin;
-use PVE::Storage::CephFSPlugin;
-use PVE::Storage::ISCSIDirectPlugin;
-use PVE::Storage::GlusterfsPlugin;
-use PVE::Storage::ZFSPoolPlugin;
-use PVE::Storage::ZFSPlugin;
-use PVE::Storage::PBSPlugin;
-use PVE::Storage::BTRFSPlugin;
-
-# Storage API version. Increment it on changes in storage API interface.
-use constant APIVER => 10;
-# Age is the number of versions we're backward compatible with.
-# This is like having 'current=APIVER' and age='APIAGE' in libtool,
-# see https://www.gnu.org/software/libtool/manual/html_node/Libtool-versioning.html
-use constant APIAGE => 1;
-
-our $KNOWN_EXPORT_FORMATS = ['raw+size', 'tar+size', 'qcow2+size', 'vmdk+size', 'zfs', 'btrfs'];
-
-# load standard plugins
-PVE::Storage::DirPlugin->register();
-PVE::Storage::LVMPlugin->register();
-PVE::Storage::LvmThinPlugin->register();
-PVE::Storage::NFSPlugin->register();
-PVE::Storage::CIFSPlugin->register();
-PVE::Storage::ISCSIPlugin->register();
-PVE::Storage::RBDPlugin->register();
-PVE::Storage::CephFSPlugin->register();
-PVE::Storage::ISCSIDirectPlugin->register();
-PVE::Storage::GlusterfsPlugin->register();
-PVE::Storage::ZFSPoolPlugin->register();
-PVE::Storage::ZFSPlugin->register();
-PVE::Storage::PBSPlugin->register();
-PVE::Storage::BTRFSPlugin->register();
-
-# load third-party plugins
-if ( -d '/usr/share/perl5/PVE/Storage/Custom' ) {
-    dir_glob_foreach('/usr/share/perl5/PVE/Storage/Custom', '.*\.pm$', sub {
-       my ($file) = @_;
-       my $modname = 'PVE::Storage::Custom::' . $file;
-       $modname =~ s!\.pm$!!;
-       $file = 'PVE/Storage/Custom/' . $file;
-
-       eval {
-           require $file;
-
-           # Check perl interface:
-           die "not derived from PVE::Storage::Plugin\n" if !$modname->isa('PVE::Storage::Plugin');
-           die "does not provide an api() method\n" if !$modname->can('api');
-           # Check storage API version and that file is really storage plugin.
-           my $version = $modname->api();
-           die "implements an API version newer than current ($version > " . APIVER . ")\n"
-               if $version > APIVER;
-           my $min_version = (APIVER - APIAGE);
-           die "API version too old, please update the plugin ($version < $min_version)\n"
-               if $version < $min_version;
-           # all OK, do import and register (i.e., "use")
-           import $file;
-           $modname->register();
-
-           # If we got this far and the API version is not the same, make some noise:
-           warn "Plugin \"$modname\" is implementing an older storage API, an upgrade is recommended\n"
-               if $version != APIVER;
-       };
-       if ($@) {
-           warn "Error loading storage plugin \"$modname\": $@";
-       }
-    });
-}
-
-# initialize all plugins
-PVE::Storage::Plugin->init();
-
-# the following REs indicate the number or capture groups via the trailing digit
-# CAUTION don't forget to update the digits accordingly after messing with the capture groups
-
-our $ISO_EXT_RE_0 = qr/\.(?:iso|img)/i;
-
-our $VZTMPL_EXT_RE_1 = qr/\.tar\.(gz|xz|zst)/i;
-
-our $BACKUP_EXT_RE_2 = qr/\.(tgz|(?:tar|vma)(?:\.(${\PVE::Storage::Plugin::COMPRESSOR_RE}))?)/;
-
-# FIXME remove with PVE 8.0, add versioned breaks for pve-manager
-our $vztmpl_extension_re = $VZTMPL_EXT_RE_1;
-
-#  PVE::Storage utility functions
-
-sub config {
-    return cfs_read_file("storage.cfg");
-}
-
-sub write_config {
-    my ($cfg) = @_;
-
-    cfs_write_file('storage.cfg', $cfg);
-}
-
-sub lock_storage_config {
-    my ($code, $errmsg) = @_;
-
-    cfs_lock_file("storage.cfg", undef, $code);
-    my $err = $@;
-    if ($err) {
-       $errmsg ? die "$errmsg: $err" : die $err;
-    }
-}
-
-# FIXME remove maxfiles for PVE 8.0 or PVE 9.0
-my $convert_maxfiles_to_prune_backups = sub {
-    my ($scfg) = @_;
-
-    return if !$scfg;
-
-    my $maxfiles = delete $scfg->{maxfiles};
-
-    if (!defined($scfg->{'prune-backups'}) && defined($maxfiles)) {
-       my $prune_backups;
-       if ($maxfiles) {
-           $prune_backups = { 'keep-last' => $maxfiles };
-       } else { # maxfiles 0 means no limit
-           $prune_backups = { 'keep-all' => 1 };
-       }
-       $scfg->{'prune-backups'} = PVE::JSONSchema::print_property_string(
-           $prune_backups,
-           'prune-backups'
-       );
-    }
-};
-
-sub storage_config {
-    my ($cfg, $storeid, $noerr) = @_;
-
-    die "no storage ID specified\n" if !$storeid;
-
-    my $scfg = $cfg->{ids}->{$storeid};
-
-    die "storage '$storeid' does not exist\n" if (!$noerr && !$scfg);
-
-    $convert_maxfiles_to_prune_backups->($scfg);
-
-    return $scfg;
-}
-
-sub storage_check_node {
-    my ($cfg, $storeid, $node, $noerr) = @_;
-
-    my $scfg = storage_config($cfg, $storeid);
-
-    if ($scfg->{nodes}) {
-       $node = PVE::INotify::nodename() if !$node || ($node eq 'localhost');
-       if (!$scfg->{nodes}->{$node}) {
-           die "storage '$storeid' is not available on node '$node'\n" if !$noerr;
-           return undef;
-       }
-    }
-
-    return $scfg;
-}
-
-sub storage_check_enabled {
-    my ($cfg, $storeid, $node, $noerr) = @_;
-
-    my $scfg = storage_config($cfg, $storeid);
-
-    if ($scfg->{disable}) {
-       die "storage '$storeid' is disabled\n" if !$noerr;
-       return undef;
-    }
-
-    return storage_check_node($cfg, $storeid, $node, $noerr);
-}
-
-# storage_can_replicate:
-# return true if storage supports replication
-# (volumes allocated with vdisk_alloc() has replication feature)
-sub storage_can_replicate {
-    my ($cfg, $storeid, $format) = @_;
-
-    my $scfg = storage_config($cfg, $storeid);
-    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-    return $plugin->storage_can_replicate($scfg, $storeid, $format);
-}
-
-sub get_max_protected_backups {
-    my ($scfg, $storeid) = @_;
-
-    return $scfg->{'max-protected-backups'} if defined($scfg->{'max-protected-backups'});
-
-    my $rpcenv = PVE::RPCEnvironment::get();
-    my $authuser = $rpcenv->get_user();
-
-    return $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.Allocate'], 1) ? -1 : 5;
-}
-
-sub storage_ids {
-    my ($cfg) = @_;
-
-    return keys %{$cfg->{ids}};
-}
-
-sub file_size_info {
-    my ($filename, $timeout) = @_;
-
-    return PVE::Storage::Plugin::file_size_info($filename, $timeout);
-}
-
-sub get_volume_attribute {
-    my ($cfg, $volid, $attribute) = @_;
-
-    my ($storeid, $volname) = parse_volume_id($volid);
-    my $scfg = storage_config($cfg, $storeid);
-    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-
-    return $plugin->get_volume_attribute($scfg, $storeid, $volname, $attribute);
-}
-
-sub update_volume_attribute {
-    my ($cfg, $volid, $attribute, $value) = @_;
-
-    my ($storeid, $volname) = parse_volume_id($volid);
-    my $scfg = storage_config($cfg, $storeid);
-    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-
-    my ($vtype, undef, $vmid) = $plugin->parse_volname($volname);
-    my $max_protected_backups = get_max_protected_backups($scfg, $storeid);
-
-    if (
-       $vtype eq 'backup'
-       && $vmid
-       && $attribute eq 'protected'
-       && $value
-       && !$plugin->get_volume_attribute($scfg, $storeid, $volname, 'protected')
-       && $max_protected_backups > -1 # -1 is unlimited
-    ) {
-       my $backups = $plugin->list_volumes($storeid, $scfg, $vmid, ['backup']);
-       my ($backup_type) = map { $_->{subtype} } grep { $_->{volid} eq $volid } $backups->@*;
-
-       my $protected_count = grep {
-           $_->{protected} && (!$backup_type || ($_->{subtype} && $_->{subtype} eq $backup_type))
-       } $backups->@*;
-
-       if ($max_protected_backups <= $protected_count) {
-           die "The number of protected backups per guest is limited to $max_protected_backups ".
-               "on storage '$storeid'\n";
-       }
-    }
-
-    return $plugin->update_volume_attribute($scfg, $storeid, $volname, $attribute, $value);
-}
-
-sub volume_size_info {
-    my ($cfg, $volid, $timeout) = @_;
-
-    my ($storeid, $volname) = parse_volume_id($volid, 1);
-    if ($storeid) {
-       my $scfg = storage_config($cfg, $storeid);
-       my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-       return $plugin->volume_size_info($scfg, $storeid, $volname, $timeout);
-    } elsif ($volid =~ m|^(/.+)$| && -e $volid) {
-       return file_size_info($volid, $timeout);
-    } else {
-       return 0;
-    }
-}
-
-sub volume_resize {
-    my ($cfg, $volid, $size, $running) = @_;
-
-    my $padding = (1024 - $size % 1024) % 1024;
-    $size = $size + $padding;
-
-    my ($storeid, $volname) = parse_volume_id($volid, 1);
-    if ($storeid) {
-        my $scfg = storage_config($cfg, $storeid);
-        my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-        return $plugin->volume_resize($scfg, $storeid, $volname, $size, $running);
-    } elsif ($volid =~ m|^(/.+)$| && -e $volid) {
-        die "resize file/device '$volid' is not possible\n";
-    } else {
-       die "unable to parse volume ID '$volid'\n";
-    }
-}
-
-sub volume_rollback_is_possible {
-    my ($cfg, $volid, $snap, $blockers) = @_;
-
-    my ($storeid, $volname) = parse_volume_id($volid, 1);
-    if ($storeid) {
-        my $scfg = storage_config($cfg, $storeid);
-        my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-        return $plugin->volume_rollback_is_possible($scfg, $storeid, $volname, $snap, $blockers);
-    } elsif ($volid =~ m|^(/.+)$| && -e $volid) {
-        die "snapshot rollback file/device '$volid' is not possible\n";
-    } else {
-       die "unable to parse volume ID '$volid'\n";
-    }
-}
-
-sub volume_snapshot {
-    my ($cfg, $volid, $snap) = @_;
-
-    my ($storeid, $volname) = parse_volume_id($volid, 1);
-    if ($storeid) {
-        my $scfg = storage_config($cfg, $storeid);
-        my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-        return $plugin->volume_snapshot($scfg, $storeid, $volname, $snap);
-    } elsif ($volid =~ m|^(/.+)$| && -e $volid) {
-        die "snapshot file/device '$volid' is not possible\n";
-    } else {
-       die "unable to parse volume ID '$volid'\n";
-    }
-}
-
-sub volume_snapshot_rollback {
-    my ($cfg, $volid, $snap) = @_;
-
-    my ($storeid, $volname) = parse_volume_id($volid, 1);
-    if ($storeid) {
-        my $scfg = storage_config($cfg, $storeid);
-        my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-       $plugin->volume_rollback_is_possible($scfg, $storeid, $volname, $snap);
-        return $plugin->volume_snapshot_rollback($scfg, $storeid, $volname, $snap);
-    } elsif ($volid =~ m|^(/.+)$| && -e $volid) {
-        die "snapshot rollback file/device '$volid' is not possible\n";
-    } else {
-       die "unable to parse volume ID '$volid'\n";
-    }
-}
-
-# FIXME PVE 8.x remove $running parameter (needs APIAGE reset)
-sub volume_snapshot_delete {
-    my ($cfg, $volid, $snap, $running) = @_;
-
-    my ($storeid, $volname) = parse_volume_id($volid, 1);
-    if ($storeid) {
-        my $scfg = storage_config($cfg, $storeid);
-        my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-        return $plugin->volume_snapshot_delete($scfg, $storeid, $volname, $snap, $running);
-    } elsif ($volid =~ m|^(/.+)$| && -e $volid) {
-        die "snapshot delete file/device '$volid' is not possible\n";
-    } else {
-       die "unable to parse volume ID '$volid'\n";
-    }
-}
-
-# check if a filesystem on top of a volume needs to flush its journal for
-# consistency (see fsfreeze(8)) before a snapshot is taken - needed for
-# container mountpoints
-sub volume_snapshot_needs_fsfreeze {
-    my ($cfg, $volid) = @_;
-
-    my ($storeid, $volname) = parse_volume_id($volid);
-    my $scfg = storage_config($cfg, $storeid);
-    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-    return $plugin->volume_snapshot_needs_fsfreeze();
-}
-
-# check if a volume or snapshot supports a given feature
-# $feature - one of:
-#            clone - linked clone is possible
-#            copy  - full clone is possible
-#            replicate - replication is possible
-#            snapshot - taking a snapshot is possible
-#            sparseinit - volume is sparsely initialized
-#            template - conversion to base image is possible
-#            rename - renaming volumes is possible
-# $snap - check if the feature is supported for a given snapshot
-# $running - if the guest owning the volume is running
-# $opts - hash with further options:
-#         valid_target_formats - list of formats for the target of a copy/clone
-#                                operation that the caller could work with. The
-#                                format of $volid is always considered valid and if
-#                                no list is specified, all formats are considered valid.
-sub volume_has_feature {
-    my ($cfg, $feature, $volid, $snap, $running, $opts) = @_;
-
-    my ($storeid, $volname) = parse_volume_id($volid, 1);
-    if ($storeid) {
-        my $scfg = storage_config($cfg, $storeid);
-        my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-        return $plugin->volume_has_feature($scfg, $feature, $storeid, $volname, $snap, $running, $opts);
-    } elsif ($volid =~ m|^(/.+)$| && -e $volid) {
-       return undef;
-    } else {
-       return undef;
-    }
-}
-
-sub volume_snapshot_info {
-    my ($cfg, $volid) = @_;
-
-    my ($storeid, $volname) = parse_volume_id($volid);
-    my $scfg = storage_config($cfg, $storeid);
-    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-    return $plugin->volume_snapshot_info($scfg, $storeid, $volname);
-}
-
-sub get_image_dir {
-    my ($cfg, $storeid, $vmid) = @_;
-
-    my $scfg = storage_config($cfg, $storeid);
-    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-
-    my $path = $plugin->get_subdir($scfg, 'images');
-
-    return $vmid ? "$path/$vmid" : $path;
-}
-
-sub get_private_dir {
-    my ($cfg, $storeid, $vmid) = @_;
-
-    my $scfg = storage_config($cfg, $storeid);
-    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-
-    my $path = $plugin->get_subdir($scfg, 'rootdir');
-
-    return $vmid ? "$path/$vmid" : $path;
-}
-
-sub get_iso_dir {
-    my ($cfg, $storeid) = @_;
-
-    my $scfg = storage_config($cfg, $storeid);
-    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-
-    return $plugin->get_subdir($scfg, 'iso');
-}
-
-sub get_vztmpl_dir {
-    my ($cfg, $storeid) = @_;
-
-    my $scfg = storage_config($cfg, $storeid);
-    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-
-    return $plugin->get_subdir($scfg, 'vztmpl');
-}
-
-sub get_backup_dir {
-    my ($cfg, $storeid) = @_;
-
-    my $scfg = storage_config($cfg, $storeid);
-    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-
-    return $plugin->get_subdir($scfg, 'backup');
-}
-
-# library implementation
-
-sub parse_vmid {
-    my $vmid = shift;
-
-    die "VMID '$vmid' contains illegal characters\n" if $vmid !~ m/^\d+$/;
-
-    return int($vmid);
-}
-
-# NOTE: basename and basevmid are always undef for LVM-thin, where the
-# clone -> base reference is not encoded in the volume ID.
-# see note in PVE::Storage::LvmThinPlugin for details.
-sub parse_volname {
-    my ($cfg, $volid) = @_;
-
-    my ($storeid, $volname) = parse_volume_id($volid);
-
-    my $scfg = storage_config($cfg, $storeid);
-
-    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-
-    # returns ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format)
-
-    return $plugin->parse_volname($volname);
-}
-
-sub parse_volume_id {
-    my ($volid, $noerr) = @_;
-
-    return PVE::Storage::Plugin::parse_volume_id($volid, $noerr);
-}
-
-# test if we have read access to volid
-sub check_volume_access {
-    my ($rpcenv, $user, $cfg, $vmid, $volid, $type) = @_;
-
-    my ($sid, $volname) = parse_volume_id($volid, 1);
-    if ($sid) {
-       my ($vtype, undef, $ownervm) = parse_volname($cfg, $volid);
-
-       # Need to allow 'images' when expecting 'rootdir' too - not cleanly separated in plugins.
-       die "unable to use volume $volid - content type needs to be '$type'\n"
-           if defined($type) && $vtype ne $type && ($type ne 'rootdir' || $vtype ne 'images');
-
-       return if $rpcenv->check($user, "/storage/$sid", ['Datastore.Allocate'], 1);
-
-       if ($vtype eq 'iso' || $vtype eq 'vztmpl') {
-           # require at least read access to storage, (custom) templates/ISOs could be sensitive
-           $rpcenv->check_any($user, "/storage/$sid", ['Datastore.AllocateSpace', 'Datastore.Audit']);
-       } elsif (defined($ownervm) && defined($vmid) && ($ownervm == $vmid)) {
-           # we are owner - allow access
-       } elsif ($vtype eq 'backup' && $ownervm) {
-           $rpcenv->check($user, "/storage/$sid", ['Datastore.AllocateSpace']);
-           $rpcenv->check($user, "/vms/$ownervm", ['VM.Backup']);
-       } elsif (($vtype eq 'images' || $vtype eq 'rootdir') && $ownervm) {
-           $rpcenv->check($user, "/storage/$sid", ['Datastore.Audit']);
-           $rpcenv->check($user, "/vms/$ownervm", ['VM.Config.Disk']);
-       } else {
-           die "missing privileges to access $volid\n";
-       }
-    } else {
-       die "Only root can pass arbitrary filesystem paths."
-           if $user ne 'root@pam';
-    }
-
-    return undef;
-}
-
-# NOTE: this check does not work for LVM-thin, where the clone -> base
-# reference is not encoded in the volume ID.
-# see note in PVE::Storage::LvmThinPlugin for details.
-sub volume_is_base_and_used {
-    my ($cfg, $volid) = @_;
-
-    my ($storeid, $volname) = parse_volume_id($volid);
-    my $scfg = storage_config($cfg, $storeid);
-    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-
-    my ($vtype, $name, $vmid, undef, undef, $isBase, undef) =
-       $plugin->parse_volname($volname);
-
-    if ($isBase) {
-       my $vollist = $plugin->list_images($storeid, $scfg);
-       foreach my $info (@$vollist) {
-           my (undef, $tmpvolname) = parse_volume_id($info->{volid});
-           my $basename = undef;
-           my $basevmid = undef;
-
-           eval{
-               (undef, undef, undef, $basename, $basevmid) =
-                   $plugin->parse_volname($tmpvolname);
-           };
-
-           if ($basename && defined($basevmid) && $basevmid == $vmid && $basename eq $name) {
-               return 1;
-           }
-       }
-    }
-    return 0;
-}
-
-# try to map a filesystem path to a volume identifier
-sub path_to_volume_id {
-    my ($cfg, $path) = @_;
-
-    my $ids = $cfg->{ids};
-
-    my ($sid, $volname) = parse_volume_id($path, 1);
-    if ($sid) {
-       if (my $scfg = $ids->{$sid}) {
-           if ($scfg->{path}) {
-               my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-               my ($vtype, $name, $vmid) = $plugin->parse_volname($volname);
-               return ($vtype, $path);
-           }
-       }
-       return ('');
-    }
-
-    # Note: abs_path() return undef if $path doesn not exist
-    # for example when nfs storage is not mounted
-    $path = abs_path($path) || $path;
-
-    foreach my $sid (keys %$ids) {
-       my $scfg = $ids->{$sid};
-       next if !$scfg->{path};
-       my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-       my $imagedir = $plugin->get_subdir($scfg, 'images');
-       my $isodir = $plugin->get_subdir($scfg, 'iso');
-       my $tmpldir = $plugin->get_subdir($scfg, 'vztmpl');
-       my $backupdir = $plugin->get_subdir($scfg, 'backup');
-       my $privatedir = $plugin->get_subdir($scfg, 'rootdir');
-       my $snippetsdir = $plugin->get_subdir($scfg, 'snippets');
-
-       if ($path =~ m!^$imagedir/(\d+)/([^/\s]+)$!) {
-           my $vmid = $1;
-           my $name = $2;
-
-           my $vollist = $plugin->list_images($sid, $scfg, $vmid);
-           foreach my $info (@$vollist) {
-               my ($storeid, $volname) = parse_volume_id($info->{volid});
-               my $volpath = $plugin->path($scfg, $volname, $storeid);
-               if ($volpath eq $path) {
-                   return ('images', $info->{volid});
-               }
-           }
-       } elsif ($path =~ m!^$isodir/([^/]+$ISO_EXT_RE_0)$!) {
-           my $name = $1;
-           return ('iso', "$sid:iso/$name");
-       } elsif ($path =~ m!^$tmpldir/([^/]+$VZTMPL_EXT_RE_1)$!) {
-           my $name = $1;
-           return ('vztmpl', "$sid:vztmpl/$name");
-       } elsif ($path =~ m!^$privatedir/(\d+)$!) {
-           my $vmid = $1;
-           return ('rootdir', "$sid:rootdir/$vmid");
-       } elsif ($path =~ m!^$backupdir/([^/]+$BACKUP_EXT_RE_2)$!) {
-           my $name = $1;
-           return ('backup', "$sid:backup/$name");
-       } elsif ($path =~ m!^$snippetsdir/([^/]+)$!) {
-           my $name = $1;
-           return ('snippets', "$sid:snippets/$name");
-       }
-    }
-
-    # can't map path to volume id
-    return ('');
-}
-
-sub path {
-    my ($cfg, $volid, $snapname) = @_;
-
-    my ($storeid, $volname) = parse_volume_id($volid);
-
-    my $scfg = storage_config($cfg, $storeid);
-
-    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-    my ($path, $owner, $vtype) = $plugin->path($scfg, $volname, $storeid, $snapname);
-    return wantarray ? ($path, $owner, $vtype) : $path;
-}
-
-sub abs_filesystem_path {
-    my ($cfg, $volid, $allow_blockdev) = @_;
-
-    my $path;
-    if (parse_volume_id ($volid, 1)) {
-       activate_volumes($cfg, [ $volid ]);
-       $path = PVE::Storage::path($cfg, $volid);
-    } else {
-       if (-f $volid || ($allow_blockdev && -b $volid)) {
-           my $abspath = abs_path($volid);
-           if ($abspath && $abspath =~ m|^(/.+)$|) {
-               $path = $1; # untaint any path
-           }
-       }
-    }
-    die "can't find file '$volid'\n"
-       if !($path && (-f $path || ($allow_blockdev && -b $path)));
-
-    return $path;
-}
-
-# used as last resort to adapt volnames when migrating
-my $volname_for_storage = sub {
-    my ($cfg, $storeid, $name, $vmid, $format) = @_;
-
-    my $scfg = storage_config($cfg, $storeid);
-
-    my (undef, $valid_formats) = PVE::Storage::Plugin::default_format($scfg);
-    my $format_is_valid = grep { $_ eq $format } @$valid_formats;
-    die "unsupported format '$format' for storage type $scfg->{type}\n"
-       if !$format_is_valid;
-
-    (my $name_without_extension = $name) =~ s/\.$format$//;
-
-    if ($scfg->{path}) {
-       return "$vmid/$name_without_extension.$format";
-    } else {
-       return "$name_without_extension";
-    }
-};
-
-# whether a migration snapshot is needed for a given storage
-sub storage_migrate_snapshot {
-    my ($cfg, $storeid, $existing_snapshots) = @_;
-    my $scfg = storage_config($cfg, $storeid);
-
-    return $scfg->{type} eq 'zfspool' || ($scfg->{type} eq 'btrfs' && $existing_snapshots);
-}
-
-my $volume_import_prepare = sub {
-    my ($volid, $format, $path, $apiver, $opts) = @_;
-
-    my $base_snapshot = $opts->{base_snapshot};
-    my $snapshot = $opts->{snapshot};
-    my $with_snapshots = $opts->{with_snapshots} ? 1 : 0;
-    my $migration_snapshot = $opts->{migration_snapshot} ? 1 : 0;
-    my $allow_rename = $opts->{allow_rename} ? 1 : 0;
-
-    my $recv = ['pvesm', 'import', $volid, $format, $path, '-with-snapshots', $with_snapshots];
-    if (defined($snapshot)) {
-       push @$recv, '-snapshot', $snapshot;
-    }
-    if ($migration_snapshot) {
-       push @$recv, '-delete-snapshot', $snapshot;
-    }
-    push @$recv, '-allow-rename', $allow_rename if $apiver >= 5;
-
-    if (defined($base_snapshot)) {
-       # Check if the snapshot exists on the remote side:
-       push @$recv, '-base', $base_snapshot if $apiver >= 9;
-    }
-
-    return $recv;
-};
-
-my $volume_export_prepare = sub {
-    my ($cfg, $volid, $format, $logfunc, $opts) = @_;
-    my $base_snapshot = $opts->{base_snapshot};
-    my $snapshot = $opts->{snapshot};
-    my $with_snapshots = $opts->{with_snapshots} ? 1 : 0;
-    my $migration_snapshot = $opts->{migration_snapshot} ? 1 : 0;
-    my $ratelimit_bps = $opts->{ratelimit_bps};
-
-    my $send = ['pvesm', 'export', $volid, $format, '-', '-with-snapshots', $with_snapshots];
-    if (defined($snapshot)) {
-       push @$send, '-snapshot', $snapshot;
-    }
-    if (defined($base_snapshot)) {
-       push @$send, '-base', $base_snapshot;
-    }
-
-    my $cstream;
-    if (defined($ratelimit_bps)) {
-       $cstream = [ '/usr/bin/cstream', '-t', $ratelimit_bps ];
-       $logfunc->("using a bandwidth limit of $ratelimit_bps bps for transferring '$volid'") if $logfunc;
-    }
-
-    volume_snapshot($cfg, $volid, $snapshot) if $migration_snapshot;
-
-    if (defined($snapshot)) {
-       activate_volumes($cfg, [$volid], $snapshot);
-    } else {
-       activate_volumes($cfg, [$volid]);
-    }
-
-    return $cstream ? [ $send, $cstream ] : [ $send ];
-};
-
-sub storage_migrate {
-    my ($cfg, $volid, $target_sshinfo, $target_storeid, $opts, $logfunc) = @_;
-
-    my $insecure = $opts->{insecure};
-
-    my ($storeid, $volname) = parse_volume_id($volid);
-
-    my $scfg = storage_config($cfg, $storeid);
-
-    # no need to migrate shared content
-    return $volid if $storeid eq $target_storeid && $scfg->{shared};
-
-    my $tcfg = storage_config($cfg, $target_storeid);
-
-    my $target_volname;
-    if ($opts->{target_volname}) {
-       $target_volname = $opts->{target_volname};
-    } elsif ($scfg->{type} eq $tcfg->{type}) {
-       $target_volname = $volname;
-    } else {
-       my (undef, $name, $vmid, undef, undef, undef, $format) = parse_volname($cfg, $volid);
-       $target_volname = $volname_for_storage->($cfg, $target_storeid, $name, $vmid, $format);
-    }
-
-    my $target_volid = "${target_storeid}:${target_volname}";
-
-    my $target_ip = $target_sshinfo->{ip};
-
-    my $ssh = PVE::SSHInfo::ssh_info_to_command($target_sshinfo);
-    my $ssh_base = PVE::SSHInfo::ssh_info_to_command_base($target_sshinfo);
-    local $ENV{RSYNC_RSH} = PVE::Tools::cmd2string($ssh_base);
-
-    if (!defined($opts->{snapshot})) {
-       $opts->{migration_snapshot} = storage_migrate_snapshot($cfg, $storeid, $opts->{with_snapshots});
-       $opts->{snapshot} = '__migration__' if $opts->{migration_snapshot};
-    }
-
-    my @formats = volume_transfer_formats($cfg, $volid, $target_volid, $opts->{snapshot}, $opts->{base_snapshot}, $opts->{with_snapshots});
-    die "cannot migrate from storage type '$scfg->{type}' to '$tcfg->{type}'\n" if !@formats;
-    my $format = $formats[0];
-
-    my $import_fn = '-'; # let pvesm import read from stdin per default
-    if ($insecure) {
-       my $net = $target_sshinfo->{network} // $target_sshinfo->{ip};
-       $import_fn = "tcp://$net";
-    }
-
-    my $target_apiver = 1; # if there is no apiinfo call, assume 1
-    my $get_api_version = [@$ssh, 'pvesm', 'apiinfo'];
-    my $match_api_version = sub { $target_apiver = $1 if $_[0] =~ m!^APIVER (\d+)$!; };
-    eval { run_command($get_api_version, logfunc => $match_api_version); };
-
-    my $recv = [ @$ssh, '--', $volume_import_prepare->($target_volid, $format, $import_fn, $target_apiver, $opts)->@* ];
-
-    my $new_volid;
-    my $pattern = volume_imported_message(undef, 1);
-    my $match_volid_and_log = sub {
-       my $line = shift;
-
-       $new_volid = $1 if ($line =~ $pattern);
-
-       if ($logfunc) {
-           chomp($line);
-           $logfunc->($line);
-       }
-    };
-
-    my $cmds = $volume_export_prepare->($cfg, $volid, $format, $logfunc, $opts);
-
-    eval {
-       if ($insecure) {
-           my $input = IO::File->new();
-           my $info = IO::File->new();
-           open3($input, $info, $info, @$recv)
-               or die "receive command failed: $!\n";
-           close($input);
-
-           my $try_ip = <$info> // '';
-           my ($ip) = $try_ip =~ /^($PVE::Tools::IPRE)$/ # untaint
-               or die "no tunnel IP received, got '$try_ip'\n";
-
-           my $try_port = <$info> // '';
-           my ($port) = $try_port =~ /^(\d+)$/ # untaint
-               or die "no tunnel port received, got '$try_port'\n";
-
-           my $socket = IO::Socket::IP->new(PeerHost => $ip, PeerPort => $port, Type => SOCK_STREAM)
-               or die "failed to connect to tunnel at $ip:$port\n";
-           # we won't be reading from the socket
-           shutdown($socket, 0);
-
-           eval { run_command($cmds, output => '>&'.fileno($socket), errfunc => $logfunc); };
-           my $send_error = $@;
-
-           # don't close the connection entirely otherwise the receiving end
-           # might not get all buffered data (and fails with 'connection reset by peer')
-           shutdown($socket, 1);
-
-           # wait for the remote process to finish
-           while (my $line = <$info>) {
-               $match_volid_and_log->("[$target_sshinfo->{name}] $line");
-           }
-
-           # now close the socket
-           close($socket);
-           if (!close($info)) { # does waitpid()
-               die "import failed: $!\n" if $!;
-               die "import failed: exit code ".($?>>8)."\n";
-           }
-
-           die $send_error if $send_error;
-       } else {
-           push @$cmds, $recv;
-           run_command($cmds, logfunc => $match_volid_and_log);
-       }
-
-       die "unable to get ID of the migrated volume\n"
-           if !defined($new_volid) && $target_apiver >= 5;
-    };
-    my $err = $@;
-    warn "send/receive failed, cleaning up snapshot(s)..\n" if $err;
-    if ($opts->{migration_snapshot}) {
-       eval { volume_snapshot_delete($cfg, $volid, $opts->{snapshot}, 0) };
-       warn "could not remove source snapshot: $@\n" if $@;
-    }
-    die $err if $err;
-
-    return $new_volid // $target_volid;
-}
-
-sub vdisk_clone {
-    my ($cfg, $volid, $vmid, $snap) = @_;
-
-    my ($storeid, $volname) = parse_volume_id($volid);
-
-    my $scfg = storage_config($cfg, $storeid);
-
-    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-
-    activate_storage($cfg, $storeid);
-
-    # lock shared storage
-    return $plugin->cluster_lock_storage($storeid, $scfg->{shared}, undef, sub {
-       my $volname = $plugin->clone_image($scfg, $storeid, $volname, $vmid, $snap);
-       return "$storeid:$volname";
-    });
-}
-
-sub vdisk_create_base {
-    my ($cfg, $volid) = @_;
-
-    my ($storeid, $volname) = parse_volume_id($volid);
-
-    my $scfg = storage_config($cfg, $storeid);
-
-    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-
-    activate_storage($cfg, $storeid);
-
-    # lock shared storage
-    return $plugin->cluster_lock_storage($storeid, $scfg->{shared}, undef, sub {
-       my $volname = $plugin->create_base($storeid, $scfg, $volname);
-       return "$storeid:$volname";
-    });
-}
-
-sub map_volume {
-    my ($cfg, $volid, $snapname) = @_;
-
-    my ($storeid, $volname) = parse_volume_id($volid);
-
-    my $scfg = storage_config($cfg, $storeid);
-
-    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-
-    return $plugin->map_volume($storeid, $scfg, $volname, $snapname);
-}
-
-sub unmap_volume {
-    my ($cfg, $volid, $snapname) = @_;
-
-    my ($storeid, $volname) = parse_volume_id($volid);
-
-    my $scfg = storage_config($cfg, $storeid);
-
-    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-
-    return $plugin->unmap_volume($storeid, $scfg, $volname, $snapname);
-}
-
-sub vdisk_alloc {
-    my ($cfg, $storeid, $vmid, $fmt, $name, $size) = @_;
-
-    die "no storage ID specified\n" if !$storeid;
-
-    PVE::JSONSchema::parse_storage_id($storeid);
-
-    my $scfg = storage_config($cfg, $storeid);
-
-    die "no VMID specified\n" if !$vmid;
-
-    $vmid = parse_vmid($vmid);
-
-    my $defformat = PVE::Storage::Plugin::default_format($scfg);
-
-    $fmt = $defformat if !$fmt;
-
-    activate_storage($cfg, $storeid);
-
-    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-
-    # lock shared storage
-    return $plugin->cluster_lock_storage($storeid, $scfg->{shared}, undef, sub {
-       my $old_umask = umask(umask|0037);
-       my $volname = eval { $plugin->alloc_image($storeid, $scfg, $vmid, $fmt, $name, $size) };
-       my $err = $@;
-       umask $old_umask;
-       die $err if $err;
-       return "$storeid:$volname";
-    });
-}
-
-sub vdisk_free {
-    my ($cfg, $volid) = @_;
-
-    my ($storeid, $volname) = parse_volume_id($volid);
-    my $scfg = storage_config($cfg, $storeid);
-    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-
-    activate_storage($cfg, $storeid);
-
-    my $cleanup_worker;
-
-    # lock shared storage
-    $plugin->cluster_lock_storage($storeid, $scfg->{shared}, undef, sub {
-       # LVM-thin allows deletion of still referenced base volumes!
-       die "base volume '$volname' is still in use by linked clones\n"
-           if volume_is_base_and_used($cfg, $volid);
-
-       my (undef, undef, undef, undef, undef, $isBase, $format) =
-           $plugin->parse_volname($volname);
-       $cleanup_worker = $plugin->free_image($storeid, $scfg, $volname, $isBase, $format);
-    });
-
-    return if !$cleanup_worker;
-
-    my $rpcenv = PVE::RPCEnvironment::get();
-    my $authuser = $rpcenv->get_user();
-
-    $rpcenv->fork_worker('imgdel', undef, $authuser, $cleanup_worker);
-}
-
-sub vdisk_list {
-    my ($cfg, $storeid, $vmid, $vollist, $ctype) = @_;
-
-    my $ids = $cfg->{ids};
-
-    storage_check_enabled($cfg, $storeid) if ($storeid);
-
-    my $res = $storeid ? { $storeid => [] } : {};
-
-    # prepare/activate/refresh all storages
-
-    my $storage_list = [];
-    if ($vollist) {
-       foreach my $volid (@$vollist) {
-           my ($sid, undef) = parse_volume_id($volid);
-           next if !defined($ids->{$sid});
-           next if !storage_check_enabled($cfg, $sid, undef, 1);
-           push @$storage_list, $sid;
-       }
-    } else {
-       foreach my $sid (keys %$ids) {
-           next if $storeid && $storeid ne $sid;
-           next if !storage_check_enabled($cfg, $sid, undef, 1);
-           my $content = $ids->{$sid}->{content};
-           next if defined($ctype) && !$content->{$ctype};
-           next if !($content->{rootdir} || $content->{images});
-           push @$storage_list, $sid;
-       }
-    }
-
-    my $cache = {};
-
-    activate_storage_list($cfg, $storage_list, $cache);
-
-    for my $sid ($storage_list->@*) {
-       next if $storeid && $storeid ne $sid;
-
-       my $scfg = $ids->{$sid};
-       my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-       $res->{$sid} = $plugin->list_images($sid, $scfg, $vmid, $vollist, $cache);
-       @{$res->{$sid}} = sort {lc($a->{volid}) cmp lc ($b->{volid}) } @{$res->{$sid}} if $res->{$sid};
-    }
-
-    return $res;
-}
-
-sub template_list {
-    my ($cfg, $storeid, $tt) = @_;
-
-    die "unknown template type '$tt'\n"
-       if !($tt eq 'iso' || $tt eq 'vztmpl' || $tt eq 'backup' || $tt eq 'snippets');
-
-    my $ids = $cfg->{ids};
-
-    storage_check_enabled($cfg, $storeid) if ($storeid);
-
-    my $res = {};
-
-    # query the storage
-    foreach my $sid (keys %$ids) {
-       next if $storeid && $storeid ne $sid;
-
-       my $scfg = $ids->{$sid};
-       my $type = $scfg->{type};
-
-       next if !$scfg->{content}->{$tt};
-
-       next if !storage_check_enabled($cfg, $sid, undef, 1);
-
-       $res->{$sid} = volume_list($cfg, $sid, undef, $tt);
-    }
-
-    return $res;
-}
-
-sub volume_list {
-    my ($cfg, $storeid, $vmid, $content) = @_;
-
-    my @ctypes = qw(rootdir images vztmpl iso backup snippets);
-
-    my $cts = $content ? [ $content ] : [ @ctypes ];
-
-    my $scfg = PVE::Storage::storage_config($cfg, $storeid);
-
-    $cts = [ grep { defined($scfg->{content}->{$_}) } @$cts ];
-
-    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-
-    activate_storage($cfg, $storeid);
-
-    my $res = $plugin->list_volumes($storeid, $scfg, $vmid, $cts);
-
-    @$res = sort {lc($a->{volid}) cmp lc ($b->{volid}) } @$res;
-
-    return $res;
-}
-
-sub uevent_seqnum {
-
-    my $filename = "/sys/kernel/uevent_seqnum";
-
-    my $seqnum = 0;
-    if (my $fh = IO::File->new($filename, "r")) {
-       my $line = <$fh>;
-       if ($line =~ m/^(\d+)$/) {
-           $seqnum = int($1);
-       }
-       close ($fh);
-    }
-    return $seqnum;
-}
-
-sub activate_storage {
-    my ($cfg, $storeid, $cache) = @_;
-
-    $cache = {} if !$cache;
-
-    my $scfg = storage_check_enabled($cfg, $storeid);
-
-    return if $cache->{activated}->{$storeid};
-
-    $cache->{uevent_seqnum} = uevent_seqnum() if !$cache->{uevent_seqnum};
-
-    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-
-    if ($scfg->{base}) {
-       my ($baseid, undef) = parse_volume_id ($scfg->{base});
-       activate_storage($cfg, $baseid, $cache);
-    }
-
-    if (! eval { $plugin->check_connection($storeid, $scfg) }) {
-       die "connection check for storage '$storeid' failed - $@\n" if $@;
-       die "storage '$storeid' is not online\n";
-    }
-
-    $plugin->activate_storage($storeid, $scfg, $cache);
-
-    my $newseq = uevent_seqnum ();
-
-    # only call udevsettle if there are events
-    if ($newseq > $cache->{uevent_seqnum}) {
-       system ("udevadm settle --timeout=30"); # ignore errors
-       $cache->{uevent_seqnum} = $newseq;
-    }
-
-    $cache->{activated}->{$storeid} = 1;
-}
-
-sub activate_storage_list {
-    my ($cfg, $storeid_list, $cache) = @_;
-
-    $cache = {} if !$cache;
-
-    foreach my $storeid (@$storeid_list) {
-       activate_storage($cfg, $storeid, $cache);
-    }
-}
-
-sub deactivate_storage {
-    my ($cfg, $storeid) = @_;
-
-    my $scfg = storage_config ($cfg, $storeid);
-    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-
-    my $cache = {};
-    $plugin->deactivate_storage($storeid, $scfg, $cache);
-}
-
-sub activate_volumes {
-    my ($cfg, $vollist, $snapname) = @_;
-
-    return if !($vollist && scalar(@$vollist));
-
-    my $storagehash = {};
-    foreach my $volid (@$vollist) {
-       my ($storeid, undef) = parse_volume_id($volid);
-       $storagehash->{$storeid} = 1;
-    }
-
-    my $cache = {};
-
-    activate_storage_list($cfg, [keys %$storagehash], $cache);
-
-    foreach my $volid (@$vollist) {
-       my ($storeid, $volname) = parse_volume_id($volid);
-       my $scfg = storage_config($cfg, $storeid);
-       my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-       $plugin->activate_volume($storeid, $scfg, $volname, $snapname, $cache);
-    }
-}
-
-sub deactivate_volumes {
-    my ($cfg, $vollist, $snapname) = @_;
-
-    return if !($vollist && scalar(@$vollist));
-
-    my $cache = {};
-
-    my @errlist = ();
-    foreach my $volid (@$vollist) {
-       my ($storeid, $volname) = parse_volume_id($volid);
-
-       my $scfg = storage_config($cfg, $storeid);
-       my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-
-       eval {
-           $plugin->deactivate_volume($storeid, $scfg, $volname, $snapname, $cache);
-       };
-       if (my $err = $@) {
-           warn $err;
-           push @errlist, $volid;
-       }
-    }
-
-    die "volume deactivation failed: " . join(' ', @errlist)
-       if scalar(@errlist);
-}
-
-sub storage_info {
-    my ($cfg, $content, $includeformat) = @_;
-
-    my $ids = $cfg->{ids};
-
-    my $info = {};
-
-    my @ctypes = PVE::Tools::split_list($content);
-
-    my $slist = [];
-    foreach my $storeid (keys %$ids) {
-       my $storage_enabled = defined(storage_check_enabled($cfg, $storeid, undef, 1));
-
-       if (defined($content)) {
-           my $want_ctype = 0;
-           foreach my $ctype (@ctypes) {
-               if ($ids->{$storeid}->{content}->{$ctype}) {
-                   $want_ctype = 1;
-                   last;
-               }
-           }
-           next if !$want_ctype || !$storage_enabled;
-       }
-
-       my $type = $ids->{$storeid}->{type};
-
-       $info->{$storeid} = {
-           type => $type,
-           total => 0,
-           avail => 0,
-           used => 0,
-           shared => $ids->{$storeid}->{shared} ? 1 : 0,
-           content => PVE::Storage::Plugin::content_hash_to_string($ids->{$storeid}->{content}),
-           active => 0,
-           enabled => $storage_enabled ? 1 : 0,
-       };
-
-       push @$slist, $storeid;
-    }
-
-    my $cache = {};
-
-    foreach my $storeid (keys %$ids) {
-       my $scfg = $ids->{$storeid};
-
-       next if !$info->{$storeid};
-       next if !$info->{$storeid}->{enabled};
-
-       my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-       if ($includeformat) {
-           my $pd = $plugin->plugindata();
-           $info->{$storeid}->{format} = $pd->{format}
-               if $pd->{format};
-           $info->{$storeid}->{select_existing} = $pd->{select_existing}
-               if $pd->{select_existing};
-       }
-
-       eval { activate_storage($cfg, $storeid, $cache); };
-       if (my $err = $@) {
-           warn $err;
-           next;
-       }
-
-       my ($total, $avail, $used, $active) = eval { $plugin->status($storeid, $scfg, $cache); };
-       warn $@ if $@;
-       next if !$active;
-       $info->{$storeid}->{total} = int($total);
-       $info->{$storeid}->{avail} = int($avail);
-       $info->{$storeid}->{used} = int($used);
-       $info->{$storeid}->{active} = $active;
-    }
-
-    return $info;
-}
-
-sub resolv_server {
-    my ($server) = @_;
-
-    my ($packed_ip, $family);
-    eval {
-       my @res = PVE::Tools::getaddrinfo_all($server);
-       $family = $res[0]->{family};
-       $packed_ip = (PVE::Tools::unpack_sockaddr_in46($res[0]->{addr}))[2];
-    };
-    if (defined $packed_ip) {
-       return Socket::inet_ntop($family, $packed_ip);
-    }
-    return undef;
-}
-
-sub scan_nfs {
-    my ($server_in) = @_;
-
-    my $server;
-    if (!($server = resolv_server ($server_in))) {
-       die "unable to resolve address for server '${server_in}'\n";
-    }
-
-    my $cmd = ['/sbin/showmount',  '--no-headers', '--exports', $server];
-
-    my $res = {};
-    run_command($cmd, outfunc => sub {
-       my $line = shift;
-
-       # note: howto handle white spaces in export path??
-       if ($line =~ m!^(/\S+)\s+(.+)$!) {
-           $res->{$1} = $2;
-       }
-    });
-
-    return $res;
-}
-
-sub scan_cifs {
-    my ($server_in, $user, $password, $domain) = @_;
-
-    my $server = resolv_server($server_in);
-    die "unable to resolve address for server '${server_in}'\n" if !$server;
-
-    # we only support Windows 2012 and newer, so just use smb3
-    my $cmd = ['/usr/bin/smbclient', '-m', 'smb3', '-d', '0', '-L', $server];
-    push @$cmd, '-W', $domain if defined($domain);
-
-    push @$cmd, '-N' if !defined($password);
-    local $ENV{USER} = $user if defined($user);
-    local $ENV{PASSWD} = $password if defined($password);
-
-    my $res = {};
-    my $err = '';
-    run_command($cmd,
-       noerr => 1,
-       errfunc => sub {
-           $err .= "$_[0]\n"
-       },
-       outfunc => sub {
-           my $line = shift;
-           if ($line =~ m/(\S+)\s*Disk\s*(\S*)/) {
-               $res->{$1} = $2;
-           } elsif ($line =~ m/(NT_STATUS_(\S+))/) {
-               my $status = $1;
-               $err .= "unexpected status: $1\n" if uc($1) ne 'SUCCESS';
-           }
-       },
-    );
-    # only die if we got no share, else it's just some followup check error
-    # (like workgroup querying)
-    raise($err) if $err && !%$res;
-
-    return $res;
-}
-
-sub scan_zfs {
-
-    my $cmd = ['zfs',  'list', '-t', 'filesystem', '-Hp', '-o', 'name,avail,used'];
-
-    my $res = [];
-    run_command($cmd, outfunc => sub {
-       my $line = shift;
-
-       if ($line =~m/^(\S+)\s+(\S+)\s+(\S+)$/) {
-           my ($pool, $size_str, $used_str) = ($1, $2, $3);
-           my $size = $size_str + 0;
-           my $used = $used_str + 0;
-           # ignore subvolumes generated by our ZFSPoolPlugin
-           return if $pool =~ m!/subvol-\d+-[^/]+$!;
-           return if $pool =~ m!/basevol-\d+-[^/]+$!;
-           push @$res, { pool => $pool, size => $size, free => $size-$used };
-       }
-    });
-
-    return $res;
-}
-
-sub resolv_portal {
-    my ($portal, $noerr) = @_;
-
-    my ($server, $port) = PVE::Tools::parse_host_and_port($portal);
-    if ($server) {
-       if (my $ip = resolv_server($server)) {
-           $server = $ip;
-           $server = "[$server]" if $server =~ /^$IPV6RE$/;
-           return $port ? "$server:$port" : $server;
-       }
-    }
-    return undef if $noerr;
-
-    raise_param_exc({ portal => "unable to resolve portal address '$portal'" });
-}
-
-
-sub scan_iscsi {
-    my ($portal_in) = @_;
-
-    my $portal;
-    if (!($portal = resolv_portal($portal_in))) {
-       die "unable to parse/resolve portal address '${portal_in}'\n";
-    }
-
-    return PVE::Storage::ISCSIPlugin::iscsi_discovery($portal);
-}
-
-sub storage_default_format {
-    my ($cfg, $storeid) = @_;
-
-    my $scfg = storage_config ($cfg, $storeid);
-
-    return PVE::Storage::Plugin::default_format($scfg);
-}
-
-sub vgroup_is_used {
-    my ($cfg, $vgname) = @_;
-
-    foreach my $storeid (keys %{$cfg->{ids}}) {
-       my $scfg = storage_config($cfg, $storeid);
-       if ($scfg->{type} eq 'lvm' && $scfg->{vgname} eq $vgname) {
-           return 1;
-       }
-    }
-
-    return undef;
-}
-
-sub target_is_used {
-    my ($cfg, $target) = @_;
-
-    foreach my $storeid (keys %{$cfg->{ids}}) {
-       my $scfg = storage_config($cfg, $storeid);
-       if ($scfg->{type} eq 'iscsi' && $scfg->{target} eq $target) {
-           return 1;
-       }
-    }
-
-    return undef;
-}
-
-sub volume_is_used {
-    my ($cfg, $volid) = @_;
-
-    foreach my $storeid (keys %{$cfg->{ids}}) {
-       my $scfg = storage_config($cfg, $storeid);
-       if ($scfg->{base} && $scfg->{base} eq $volid) {
-           return 1;
-       }
-    }
-
-    return undef;
-}
-
-sub storage_is_used {
-    my ($cfg, $storeid) = @_;
-
-    foreach my $sid (keys %{$cfg->{ids}}) {
-       my $scfg = storage_config($cfg, $sid);
-       next if !$scfg->{base};
-       my ($st) = parse_volume_id($scfg->{base});
-       return 1 if $st && $st eq $storeid;
-    }
-
-    return undef;
-}
-
-sub foreach_volid {
-    my ($list, $func) = @_;
-
-    return if !$list;
-
-    foreach my $sid (keys %$list) {
-       foreach my $info (@{$list->{$sid}}) {
-           my $volid = $info->{volid};
-          my ($sid1, $volname) = parse_volume_id($volid, 1);
-          if ($sid1 && $sid1 eq $sid) {
-              &$func ($volid, $sid, $info);
-          } else {
-              warn "detected strange volid '$volid' in volume list for '$sid'\n";
-          }
-       }
-    }
-}
-
-sub decompressor_info {
-    my ($format, $comp) = @_;
-
-    if ($format eq 'tgz' && !defined($comp)) {
-       ($format, $comp) = ('tar', 'gz');
-    }
-
-    my $decompressor = {
-       tar => {
-           gz => ['tar', '-z'],
-           lzo => ['tar', '--lzop'],
-           zst => ['tar', '--zstd'],
-       },
-       vma => {
-           gz => ['zcat'],
-           lzo => ['lzop', '-d', '-c'],
-           zst => ['zstd', '-q', '-d', '-c'],
-       },
-    };
-
-    die "ERROR: archive format not defined\n"
-       if !defined($decompressor->{$format});
-
-    my $decomp;
-    $decomp = $decompressor->{$format}->{$comp} if $comp;
-
-    my $info = {
-       format => $format,
-       compression => $comp,
-       decompressor => $decomp,
-    };
-
-    return $info;
-}
-
-sub protection_file_path {
-    my ($path) = @_;
-
-    return "${path}.protected";
-}
-
-sub archive_info {
-    my ($archive) = shift;
-    my $info;
-
-    my $volid = basename($archive);
-    if ($volid =~ /^(vzdump-(lxc|openvz|qemu)-.+$BACKUP_EXT_RE_2)$/) {
-       my $filename = "$1"; # untaint
-       my ($type, $extension, $comp) = ($2, $3, $4);
-       (my $format = $extension) =~ s/\..*//;
-       $info = decompressor_info($format, $comp);
-       $info->{filename} = $filename;
-       $info->{type} = $type;
-
-       if ($volid =~ /^(vzdump-${type}-([1-9][0-9]{2,8})-(\d{4})_(\d{2})_(\d{2})-(\d{2})_(\d{2})_(\d{2}))\.${extension}$/) {
-           $info->{logfilename} = "$1".PVE::Storage::Plugin::LOG_EXT;
-           $info->{notesfilename} = "$filename".PVE::Storage::Plugin::NOTES_EXT;
-           $info->{vmid} = int($2);
-           $info->{ctime} = timelocal($8, $7, $6, $5, $4 - 1, $3);
-           $info->{is_std_name} = 1;
-       } else {
-           $info->{is_std_name} = 0;
-       }
-    } else {
-       die "ERROR: couldn't determine archive info from '$archive'\n";
-    }
-
-    return $info;
-}
-
-sub archive_remove {
-    my ($archive_path) = @_;
-
-    die "cannot remove protected archive '$archive_path'\n"
-       if -e protection_file_path($archive_path);
-
-    unlink $archive_path or $! == ENOENT or die "removing archive $archive_path failed: $!\n";
-
-    archive_auxiliaries_remove($archive_path);
-}
-
-sub archive_auxiliaries_remove {
-    my ($archive_path) = @_;
-
-    my $dirname = dirname($archive_path);
-    my $archive_info = eval { archive_info($archive_path) } // {};
-
-    for my $type (qw(log notes)) {
-       my $filename = $archive_info->{"${type}filename"} or next;
-       my $path = "$dirname/$filename";
-
-       if (-e $path) {
-           unlink $path or $! == ENOENT or log_warn("Removing $type file failed: $!");
-       }
-    }
-}
-
-sub extract_vzdump_config_tar {
-    my ($archive, $conf_re) = @_;
-
-    die "ERROR: file '$archive' does not exist\n" if ! -f $archive;
-
-    my $pid = open(my $fh, '-|', 'tar', 'tf', $archive) ||
-       die "unable to open file '$archive'\n";
-
-    my $file;
-    while (defined($file = <$fh>)) {
-       if ($file =~ $conf_re) {
-           $file = $1; # untaint
-           last;
-       }
-    }
-
-    kill 15, $pid;
-    waitpid $pid, 0;
-    close $fh;
-
-    die "ERROR: archive contains no configuration file\n" if !$file;
-    chomp $file;
-
-    my $raw = '';
-    my $out = sub {
-       my $output = shift;
-       $raw .= "$output\n";
-    };
-
-    run_command(['tar', '-xpOf', $archive, $file, '--occurrence'], outfunc => $out);
-
-    return wantarray ? ($raw, $file) : $raw;
-}
-
-sub extract_vzdump_config_vma {
-    my ($archive, $comp) = @_;
-
-    my $raw = '';
-    my $out = sub { $raw .= "$_[0]\n"; };
-
-    my $info = archive_info($archive);
-    $comp //= $info->{compression};
-    my $decompressor = $info->{decompressor};
-
-    if ($comp) {
-       my $cmd = [ [@$decompressor, $archive], ["vma", "config", "-"] ];
-
-       # lzop/zcat exits with 1 when the pipe is closed early by vma, detect this and ignore the exit code later
-       my $broken_pipe;
-       my $errstring;
-       my $err = sub {
-           my $output = shift;
-           if ($output =~ m/lzop: Broken pipe: <stdout>/ || $output =~ m/gzip: stdout: Broken pipe/ || $output =~ m/zstd: error 70 : Write error.*Broken pipe/) {
-               $broken_pipe = 1;
-           } elsif (!defined ($errstring) && $output !~ m/^\s*$/) {
-               $errstring = "Failed to extract config from VMA archive: $output\n";
-           }
-       };
-
-       my $rc = eval { run_command($cmd, outfunc => $out, errfunc => $err, noerr => 1) };
-       my $rerr = $@;
-
-       $broken_pipe ||= $rc == 141; # broken pipe from vma POV
-
-       if (!$errstring && !$broken_pipe && $rc != 0) {
-           die "$rerr\n" if $rerr;
-           die "config extraction failed with exit code $rc\n";
-       }
-       die "$errstring\n" if $errstring;
-    } else {
-       run_command(["vma", "config", $archive], outfunc => $out);
-    }
-
-    return wantarray ? ($raw, undef) : $raw;
-}
-
-sub extract_vzdump_config {
-    my ($cfg, $volid) = @_;
-
-    my ($storeid, $volname) = parse_volume_id($volid);
-    if (defined($storeid)) {
-       my $scfg = storage_config($cfg, $storeid);
-       if ($scfg->{type} eq 'pbs') {
-           storage_check_enabled($cfg, $storeid);
-           return PVE::Storage::PBSPlugin->extract_vzdump_config($scfg, $volname, $storeid);
-       }
-    }
-
-    my $archive = abs_filesystem_path($cfg, $volid);
-    my $info = archive_info($archive);
-    my $format = $info->{format};
-    my $comp = $info->{compression};
-    my $type = $info->{type};
-
-    if ($type eq 'lxc' || $type eq 'openvz') {
-       return extract_vzdump_config_tar($archive, qr!^(\./etc/vzdump/(pct|vps)\.conf)$!);
-    } elsif ($type eq 'qemu') {
-       if ($format eq 'tar') {
-           return extract_vzdump_config_tar($archive, qr!\(\./qemu-server\.conf\)!);
-       } else {
-           return extract_vzdump_config_vma($archive, $comp);
-       }
-    } else {
-       die "cannot determine backup guest type for backup archive '$volid'\n";
-    }
-}
-
-sub prune_backups {
-    my ($cfg, $storeid, $keep, $vmid, $type, $dryrun, $logfunc) = @_;
-
-    my $scfg = storage_config($cfg, $storeid);
-    die "storage '$storeid' does not support backups\n" if !$scfg->{content}->{backup};
-
-    if (!defined($keep)) {
-       die "no prune-backups options configured for storage '$storeid'\n"
-           if !defined($scfg->{'prune-backups'});
-       $keep = PVE::JSONSchema::parse_property_string('prune-backups', $scfg->{'prune-backups'});
-    }
-
-    activate_storage($cfg, $storeid);
-
-    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-    return $plugin->prune_backups($scfg, $storeid, $keep, $vmid, $type, $dryrun, $logfunc);
-}
-
-my $prune_mark = sub {
-    my ($prune_entries, $keep_count, $id_func) = @_;
-
-    return if !$keep_count;
-
-    my $already_included = {};
-    my $newly_included = {};
-
-    foreach my $prune_entry (@{$prune_entries}) {
-       my $mark = $prune_entry->{mark};
-       my $id = $id_func->($prune_entry->{ctime});
-       $already_included->{$id} = 1 if defined($mark) && $mark eq 'keep';
-    }
-
-    foreach my $prune_entry (@{$prune_entries}) {
-       my $mark = $prune_entry->{mark};
-       my $id = $id_func->($prune_entry->{ctime});
-
-       next if defined($mark) || $already_included->{$id};
-
-       if (!$newly_included->{$id}) {
-           last if scalar(keys %{$newly_included}) >= $keep_count;
-           $newly_included->{$id} = 1;
-           $prune_entry->{mark} = 'keep';
-       } else {
-           $prune_entry->{mark} = 'remove';
-       }
-    }
-};
-
-sub prune_mark_backup_group {
-    my ($backup_group, $keep) = @_;
-
-    my @positive_opts = grep { $_ ne 'keep-all' && $keep->{$_} > 0 } keys $keep->%*;
-
-    if ($keep->{'keep-all'} || scalar(@positive_opts) == 0) {
-       foreach my $prune_entry (@{$backup_group}) {
-           # preserve additional information like 'protected'
-           next if $prune_entry->{mark} && $prune_entry->{mark} ne 'remove';
-           $prune_entry->{mark} = 'keep';
-       }
-       return;
-    }
-
-    my $prune_list = [ sort { $b->{ctime} <=> $a->{ctime} } @{$backup_group} ];
-
-    $prune_mark->($prune_list, $keep->{'keep-last'}, sub {
-       my ($ctime) = @_;
-       return $ctime;
-    });
-    $prune_mark->($prune_list, $keep->{'keep-hourly'}, sub {
-       my ($ctime) = @_;
-       my (undef, undef, $hour, $day, $month, $year) = localtime($ctime);
-       return "$hour/$day/$month/$year";
-    });
-    $prune_mark->($prune_list, $keep->{'keep-daily'}, sub {
-       my ($ctime) = @_;
-       my (undef, undef, undef, $day, $month, $year) = localtime($ctime);
-       return "$day/$month/$year";
-    });
-    $prune_mark->($prune_list, $keep->{'keep-weekly'}, sub {
-       my ($ctime) = @_;
-       my ($sec, $min, $hour, $day, $month, $year) = localtime($ctime);
-       my $iso_week = int(strftime("%V", $sec, $min, $hour, $day, $month, $year));
-       my $iso_week_year = int(strftime("%G", $sec, $min, $hour, $day, $month, $year));
-       return "$iso_week/$iso_week_year";
-    });
-    $prune_mark->($prune_list, $keep->{'keep-monthly'}, sub {
-       my ($ctime) = @_;
-       my (undef, undef, undef, undef, $month, $year) = localtime($ctime);
-       return "$month/$year";
-    });
-    $prune_mark->($prune_list, $keep->{'keep-yearly'}, sub {
-       my ($ctime) = @_;
-       my $year = (localtime($ctime))[5];
-       return "$year";
-    });
-
-    foreach my $prune_entry (@{$prune_list}) {
-       $prune_entry->{mark} //= 'remove';
-    }
-}
-
-sub volume_export : prototype($$$$$$$) {
-    my ($cfg, $fh, $volid, $format, $snapshot, $base_snapshot, $with_snapshots) = @_;
-
-    my ($storeid, $volname) = parse_volume_id($volid, 1);
-    die "cannot export volume '$volid'\n" if !$storeid;
-    my $scfg = storage_config($cfg, $storeid);
-    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-    return $plugin->volume_export($scfg, $storeid, $fh, $volname, $format,
-                                  $snapshot, $base_snapshot, $with_snapshots);
-}
-
-sub volume_import : prototype($$$$$$$$) {
-    my ($cfg, $fh, $volid, $format, $snapshot, $base_snapshot, $with_snapshots, $allow_rename) = @_;
-
-    my ($storeid, $volname) = parse_volume_id($volid, 1);
-    die "cannot import into volume '$volid'\n" if !$storeid;
-    my $scfg = storage_config($cfg, $storeid);
-    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-    return $plugin->volume_import(
-       $scfg,
-       $storeid,
-       $fh,
-       $volname,
-       $format,
-       $snapshot,
-       $base_snapshot,
-       $with_snapshots,
-       $allow_rename,
-    ) // $volid;
-}
-
-sub volume_export_formats : prototype($$$$$) {
-    my ($cfg, $volid, $snapshot, $base_snapshot, $with_snapshots) = @_;
-
-    my ($storeid, $volname) = parse_volume_id($volid, 1);
-    return if !$storeid;
-    my $scfg = storage_config($cfg, $storeid);
-    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-    return $plugin->volume_export_formats($scfg, $storeid, $volname,
-                                          $snapshot, $base_snapshot,
-                                          $with_snapshots);
-}
-
-sub volume_import_formats : prototype($$$$$) {
-    my ($cfg, $volid, $snapshot, $base_snapshot, $with_snapshots) = @_;
-
-    my ($storeid, $volname) = parse_volume_id($volid, 1);
-    return if !$storeid;
-    my $scfg = storage_config($cfg, $storeid);
-    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-    return $plugin->volume_import_formats(
-       $scfg,
-       $storeid,
-       $volname,
-       $snapshot,
-       $base_snapshot,
-       $with_snapshots,
-    );
-}
-
-sub volume_transfer_formats {
-    my ($cfg, $src_volid, $dst_volid, $snapshot, $base_snapshot, $with_snapshots) = @_;
-    my @export_formats = volume_export_formats($cfg, $src_volid, $snapshot, $base_snapshot, $with_snapshots);
-    my @import_formats = volume_import_formats($cfg, $dst_volid, $snapshot, $base_snapshot, $with_snapshots);
-    my %import_hash = map { $_ => 1 } @import_formats;
-    my @common = grep { $import_hash{$_} } @export_formats;
-    return @common;
-}
-
-sub volume_imported_message {
-    my ($volid, $want_pattern) = @_;
-
-    if ($want_pattern) {
-       return qr/successfully imported '([^']*)'$/;
-    } else {
-       return "successfully imported '$volid'\n";
-    }
-}
-
-# $format and $volname are requests and might be overruled depending on $opts
-# $opts:
-# - with_snapshots: passed to `pvesm import` and used to select import format
-# - allow_rename: passed to `pvesm import`
-# - export_formats: used to select common transport format
-# - unix: unix socket path
-sub volume_import_start {
-    my ($cfg, $storeid, $volname, $format, $vmid, $opts) = @_;
-
-    my $with_snapshots = $opts->{'with_snapshots'} ? 1 : 0;
-
-    $volname = $volname_for_storage->($cfg, $storeid, $volname, $vmid, $format);
-
-    my $volid = "$storeid:$volname";
-
-    # find common import/export format, like volume_transfer_formats
-    my @import_formats = PVE::Storage::volume_import_formats($cfg, $volid, $opts->{snapshot}, undef, $with_snapshots);
-    my @export_formats = PVE::Tools::split_list($opts->{export_formats});
-    my %import_hash = map { $_ => 1 } @import_formats;
-    my @common = grep { $import_hash{$_} } @export_formats;
-    die "no matching import/export format found for storage '$storeid'\n"
-       if !@common;
-    $format = $common[0];
-
-    my $input = IO::File->new();
-    my $info = IO::File->new();
-
-    my $unix = $opts->{unix} // "/run/pve/storage-migrate-$vmid.$$.unix";
-    my $import = $volume_import_prepare->($volid, $format, "unix://$unix", APIVER, $opts);
-
-    unlink $unix;
-    my $cpid = open3($input, $info, $info, @$import)
-       or die "failed to spawn disk-import child - $!\n";
-
-    my $ready;
-    eval {
-       PVE::Tools::run_with_timeout(5, sub { $ready = <$info>; });
-    };
-
-    die "failed to read readyness from disk import child: $@\n" if $@;
-
-    print "$ready\n";
-
-    return {
-       fh => $info,
-       pid => $cpid,
-       socket => $unix,
-       format => $format,
-    };
-}
-
-sub volume_export_start {
-    my ($cfg, $volid, $format, $log, $opts) = @_;
-
-    my $known_format = [ grep { $_ eq $format } $KNOWN_EXPORT_FORMATS->@* ];
-    if (!$known_format->@*) {
-       die "Cannot export '$volid' using unknown export format '$format'\n";
-    }
-    $format = $known_format->[0];
-
-    my $run_command_params = delete $opts->{cmd} // {};
-
-    my $cmds = $volume_export_prepare->($cfg, $volid, $format, $log, $opts);
-
-    PVE::Tools::run_command($cmds, %$run_command_params);
-}
-
-# bash completion helper
-
-sub complete_storage {
-    my ($cmdname, $pname, $cvalue) = @_;
-
-    my $cfg = PVE::Storage::config();
-
-    return  $cmdname eq 'add' ? [] : [ PVE::Storage::storage_ids($cfg) ];
-}
-
-sub complete_storage_enabled {
-    my ($cmdname, $pname, $cvalue) = @_;
-
-    my $res = [];
-
-    my $cfg = PVE::Storage::config();
-    foreach my $sid (keys %{$cfg->{ids}}) {
-       next if !storage_check_enabled($cfg, $sid, undef, 1);
-       push @$res, $sid;
-    }
-    return $res;
-}
-
-sub complete_content_type {
-    my ($cmdname, $pname, $cvalue) = @_;
-
-    return [qw(rootdir images vztmpl iso backup snippets)];
-}
-
-sub complete_volume {
-    my ($cmdname, $pname, $cvalue) = @_;
-
-    my $cfg = config();
-
-    my $storage_list = complete_storage_enabled();
-
-    if ($cvalue =~ m/^([^:]+):/) {
-       $storage_list = [ $1 ];
-    } else {
-       if (scalar(@$storage_list) > 1) {
-           # only list storage IDs to avoid large listings
-           my $res = [];
-           foreach my $storeid (@$storage_list) {
-               # Hack: simply return 2 artificial values, so that
-               # completions does not finish
-               push @$res, "$storeid:volname", "$storeid:...";
-           }
-           return $res;
-       }
-    }
-
-    my $res = [];
-    foreach my $storeid (@$storage_list) {
-       my $vollist = PVE::Storage::volume_list($cfg, $storeid);
-
-       foreach my $item (@$vollist) {
-           push @$res, $item->{volid};
-       }
-    }
-
-    return $res;
-}
-
-sub rename_volume {
-    my ($cfg, $source_volid, $target_vmid, $target_volname) = @_;
-
-    die "no source volid provided\n" if !$source_volid;
-    die "no target VMID or target volname provided\n" if !$target_vmid && !$target_volname;
-
-    my ($storeid, $source_volname) = parse_volume_id($source_volid);
-
-    activate_storage($cfg, $storeid);
-
-    my $scfg = storage_config($cfg, $storeid);
-    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
-
-    $target_vmid = ($plugin->parse_volname($source_volname))[3] if !$target_vmid;
-
-    return $plugin->cluster_lock_storage($storeid, $scfg->{shared}, undef, sub {
-       return $plugin->rename_volume($scfg, $storeid, $source_volname, $target_vmid, $target_volname);
-    });
-}
-
-# Various io-heavy operations require io/bandwidth limits which can be
-# configured on multiple levels: The global defaults in datacenter.cfg, and
-# per-storage overrides. When we want to do a restore from storage A to storage
-# B, we should take the smaller limit defined for storages A and B, and if no
-# such limit was specified, use the one from datacenter.cfg.
-sub get_bandwidth_limit {
-    my ($operation, $storage_list, $override) = @_;
-
-    # called for each limit (global, per-storage) with the 'default' and the
-    # $operation limit and should update $override for every limit affecting
-    # us.
-    my $use_global_limits = 0;
-    my $apply_limit = sub {
-       my ($bwlimit) = @_;
-       if (defined($bwlimit)) {
-           my $limits = PVE::JSONSchema::parse_property_string('bwlimit', $bwlimit);
-           my $limit = $limits->{$operation} // $limits->{default};
-           if (defined($limit)) {
-               if (!$override || $limit < $override) {
-                   $override = $limit;
-               }
-               return;
-           }
-       }
-       # If there was no applicable limit, try to apply the global ones.
-       $use_global_limits = 1;
-    };
-
-    my ($rpcenv, $authuser);
-    if (defined($override)) {
-       $rpcenv = PVE::RPCEnvironment->get();
-       $authuser = $rpcenv->get_user();
-    }
-
-    # Apply per-storage limits - if there are storages involved.
-    if (defined($storage_list) && grep { defined($_) } $storage_list->@*) {
-       my $config = config();
-
-       # The Datastore.Allocate permission allows us to modify the per-storage
-       # limits, therefore it also allows us to override them.
-       # Since we have most likely multiple storages to check, do a quick check on
-       # the general '/storage' path to see if we can skip the checks entirely:
-       return $override if $rpcenv && $rpcenv->check($authuser, '/storage', ['Datastore.Allocate'], 1);
-
-       my %done;
-       foreach my $storage (@$storage_list) {
-           next if !defined($storage);
-           # Avoid duplicate checks:
-           next if $done{$storage};
-           $done{$storage} = 1;
-
-           # Otherwise we may still have individual /storage/$ID permissions:
-           if (!$rpcenv || !$rpcenv->check($authuser, "/storage/$storage", ['Datastore.Allocate'], 1)) {
-               # And if not: apply the limits.
-               my $storecfg = storage_config($config, $storage);
-               $apply_limit->($storecfg->{bwlimit});
-           }
-       }
-
-       # Storage limits take precedence over the datacenter defaults, so if
-       # a limit was applied:
-       return $override if !$use_global_limits;
-    }
-
-    # Sys.Modify on '/' means we can change datacenter.cfg which contains the
-    # global default limits.
-    if (!$rpcenv || !$rpcenv->check($authuser, '/', ['Sys.Modify'], 1)) {
-       # So if we cannot modify global limits, apply them to our currently
-       # requested override.
-       my $dc = cfs_read_file('datacenter.cfg');
-       $apply_limit->($dc->{bwlimit});
-    }
-
-    return $override;
-}
-
-# checks if the storage id is available and dies if not
-sub assert_sid_unused {
-    my ($sid) = @_;
-
-    my $cfg = config();
-    if (my $scfg = storage_config($cfg, $sid, 1)) {
-       die "storage ID '$sid' already defined\n";
-    }
-
-    return undef;
-}
-
-# removes leading/trailing spaces and (back)slashes completely
-# substitutes every non-ASCII-alphanumerical char with '_', except '_.-'
-sub normalize_content_filename {
-    my ($filename) = @_;
-
-    chomp $filename;
-    $filename =~ s/^.*[\/\\]//;
-    $filename =~ s/[^a-zA-Z0-9_.-]/_/g;
-
-    return $filename;
-}
-
-1;
diff --git a/PVE/Storage/BTRFSPlugin.pm b/PVE/Storage/BTRFSPlugin.pm
deleted file mode 100644 (file)
index 1db4e4f..0000000
+++ /dev/null
@@ -1,933 +0,0 @@
-package PVE::Storage::BTRFSPlugin;
-
-use strict;
-use warnings;
-
-use base qw(PVE::Storage::Plugin);
-
-use Fcntl qw(S_ISDIR O_WRONLY O_CREAT O_EXCL);
-use File::Basename qw(basename dirname);
-use File::Path qw(mkpath);
-use IO::Dir;
-use POSIX qw(EEXIST);
-
-use PVE::Tools qw(run_command dir_glob_foreach);
-
-use PVE::Storage::DirPlugin;
-
-use constant {
-    BTRFS_FIRST_FREE_OBJECTID => 256,
-    FS_NOCOW_FL => 0x00800000,
-    FS_IOC_GETFLAGS => 0x40086602,
-    FS_IOC_SETFLAGS => 0x80086601,
-    BTRFS_MAGIC => 0x9123683e,
-};
-
-# Configuration (similar to DirPlugin)
-
-sub type {
-    return 'btrfs';
-}
-
-sub plugindata {
-    return {
-       content => [
-           {
-               images => 1,
-               rootdir => 1,
-               vztmpl => 1,
-               iso => 1,
-               backup => 1,
-               snippets => 1,
-               none => 1,
-           },
-           { images => 1, rootdir => 1 },
-       ],
-       format => [ { raw => 1, subvol => 1 }, 'raw', ],
-    };
-}
-
-sub properties {
-    return {
-       nocow => {
-           description => "Set the NOCOW flag on files."
-               . " Disables data checksumming and causes data errors to be unrecoverable from"
-               . " while allowing direct I/O. Only use this if data does not need to be any more"
-               . " safe than on a single ext4 formatted disk with no underlying raid system.",
-           type => 'boolean',
-           default => 0,
-       },
-    };
-}
-
-sub options {
-    return {
-       path => { fixed => 1 },
-       nodes => { optional => 1 },
-       shared => { optional => 1 },
-       disable => { optional => 1 },
-       maxfiles => { optional => 1 },
-       'prune-backups' => { optional => 1 },
-       'max-protected-backups' => { optional => 1 },
-       content => { optional => 1 },
-       format => { optional => 1 },
-       is_mountpoint => { optional => 1 },
-       nocow => { optional => 1 },
-       mkdir => { optional => 1 },
-       preallocation => { optional => 1 },
-       # TODO: The new variant of mkdir with  `populate` vs `create`...
-    };
-}
-
-# Storage implementation
-#
-# We use the same volume names are directory plugins, but map *raw* disk image file names into a
-# subdirectory.
-#
-# `vm-VMID-disk-ID.raw`
-#   -> `images/VMID/vm-VMID-disk-ID/disk.raw`
-#   where the `vm-VMID-disk-ID/` subdirectory is a btrfs subvolume
-
-# Reuse `DirPlugin`'s `check_config`. This simply checks for invalid paths.
-sub check_config {
-    my ($self, $sectionId, $config, $create, $skipSchemaCheck) = @_;
-    return PVE::Storage::DirPlugin::check_config($self, $sectionId, $config, $create, $skipSchemaCheck);
-}
-
-my sub getfsmagic($) {
-    my ($path) = @_;
-    # The field type sizes in `struct statfs` are defined in a rather annoying way, and we only
-    # need the first field, which is a `long` for our supported platforms.
-    # Should be moved to pve-rs, so this can be the problem of the `libc` crate ;-)
-    # Just round up and extract what we need:
-    my $buf = pack('x160');
-    if (0 != syscall(&PVE::Syscall::SYS_statfs, $path, $buf)) {
-       die "statfs on '$path' failed - $!\n";
-    }
-
-    return unpack('L!', $buf);
-}
-
-my sub assert_btrfs($) {
-    my ($path) = @_;
-    die "'$path' is not a btrfs file system\n"
-       if getfsmagic($path) != BTRFS_MAGIC;
-}
-
-sub activate_storage {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    my $path = $scfg->{path};
-    if (!defined($scfg->{mkdir}) || $scfg->{mkdir}) {
-       mkpath $path;
-    }
-
-    my $mp = PVE::Storage::DirPlugin::parse_is_mountpoint($scfg);
-    if (defined($mp) && !PVE::Storage::DirPlugin::path_is_mounted($mp, $cache->{mountdata})) {
-       die "unable to activate storage '$storeid' - directory is expected to be a mount point but"
-       ." is not mounted: '$mp'\n";
-    }
-
-    assert_btrfs($path); # only assert this stuff now, ensures $path is there and better UX
-
-    $class->SUPER::activate_storage($storeid, $scfg, $cache);
-}
-
-sub status {
-    my ($class, $storeid, $scfg, $cache) = @_;
-    return PVE::Storage::DirPlugin::status($class, $storeid, $scfg, $cache);
-}
-
-sub get_volume_attribute {
-    my ($class, $scfg, $storeid, $volname, $attribute) = @_;
-    return PVE::Storage::DirPlugin::get_volume_attribute($class, $scfg, $storeid, $volname, $attribute);
-}
-
-sub update_volume_attribute {
-    my ($class, $scfg, $storeid, $volname, $attribute, $value) = @_;
-    return PVE::Storage::DirPlugin::update_volume_attribute(
-       $class,
-       $scfg,
-       $storeid,
-       $volname,
-       $attribute,
-       $value,
-    );
-}
-
-# croak would not include the caller from within this module
-sub __error {
-    my ($msg) = @_;
-    my (undef, $f, $n) = caller(1);
-    die "$msg at $f: $n\n";
-}
-
-# Given a name (eg. `vm-VMID-disk-ID.raw`), take the part up to the format suffix as the name of
-# the subdirectory (subvolume).
-sub raw_name_to_dir($) {
-    my ($raw) = @_;
-
-    # For the subvolume directory Strip the `.<format>` suffix:
-    if ($raw =~ /^(.*)\.raw$/) {
-       return $1;
-    }
-
-    __error "internal error: bad disk name: $raw";
-}
-
-sub raw_file_to_subvol($) {
-    my ($file) = @_;
-
-    if ($file =~ m|^(.*)/disk\.raw$|) {
-       return "$1";
-    }
-
-    __error "internal error: bad raw path: $file";
-}
-
-sub filesystem_path {
-    my ($class, $scfg, $volname, $snapname) = @_;
-
-    my ($vtype, $name, $vmid, undef, undef, $isBase, $format) =
-       $class->parse_volname($volname);
-
-    my $path = $class->get_subdir($scfg, $vtype);
-
-    $path .= "/$vmid" if $vtype eq 'images';
-
-    if (defined($format) && $format eq 'raw') {
-       my $dir = raw_name_to_dir($name);
-       if ($snapname) {
-           $dir .= "\@$snapname";
-       }
-       $path .= "/$dir/disk.raw";
-    } elsif (defined($format) && $format eq 'subvol') {
-       $path .= "/$name";
-       if ($snapname) {
-           $path .= "\@$snapname";
-       }
-    } else {
-       $path .= "/$name";
-    }
-
-    return wantarray ? ($path, $vmid, $vtype) : $path;
-}
-
-sub btrfs_cmd {
-    my ($class, $cmd, $outfunc) = @_;
-
-    my $msg = '';
-    my $func;
-    if (defined($outfunc)) {
-       $func = sub {
-           my $part = &$outfunc(@_);
-           $msg .= $part if defined($part);
-       };
-    } else {
-       $func = sub { $msg .= "$_[0]\n" };
-    }
-    run_command(['btrfs', '-q', @$cmd], errmsg => 'btrfs error', outfunc => $func);
-
-    return $msg;
-}
-
-sub btrfs_get_subvol_id {
-    my ($class, $path) = @_;
-    my $info = $class->btrfs_cmd(['subvolume', 'show', '--', $path]);
-    if ($info !~ /^\s*(?:Object|Subvolume) ID:\s*(\d+)$/m) {
-       die "failed to get btrfs subvolume ID from: $info\n";
-    }
-    return $1;
-}
-
-my sub chattr : prototype($$$) {
-    my ($fh, $mask, $xor) = @_;
-
-    my $flags = pack('L!', 0);
-    ioctl($fh, FS_IOC_GETFLAGS, $flags) or die "FS_IOC_GETFLAGS failed - $!\n";
-    $flags = pack('L!', (unpack('L!', $flags) & $mask) ^ $xor);
-    ioctl($fh, FS_IOC_SETFLAGS, $flags) or die "FS_IOC_SETFLAGS failed - $!\n";
-    return 1;
-}
-
-sub create_base {
-    my ($class, $storeid, $scfg, $volname) = @_;
-
-    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) =
-       $class->parse_volname($volname);
-
-    my $newname = $name;
-    $newname =~ s/^vm-/base-/;
-
-    # If we're not working with a 'raw' file, which is the only thing that's "different" for btrfs,
-    # or a subvolume, we forward to the DirPlugin
-    if ($format ne 'raw' && $format ne 'subvol') {
-       return PVE::Storage::DirPlugin::create_base(@_);
-    }
-
-    my $path = $class->filesystem_path($scfg, $volname);
-    my $newvolname = $basename ? "$basevmid/$basename/$vmid/$newname" : "$vmid/$newname";
-    my $newpath = $class->filesystem_path($scfg, $newvolname);
-
-    my $subvol = $path;
-    my $newsubvol = $newpath;
-    if ($format eq 'raw') {
-       $subvol = raw_file_to_subvol($subvol);
-       $newsubvol = raw_file_to_subvol($newsubvol);
-    }
-
-    rename($subvol, $newsubvol)
-       || die "rename '$subvol' to '$newsubvol' failed - $!\n";
-    eval { $class->btrfs_cmd(['property', 'set', $newsubvol, 'ro', 'true']) };
-    warn $@ if $@;
-
-    return $newvolname;
-}
-
-sub clone_image {
-    my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
-
-    my ($vtype, $basename, $basevmid, undef, undef, $isBase, $format) =
-       $class->parse_volname($volname);
-
-    # If we're not working with a 'raw' file, which is the only thing that's "different" for btrfs,
-    # or a subvolume, we forward to the DirPlugin
-    if ($format ne 'raw' && $format ne 'subvol') {
-       return PVE::Storage::DirPlugin::clone_image(@_);
-    }
-
-    my $imagedir = $class->get_subdir($scfg, 'images');
-    $imagedir .= "/$vmid";
-    mkpath $imagedir;
-
-    my $path = $class->filesystem_path($scfg, $volname);
-    my $newname = $class->find_free_diskname($storeid, $scfg, $vmid, $format, 1);
-
-    # For btrfs subvolumes we don't actually need the "link":
-    #my $newvolname = "$basevmid/$basename/$vmid/$newname";
-    my $newvolname = "$vmid/$newname";
-    my $newpath = $class->filesystem_path($scfg, $newvolname);
-
-    my $subvol = $path;
-    my $newsubvol = $newpath;
-    if ($format eq 'raw') {
-       $subvol = raw_file_to_subvol($subvol);
-       $newsubvol = raw_file_to_subvol($newsubvol);
-    }
-
-    $class->btrfs_cmd(['subvolume', 'snapshot', '--', $subvol, $newsubvol]);
-
-    return $newvolname;
-}
-
-sub alloc_image {
-    my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
-
-    if ($fmt ne 'raw' && $fmt ne 'subvol') {
-       return $class->SUPER::alloc_image($storeid, $scfg, $vmid, $fmt, $name, $size);
-    }
-
-    # From Plugin.pm:
-
-    my $imagedir = $class->get_subdir($scfg, 'images') . "/$vmid";
-
-    mkpath $imagedir;
-
-    $name = $class->find_free_diskname($storeid, $scfg, $vmid, $fmt, 1) if !$name;
-
-    my (undef, $tmpfmt) = PVE::Storage::Plugin::parse_name_dir($name);
-
-    die "illegal name '$name' - wrong extension for format ('$tmpfmt != '$fmt')\n"
-       if $tmpfmt ne $fmt;
-
-    # End copy from Plugin.pm
-
-    my $subvol = "$imagedir/$name";
-    # .raw is not part of the directory name
-    $subvol =~ s/\.raw$//;
-
-    die "disk image '$subvol' already exists\n" if -e $subvol;
-
-    my $path;
-    if ($fmt eq 'raw') {
-       $path = "$subvol/disk.raw";
-    }
-
-    if ($fmt eq 'subvol' && !!$size) {
-       # NOTE: `btrfs send/recv` actually drops quota information so supporting subvolumes with
-       # quotas doesn't play nice with send/recv.
-       die "btrfs quotas are currently not supported, use an unsized subvolume or a raw file\n";
-    }
-
-    $class->btrfs_cmd(['subvolume', 'create', '--', $subvol]);
-
-    eval {
-       if ($fmt eq 'subvol') {
-           # Nothing to do for now...
-
-           # This is how we *would* do it:
-           # # Use the subvol's default 0/$id qgroup
-           # eval {
-           #     # This call should happen at storage creation instead and therefore governed by a
-           #     # configuration option!
-           #     # $class->btrfs_cmd(['quota', 'enable', $subvol]);
-           #     my $id = $class->btrfs_get_subvol_id($subvol);
-           #     $class->btrfs_cmd(['qgroup', 'limit', "${size}k", "0/$id", $subvol]);
-           # };
-       } elsif ($fmt eq 'raw') {
-           sysopen my $fh, $path, O_WRONLY | O_CREAT | O_EXCL
-               or die "failed to create raw file '$path' - $!\n";
-           chattr($fh, ~FS_NOCOW_FL, FS_NOCOW_FL) if $scfg->{nocow};
-           truncate($fh, $size * 1024)
-               or die "failed to set file size for '$path' - $!\n";
-           close($fh);
-       } else {
-           die "internal format error (format = $fmt)\n";
-       }
-    };
-
-    if (my $err = $@) {
-       eval { $class->btrfs_cmd(['subvolume', 'delete', '--', $subvol]); };
-       warn $@ if $@;
-       die $err;
-    }
-
-    return "$vmid/$name";
-}
-
-# Same as btrfsprogs does:
-my sub path_is_subvolume : prototype($) {
-    my ($path) = @_;
-    my @stat = stat($path)
-       or die "stat failed on '$path' - $!\n";
-    my ($ino, $mode) = @stat[1, 2];
-    return S_ISDIR($mode) && $ino == BTRFS_FIRST_FREE_OBJECTID;
-}
-
-my $BTRFS_VOL_REGEX = qr/((?:vm|base|subvol)-\d+-disk-\d+(?:\.subvol)?)(?:\@(\S+))$/;
-
-# Calls `$code->($volume, $name, $snapshot)` for each subvol in a directory matching our volume
-# regex.
-my sub foreach_subvol : prototype($$) {
-    my ($dir, $code) = @_;
-
-    dir_glob_foreach($dir, $BTRFS_VOL_REGEX, sub {
-       my ($volume, $name, $snapshot) = ($1, $2, $3);
-       return if !path_is_subvolume("$dir/$volume");
-       $code->($volume, $name, $snapshot);
-    })
-}
-
-sub free_image {
-    my ($class, $storeid, $scfg, $volname, $isBase, $_format) = @_;
-
-    my (undef, undef, $vmid, undef, undef, undef, $format) =
-       $class->parse_volname($volname);
-
-    if (!defined($format) || ($format ne 'subvol' && $format ne 'raw')) {
-       return $class->SUPER::free_image($storeid, $scfg, $volname, $isBase, $_format);
-    }
-
-    my $path = $class->filesystem_path($scfg, $volname);
-
-    my $subvol = $path;
-    if ($format eq 'raw') {
-       $subvol = raw_file_to_subvol($path);
-    }
-
-    my $dir = dirname($subvol);
-    my $basename = basename($subvol);
-    my @snapshot_vols;
-    foreach_subvol($dir, sub {
-       my ($volume, $name, $snapshot) = @_;
-       return if $name ne $basename;
-       return if !defined $snapshot;
-       push @snapshot_vols, "$dir/$volume";
-    });
-
-    $class->btrfs_cmd(['subvolume', 'delete', '--', @snapshot_vols, $subvol]);
-    # try to cleanup directory to not clutter storage with empty $vmid dirs if
-    # all images from a guest got deleted
-    rmdir($dir);
-
-    return undef;
-}
-
-# Currently not used because quotas clash with send/recv.
-# my sub btrfs_subvol_quota {
-#     my ($class, $path) = @_;
-#     my $id = '0/' . $class->btrfs_get_subvol_id($path);
-#     my $search = qr/^\Q$id\E\s+(\d)+\s+\d+\s+(\d+)\s*$/;
-#     my ($used, $size);
-#     $class->btrfs_cmd(['qgroup', 'show', '--raw', '-rf', '--', $path], sub {
-#      return if defined($size);
-#      if ($_[0] =~ $search) {
-#          ($used, $size) = ($1, $2);
-#      }
-#     });
-#     if (!defined($size)) {
-#      # syslog should include more information:
-#      syslog('err', "failed to get subvolume size for: $path (id $id)");
-#      # UI should only see the last path component:
-#      $path =~ s|^.*/||;
-#      die "failed to get subvolume size for $path\n";
-#     }
-#     return wantarray ? ($used, $size) : $size;
-# }
-
-sub volume_size_info {
-    my ($class, $scfg, $storeid, $volname, $timeout) = @_;
-
-    my $path = $class->filesystem_path($scfg, $volname);
-
-    my $format = ($class->parse_volname($volname))[6];
-
-    if (defined($format) && $format eq 'subvol') {
-       my $ctime = (stat($path))[10];
-       my ($used, $size) = (0, 0);
-       #my ($used, $size) = btrfs_subvol_quota($class, $path); # uses wantarray
-       return wantarray ? ($size, 'subvol', $used, undef, $ctime) : 1;
-    }
-
-    return PVE::Storage::Plugin::file_size_info($path, $timeout);
-}
-
-sub volume_resize {
-    my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
-
-    my $format = ($class->parse_volname($volname))[6];
-    if ($format eq 'subvol') {
-       my $path = $class->filesystem_path($scfg, $volname);
-       my $id = '0/' . $class->btrfs_get_subvol_id($path);
-       $class->btrfs_cmd(['qgroup', 'limit', '--', "${size}k", "0/$id", $path]);
-       return undef;
-    }
-
-    return PVE::Storage::Plugin::volume_resize(@_);
-}
-
-sub volume_snapshot {
-    my ($class, $scfg, $storeid, $volname, $snap) = @_;
-
-    my ($name, $vmid, $format) = ($class->parse_volname($volname))[1,2,6];
-    if ($format ne 'subvol' && $format ne 'raw') {
-       return PVE::Storage::Plugin::volume_snapshot(@_);
-    }
-
-    my $path = $class->filesystem_path($scfg, $volname);
-    my $snap_path = $class->filesystem_path($scfg, $volname, $snap);
-
-    if ($format eq 'raw') {
-       $path = raw_file_to_subvol($path);
-       $snap_path = raw_file_to_subvol($snap_path);
-    }
-
-    my $snapshot_dir = $class->get_subdir($scfg, 'images') . "/$vmid";
-    mkpath $snapshot_dir;
-
-    $class->btrfs_cmd(['subvolume', 'snapshot', '-r', '--', $path, $snap_path]);
-    return undef;
-}
-
-sub volume_rollback_is_possible {
-    my ($class, $scfg, $storeid, $volname, $snap, $blockers) = @_;
-
-    return 1; 
-}
-
-sub volume_snapshot_rollback {
-    my ($class, $scfg, $storeid, $volname, $snap) = @_;
-
-    my ($name, $format) = ($class->parse_volname($volname))[1,6];
-
-    if ($format ne 'subvol' && $format ne 'raw') {
-       return PVE::Storage::Plugin::volume_snapshot_rollback(@_);
-    }
-
-    my $path = $class->filesystem_path($scfg, $volname);
-    my $snap_path = $class->filesystem_path($scfg, $volname, $snap);
-
-    if ($format eq 'raw') {
-       $path = raw_file_to_subvol($path);
-       $snap_path = raw_file_to_subvol($snap_path);
-    }
-
-    # Simple version would be:
-    #   rename old to temp
-    #   create new
-    #   on error rename temp back
-    # But for atomicity in case the rename after create-failure *also* fails, we create the new
-    # subvol first, then use RENAME_EXCHANGE, 
-    my $tmp_path = "$path.tmp.$$";
-    $class->btrfs_cmd(['subvolume', 'snapshot', '--', $snap_path, $tmp_path]);
-    # The paths are absolute, so pass -1 as file descriptors.
-    my $ok = PVE::Tools::renameat2(-1, $tmp_path, -1, $path, &PVE::Tools::RENAME_EXCHANGE);
-
-    eval { $class->btrfs_cmd(['subvolume', 'delete', '--', $tmp_path]) };
-    warn "failed to remove '$tmp_path' subvolume: $@" if $@;
-
-    if (!$ok) {
-       die "failed to rotate '$tmp_path' into place at '$path' - $!\n";
-    }
-
-    return undef;
-}
-
-sub volume_snapshot_delete {
-    my ($class, $scfg, $storeid, $volname, $snap, $running) = @_;
-
-    my ($name, $vmid, $format) = ($class->parse_volname($volname))[1,2,6];
-
-    if ($format ne 'subvol' && $format ne 'raw') {
-       return PVE::Storage::Plugin::volume_snapshot_delete(@_);
-    }
-
-    my $path = $class->filesystem_path($scfg, $volname, $snap);
-
-    if ($format eq 'raw') {
-       $path = raw_file_to_subvol($path);
-    }
-
-    $class->btrfs_cmd(['subvolume', 'delete', '--', $path]);
-
-    return undef;
-}
-
-sub volume_has_feature {
-    my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
-
-    my $features = {
-       snapshot => {
-           current => { qcow2 => 1, raw => 1, subvol => 1 },
-           snap => { qcow2 => 1, raw => 1, subvol => 1 }
-       },
-       clone => {
-           base => { qcow2 => 1, raw => 1, subvol => 1, vmdk => 1 },
-           current => { raw => 1 },
-           snap => { raw => 1 },
-       },
-       template => {
-           current => { qcow2 => 1, raw => 1, vmdk => 1, subvol => 1 },
-       },
-       copy => {
-           base => { qcow2 => 1, raw => 1, subvol => 1, vmdk => 1 },
-           current => { qcow2 => 1, raw => 1, subvol => 1, vmdk => 1 },
-           snap => { qcow2 => 1, raw => 1, subvol => 1 },
-       },
-       sparseinit => {
-           base => { qcow2 => 1, raw => 1, vmdk => 1 },
-           current => { qcow2 => 1, raw => 1, vmdk => 1 },
-       },
-    };
-
-    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) = $class->parse_volname($volname);
-
-    my $key = undef;
-    if ($snapname) {
-        $key = 'snap';
-    } else {
-        $key =  $isBase ? 'base' : 'current';
-    }
-
-    return 1 if defined($features->{$feature}->{$key}->{$format});
-
-    return undef;
-}
-
-sub list_images {
-    my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
-    my $imagedir = $class->get_subdir($scfg, 'images');
-
-    my $res = [];
-
-    # Copied from Plugin.pm, with file_size_info calls adapted:
-    foreach my $fn (<$imagedir/[0-9][0-9]*/*>) {
-       # different to in Plugin.pm the regex below also excludes '@' as valid file name
-       next if $fn !~ m@^(/.+/(\d+)/([^/\@.]+(?:\.(qcow2|vmdk|subvol))?))$@;
-       $fn = $1; # untaint
-
-       my $owner = $2;
-       my $name = $3;
-       my $ext = $4;
-
-       next if !$vollist && defined($vmid) && ($owner ne $vmid);
-
-       my $volid = "$storeid:$owner/$name";
-       my ($size, $format, $used, $parent, $ctime);
-
-       if (!$ext) { # raw
-           $volid .= '.raw';
-           ($size, $format, $used, $parent, $ctime) = PVE::Storage::Plugin::file_size_info("$fn/disk.raw");
-       } elsif ($ext eq 'subvol') {
-           ($used, $size) = (0, 0);
-           #($used, $size) = btrfs_subvol_quota($class, $fn);
-           $format = 'subvol';
-       } else {
-           ($size, $format, $used, $parent, $ctime) = PVE::Storage::Plugin::file_size_info($fn);
-       }
-       next if !($format && defined($size));
-
-       if ($vollist) {
-           next if ! grep { $_ eq $volid } @$vollist;
-       }
-
-       my $info = {
-           volid => $volid, format => $format,
-           size => $size, vmid => $owner, used => $used, parent => $parent,
-       };
-
-        $info->{ctime} = $ctime if $ctime;
-
-        push @$res, $info;
-    }
-
-    return $res;
-}
-
-sub volume_export_formats {
-    my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
-
-    # We can do whatever `DirPlugin` can do.
-    my @result = PVE::Storage::Plugin::volume_export_formats(@_);
-
-    # `btrfs send` only works on snapshots:
-    return @result if !defined $snapshot;
-
-    # Incremental stream with snapshots is only supported if the snapshots are listed (new api):
-    return @result if defined($base_snapshot) && $with_snapshots && ref($with_snapshots) ne 'ARRAY';
-
-    # Otherwise we do also support `with_snapshots`.
-
-    # Finally, `btrfs send` only works on formats where we actually use btrfs subvolumes:
-    my $format = ($class->parse_volname($volname))[6];
-    return @result if $format ne 'raw' && $format ne 'subvol';
-
-    return ('btrfs', @result);
-}
-
-sub volume_import_formats {
-    my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
-
-    # Same as export-formats, beware the parameter order:
-    return volume_export_formats(
-       $class,
-       $scfg,
-       $storeid,
-       $volname,
-       $snapshot,
-       $base_snapshot,
-       $with_snapshots,
-    );
-}
-
-sub volume_export {
-    my (
-       $class,
-       $scfg,
-       $storeid,
-       $fh,
-       $volname,
-       $format,
-       $snapshot,
-       $base_snapshot,
-       $with_snapshots,
-    ) = @_;
-
-    if ($format ne 'btrfs') {
-       return PVE::Storage::Plugin::volume_export(@_);
-    }
-
-    die "format 'btrfs' only works on snapshots\n"
-       if !defined $snapshot;
-
-    die "'btrfs' format in incremental mode requires snapshots to be listed explicitly\n"
-       if defined($base_snapshot) && $with_snapshots && ref($with_snapshots) ne 'ARRAY';
-
-    my $volume_format = ($class->parse_volname($volname))[6];
-
-    die "btrfs-sending volumes of type $volume_format ('$volname') is not supported\n"
-       if $volume_format ne 'raw' && $volume_format ne 'subvol';
-
-    my $path = $class->path($scfg, $volname, $storeid);
-
-    if ($volume_format eq 'raw') {
-       $path = raw_file_to_subvol($path);
-    }
-
-    my $cmd = ['btrfs', '-q', 'send', '-e'];
-    if ($base_snapshot) {
-       my $base = $class->path($scfg, $volname, $storeid, $base_snapshot);
-       if ($volume_format eq 'raw') {
-           $base = raw_file_to_subvol($base);
-       }
-       push @$cmd, '-p', $base;
-    }
-    push @$cmd, '--';
-    if (ref($with_snapshots) eq 'ARRAY') {
-       push @$cmd, (map { "$path\@$_" } ($with_snapshots // [])->@*), $path;
-    } else {
-       dir_glob_foreach(dirname($path), $BTRFS_VOL_REGEX, sub {
-           push @$cmd, "$path\@$_[2]" if !(defined($snapshot) && $_[2] eq $snapshot);
-       });
-    }
-    $path .= "\@$snapshot" if defined($snapshot);
-    push @$cmd, $path;
-
-    run_command($cmd, output => '>&'.fileno($fh));
-    return;
-}
-
-sub volume_import {
-    my (
-       $class,
-       $scfg,
-       $storeid,
-       $fh,
-       $volname,
-       $format,
-       $snapshot,
-       $base_snapshot,
-       $with_snapshots,
-       $allow_rename,
-    ) = @_;
-
-    if ($format ne 'btrfs') {
-       return PVE::Storage::Plugin::volume_import(@_);
-    }
-
-    die "format 'btrfs' only works on snapshots\n"
-       if !defined $snapshot;
-
-    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $volume_format) =
-       $class->parse_volname($volname);
-
-    die "btrfs-receiving volumes of type $volume_format ('$volname') is not supported\n"
-       if $volume_format ne 'raw' && $volume_format ne 'subvol';
-
-    if (defined($base_snapshot)) {
-       my $path = $class->path($scfg, $volname, $storeid, $base_snapshot);
-       die "base snapshot '$base_snapshot' not found - no such directory '$path'\n"
-           if !path_is_subvolume($path);
-    }
-
-    my $destination = $class->filesystem_path($scfg, $volname);
-    if ($volume_format eq 'raw') {
-       $destination = raw_file_to_subvol($destination);
-    }
-
-    if (!defined($base_snapshot) && -e $destination) {
-       die "volume $volname already exists\n" if !$allow_rename;
-       $volname = $class->find_free_diskname($storeid, $scfg, $vmid, $volume_format, 1);
-    }
-
-    my $imagedir = $class->get_subdir($scfg, $vtype);
-    $imagedir .= "/$vmid" if $vtype eq 'images';
-
-    my $tmppath = "$imagedir/recv.$vmid.tmp";
-    mkdir($imagedir); # FIXME: if $scfg->{mkdir};
-    if (!mkdir($tmppath)) {
-       die "temp receive directory already exists at '$tmppath', incomplete concurrent import?\n"
-           if $! == EEXIST;
-       die "failed to create temporary receive directory at '$tmppath' - $!\n";
-    }
-
-    my $dh = IO::Dir->new($tmppath)
-       or die "failed to open temporary receive directory '$tmppath' - $!\n";
-    eval {
-       run_command(['btrfs', '-q', 'receive', '-e', '--', $tmppath], input => '<&'.fileno($fh));
-
-       # Analyze the received subvolumes;
-       my ($diskname, $found_snapshot, @snapshots);
-       $dh->rewind;
-       while (defined(my $entry = $dh->read)) {
-           next if $entry eq '.' || $entry eq '..';
-           next if $entry !~ /^$BTRFS_VOL_REGEX$/;
-           my ($cur_diskname, $cur_snapshot) = ($1, $2);
-
-           die "send stream included a non-snapshot subvolume\n"
-               if !defined($cur_snapshot);
-
-           if (!defined($diskname)) {
-               $diskname = $cur_diskname;
-           } else {
-               die "multiple disks contained in stream ('$diskname' vs '$cur_diskname')\n"
-                   if $diskname ne $cur_diskname;
-           }
-
-           if ($cur_snapshot eq $snapshot) {
-               $found_snapshot = 1;
-           } else {
-               push @snapshots, $cur_snapshot;
-           }
-       }
-
-       die "send stream did not contain the expected current snapshot '$snapshot'\n"
-           if !$found_snapshot;
-
-       # Rotate the disk into place, first the current state:
-       # Note that read-only subvolumes cannot be moved into different directories, but for the
-       # "current" state we also want a writable copy, so start with that:
-       $class->btrfs_cmd(['property', 'set', "$tmppath/$diskname\@$snapshot", 'ro', 'false']);
-       PVE::Tools::renameat2(
-           -1,
-           "$tmppath/$diskname\@$snapshot",
-           -1,
-           $destination,
-           &PVE::Tools::RENAME_NOREPLACE,
-       ) or die "failed to move received snapshot '$tmppath/$diskname\@$snapshot'"
-           . " into place at '$destination' - $!\n";
-
-       # Now recreate the actual snapshot:
-       $class->btrfs_cmd([
-           'subvolume',
-           'snapshot',
-           '-r',
-           '--',
-           $destination,
-           "$destination\@$snapshot",
-       ]);
-
-       # Now go through the remaining snapshots (if any)
-       foreach my $snap (@snapshots) {
-           $class->btrfs_cmd(['property', 'set', "$tmppath/$diskname\@$snap", 'ro', 'false']);
-           PVE::Tools::renameat2(
-               -1,
-               "$tmppath/$diskname\@$snap",
-               -1,
-               "$destination\@$snap",
-               &PVE::Tools::RENAME_NOREPLACE,
-           ) or die "failed to move received snapshot '$tmppath/$diskname\@$snap'"
-               . " into place at '$destination\@$snap' - $!\n";
-           eval { $class->btrfs_cmd(['property', 'set', "$destination\@$snap", 'ro', 'true']) };
-           warn "failed to make $destination\@$snap read-only - $!\n" if $@;
-       }
-    };
-    my $err = $@;
-
-    eval {
-       # Cleanup all the received snapshots we did not move into place, so we can remove the temp
-       # directory.
-       if ($dh) {
-           $dh->rewind;
-           while (defined(my $entry = $dh->read)) {
-               next if $entry eq '.' || $entry eq '..';
-               eval { $class->btrfs_cmd(['subvolume', 'delete', '--', "$tmppath/$entry"]) };
-               warn $@ if $@;
-           }
-           $dh->close; undef $dh;
-       }
-       if (!rmdir($tmppath)) {
-           warn "failed to remove temporary directory '$tmppath' - $!\n"
-       }
-    };
-    warn $@ if $@;
-    if ($err) {
-       # clean up if the directory ended up being empty after an error
-       rmdir($tmppath);
-       die $err;
-    }
-
-    return "$storeid:$volname";
-}
-
-1
diff --git a/PVE/Storage/CIFSPlugin.pm b/PVE/Storage/CIFSPlugin.pm
deleted file mode 100644 (file)
index e03226d..0000000
+++ /dev/null
@@ -1,313 +0,0 @@
-package PVE::Storage::CIFSPlugin;
-
-use strict;
-use warnings;
-use Net::IP;
-use PVE::Tools qw(run_command);
-use PVE::ProcFSTools;
-use File::Path;
-use PVE::Storage::Plugin;
-use PVE::JSONSchema qw(get_standard_option);
-
-use base qw(PVE::Storage::Plugin);
-
-# CIFS helper functions
-
-sub cifs_is_mounted : prototype($$) {
-    my ($scfg, $mountdata) = @_;
-
-    my ($mountpoint, $server, $share) = $scfg->@{'path', 'server', 'share'};
-    my $subdir = $scfg->{subdir} // '';
-
-    $server = "[$server]" if Net::IP::ip_is_ipv6($server);
-    my $source = "//${server}/$share$subdir";
-    $mountdata = PVE::ProcFSTools::parse_proc_mounts() if !$mountdata;
-
-    return $mountpoint if grep {
-       $_->[2] =~ /^cifs/ &&
-       $_->[0] =~ m|^\Q$source\E/?$| &&
-       $_->[1] eq $mountpoint
-    } @$mountdata;
-    return undef;
-}
-
-sub cifs_cred_file_name {
-    my ($storeid) = @_;
-    return "/etc/pve/priv/storage/${storeid}.pw";
-}
-
-sub cifs_delete_credentials {
-    my ($storeid) = @_;
-
-    if (my $cred_file = get_cred_file($storeid)) {
-       unlink($cred_file) or warn "removing cifs credientials '$cred_file' failed: $!\n";
-    }
-}
-
-sub cifs_set_credentials {
-    my ($password, $storeid) = @_;
-
-    my $cred_file = cifs_cred_file_name($storeid);
-    mkdir "/etc/pve/priv/storage";
-
-    PVE::Tools::file_set_contents($cred_file, "password=$password\n");
-
-    return $cred_file;
-}
-
-sub get_cred_file {
-    my ($storeid) = @_;
-
-    my $cred_file = cifs_cred_file_name($storeid);
-
-    if (-e $cred_file) {
-       return $cred_file;
-    }
-    return undef;
-}
-
-sub cifs_mount : prototype($$$$$) {
-    my ($scfg, $storeid, $smbver, $user, $domain) = @_;
-
-    my ($mountpoint, $server, $share) = $scfg->@{'path', 'server', 'share'};
-    my $subdir = $scfg->{subdir} // '';
-
-    $server = "[$server]" if Net::IP::ip_is_ipv6($server);
-    my $source = "//${server}/$share$subdir";
-
-    my $cmd = ['/bin/mount', '-t', 'cifs', $source, $mountpoint, '-o', 'soft', '-o'];
-
-    if (my $cred_file = get_cred_file($storeid)) {
-       push @$cmd, "username=$user", '-o', "credentials=$cred_file";
-       push @$cmd, '-o', "domain=$domain" if defined($domain);
-    } else {
-       push @$cmd, 'guest,username=guest';
-    }
-
-    push @$cmd, '-o', defined($smbver) ? "vers=$smbver" : "vers=default";
-
-    run_command($cmd, errmsg => "mount error");
-}
-
-# Configuration
-
-sub type {
-    return 'cifs';
-}
-
-sub plugindata {
-    return {
-       content => [ { images => 1, rootdir => 1, vztmpl => 1, iso => 1,
-                  backup => 1, snippets => 1}, { images => 1 }],
-       format => [ { raw => 1, qcow2 => 1, vmdk => 1 } , 'raw' ],
-    };
-}
-
-sub properties {
-    return {
-       share => {
-           description => "CIFS share.",
-           type => 'string',
-       },
-       password => {
-           description => "Password for accessing the share/datastore.",
-           type => 'string',
-           maxLength => 256,
-       },
-       domain => {
-           description => "CIFS domain.",
-           type => 'string',
-           optional => 1,
-           maxLength => 256,
-       },
-       smbversion => {
-           description => "SMB protocol version. 'default' if not set, negotiates the highest SMB2+"
-               ." version supported by both the client and server.",
-           type => 'string',
-           default => 'default',
-           enum => ['default', '2.0', '2.1', '3', '3.0', '3.11'],
-           optional => 1,
-       },
-    };
-}
-
-sub options {
-    return {
-       path => { fixed => 1 },
-       'content-dirs' => { optional => 1 },
-       server => { fixed => 1 },
-       share => { fixed => 1 },
-       subdir => { optional => 1 },
-       nodes => { optional => 1 },
-       disable => { optional => 1 },
-       maxfiles => { optional => 1 },
-       'prune-backups' => { optional => 1 },
-       'max-protected-backups' => { optional => 1 },
-       content => { optional => 1 },
-       format => { optional => 1 },
-       username => { optional => 1 },
-       password => { optional => 1},
-       domain => { optional => 1},
-       smbversion => { optional => 1},
-       mkdir => { optional => 1 },
-       bwlimit => { optional => 1 },
-       preallocation => { optional => 1 },
-    };
-}
-
-
-sub check_config {
-    my ($class, $sectionId, $config, $create, $skipSchemaCheck) = @_;
-
-    $config->{path} = "/mnt/pve/$sectionId" if $create && !$config->{path};
-
-    return $class->SUPER::check_config($sectionId, $config, $create, $skipSchemaCheck);
-}
-
-# Storage implementation
-
-sub on_add_hook {
-    my ($class, $storeid, $scfg, %sensitive) = @_;
-
-    if (defined($sensitive{password})) {
-       cifs_set_credentials($sensitive{password}, $storeid);
-       if (!exists($scfg->{username})) {
-           warn "storage $storeid: ignoring password parameter, no user set\n";
-       }
-    } else {
-       cifs_delete_credentials($storeid);
-    }
-
-    return;
-}
-
-sub on_update_hook {
-    my ($class, $storeid, $scfg, %sensitive) = @_;
-
-    return if !exists($sensitive{password});
-
-    if (defined($sensitive{password})) {
-       cifs_set_credentials($sensitive{password}, $storeid);
-       if (!exists($scfg->{username})) {
-           warn "storage $storeid: ignoring password parameter, no user set\n";
-       }
-    } else {
-       cifs_delete_credentials($storeid);
-    }
-
-    return;
-}
-
-sub on_delete_hook {
-    my ($class, $storeid, $scfg) = @_;
-
-    cifs_delete_credentials($storeid);
-
-    return;
-}
-
-sub status {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    $cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts()
-       if !$cache->{mountdata};
-
-    return undef
-       if !cifs_is_mounted($scfg, $cache->{mountdata});
-
-    return $class->SUPER::status($storeid, $scfg, $cache);
-}
-
-sub activate_storage {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    $cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts()
-       if !$cache->{mountdata};
-
-    my $path = $scfg->{path};
-
-    if (!cifs_is_mounted($scfg, $cache->{mountdata})) {
-
-       mkpath $path if !(defined($scfg->{mkdir}) && !$scfg->{mkdir});
-
-       die "unable to activate storage '$storeid' - " .
-           "directory '$path' does not exist\n" if ! -d $path;
-
-       cifs_mount($scfg, $storeid, $scfg->{smbversion},
-           $scfg->{username}, $scfg->{domain});
-    }
-
-    $class->SUPER::activate_storage($storeid, $scfg, $cache);
-}
-
-sub deactivate_storage {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    $cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts()
-       if !$cache->{mountdata};
-
-    my $path = $scfg->{path};
-
-    if (cifs_is_mounted($scfg, $cache->{mountdata})) {
-       my $cmd = ['/bin/umount', $path];
-       run_command($cmd, errmsg => 'umount error');
-    }
-}
-
-sub check_connection {
-    my ($class, $storeid, $scfg) = @_;
-
-    my $servicename = '//'.$scfg->{server}.'/'.$scfg->{share};
-
-    my $cmd = ['/usr/bin/smbclient', $servicename, '-d', '0'];
-
-    if (defined($scfg->{smbversion}) && $scfg->{smbversion} ne 'default') {
-       # max-protocol version, so basically only relevant for smb2 vs smb3
-       push @$cmd, '-m', "smb" . int($scfg->{smbversion});
-    }
-
-    if (my $cred_file = get_cred_file($storeid)) {
-       push @$cmd, '-U', $scfg->{username}, '-A', $cred_file;
-       push @$cmd, '-W', $scfg->{domain} if defined($scfg->{domain});
-    } else {
-       push @$cmd, '-U', 'Guest','-N';
-    }
-    push @$cmd, '-c', 'echo 1 0';
-
-    my $out_str;
-    my $out = sub { $out_str .= shift };
-
-    eval { run_command($cmd, timeout => 10, outfunc => $out, errfunc => sub {}) };
-
-    if (my $err = $@) {
-       die "$out_str\n" if defined($out_str) &&
-           ($out_str =~ m/NT_STATUS_(ACCESS_DENIED|LOGON_FAILURE)/);
-       return 0;
-    }
-
-    return 1;
-}
-
-# FIXME remove on the next APIAGE reset.
-# Deprecated, use get_volume_attribute instead.
-sub get_volume_notes {
-    my $class = shift;
-    PVE::Storage::DirPlugin::get_volume_notes($class, @_);
-}
-
-# FIXME remove on the next APIAGE reset.
-# Deprecated, use update_volume_attribute instead.
-sub update_volume_notes {
-    my $class = shift;
-    PVE::Storage::DirPlugin::update_volume_notes($class, @_);
-}
-
-sub get_volume_attribute {
-    return PVE::Storage::DirPlugin::get_volume_attribute(@_);
-}
-
-sub update_volume_attribute {
-    return PVE::Storage::DirPlugin::update_volume_attribute(@_);
-}
-
-1;
diff --git a/PVE/Storage/CephFSPlugin.pm b/PVE/Storage/CephFSPlugin.pm
deleted file mode 100644 (file)
index db0c2f6..0000000
+++ /dev/null
@@ -1,262 +0,0 @@
-package PVE::Storage::CephFSPlugin;
-
-use strict;
-use warnings;
-
-use IO::File;
-use Net::IP;
-use File::Path;
-
-use PVE::CephConfig;
-use PVE::JSONSchema qw(get_standard_option);
-use PVE::ProcFSTools;
-use PVE::Storage::Plugin;
-use PVE::Systemd;
-use PVE::Tools qw(run_command file_set_contents);
-
-use base qw(PVE::Storage::Plugin);
-
-sub cephfs_is_mounted {
-    my ($scfg, $storeid, $mountdata) = @_;
-
-    my $cmd_option = PVE::CephConfig::ceph_connect_option($scfg, $storeid);
-    my $configfile = $cmd_option->{ceph_conf};
-
-    my $subdir = $scfg->{subdir} // '/';
-    my $mountpoint = $scfg->{path};
-
-    $mountdata = PVE::ProcFSTools::parse_proc_mounts() if !$mountdata;
-    return $mountpoint if grep {
-       $_->[2] =~ m#^ceph|fuse\.ceph-fuse# &&
-       $_->[0] =~ m#\Q:$subdir\E$|^ceph-fuse$# &&
-       $_->[1] eq $mountpoint
-    } @$mountdata;
-
-    warn "A filesystem is already mounted on $mountpoint\n"
-       if grep { $_->[1] eq $mountpoint } @$mountdata;
-
-    return undef;
-}
-
-# FIXME: remove once it's possible to specify _netdev for fuse.ceph mounts
-sub systemd_netmount {
-    my ($where, $type, $what, $opts) = @_;
-
-# don't do default deps, systemd v241 generator produces ordering deps on both
-# local-fs(-pre) and remote-fs(-pre) targets if we use the required _netdev
-# option. Over three corners this gets us an ordering cycle on shutdown, which
-# may make shutdown hang if the random cycle breaking hits the "wrong" unit to
-# delete.
-    my $unit =  <<"EOF";
-[Unit]
-Description=${where}
-DefaultDependencies=no
-Requires=system.slice
-Wants=network-online.target
-Before=umount.target remote-fs.target
-After=systemd-journald.socket system.slice network.target -.mount remote-fs-pre.target network-online.target
-Conflicts=umount.target
-
-[Mount]
-Where=${where}
-What=${what}
-Type=${type}
-Options=${opts}
-EOF
-
-    my $unit_fn = PVE::Systemd::escape_unit($where, 1) . ".mount";
-    my $unit_path = "/run/systemd/system/$unit_fn";
-    my $daemon_needs_reload = -e $unit_path;
-
-    file_set_contents($unit_path, $unit);
-
-    run_command(['systemctl', 'daemon-reload'], errmsg => "daemon-reload error")
-       if $daemon_needs_reload;
-    run_command(['systemctl', 'start', $unit_fn], errmsg => "mount error");
-
-}
-
-sub cephfs_mount {
-    my ($scfg, $storeid) = @_;
-
-    my $mountpoint = $scfg->{path};
-    my $subdir = $scfg->{subdir} // '/';
-
-    my $cmd_option = PVE::CephConfig::ceph_connect_option($scfg, $storeid);
-    my $configfile = $cmd_option->{ceph_conf};
-    my $secretfile = $cmd_option->{keyring};
-    my $server = $cmd_option->{mon_host} // PVE::CephConfig::get_monaddr_list($configfile);
-    my $type = 'ceph';
-    my $fs_name = $scfg->{'fs-name'};
-
-    my @opts = ();
-    if ($scfg->{fuse}) {
-       $type = 'fuse.ceph';
-       push @opts, "ceph.id=$cmd_option->{userid}";
-       push @opts, "ceph.keyfile=$secretfile" if defined($secretfile);
-       push @opts, "ceph.conf=$configfile" if defined($configfile);
-       push @opts, "ceph.client_fs=$fs_name" if defined($fs_name);
-    } else {
-       push @opts, "name=$cmd_option->{userid}";
-       push @opts, "secretfile=$secretfile" if defined($secretfile);
-       push @opts, "conf=$configfile" if defined($configfile);
-       push @opts, "fs=$fs_name" if defined($fs_name);
-    }
-
-    push @opts, $scfg->{options} if $scfg->{options};
-
-    systemd_netmount($mountpoint, $type, "$server:$subdir", join(',', @opts));
-}
-
-# Configuration
-
-sub type {
-    return 'cephfs';
-}
-
-sub plugindata {
-    return {
-       content => [ { vztmpl => 1, iso => 1, backup => 1, snippets => 1},
-                    { backup => 1 }],
-    };
-}
-
-sub properties {
-    return {
-       fuse => {
-           description => "Mount CephFS through FUSE.",
-           type => 'boolean',
-       },
-       'fs-name' => {
-           description => "The Ceph filesystem name.",
-           type => 'string', format => 'pve-configid',
-       },
-    };
-}
-
-sub options {
-    return {
-       path => { fixed => 1 },
-       'content-dirs' => { optional => 1 },
-       monhost => { optional => 1},
-       nodes => { optional => 1 },
-       subdir => { optional => 1 },
-       disable => { optional => 1 },
-       options => { optional => 1 },
-       username => { optional => 1 },
-       content => { optional => 1 },
-       format => { optional => 1 },
-       mkdir => { optional => 1 },
-       fuse => { optional => 1 },
-       bwlimit => { optional => 1 },
-       maxfiles => { optional => 1 },
-       keyring => { optional => 1 },
-       'prune-backups' => { optional => 1 },
-       'max-protected-backups' => { optional => 1 },
-       'fs-name' => { optional => 1 },
-    };
-}
-
-sub check_config {
-    my ($class, $sectionId, $config, $create, $skipSchemaCheck) = @_;
-
-    $config->{path} = "/mnt/pve/$sectionId" if $create && !$config->{path};
-
-    return $class->SUPER::check_config($sectionId, $config, $create, $skipSchemaCheck);
-}
-
-# Storage implementation
-
-sub on_add_hook {
-    my ($class, $storeid, $scfg, %param) = @_;
-
-    PVE::CephConfig::ceph_create_keyfile($scfg->{type}, $storeid, $param{keyring});
-
-    return;
-}
-
-sub on_update_hook {
-    my ($class, $storeid, $scfg, %param) = @_;
-
-    if (exists($param{keyring})) {
-       if (defined($param{keyring})) {
-           PVE::CephConfig::ceph_create_keyfile($scfg->{type}, $storeid, $param{keyring});
-       } else {
-           PVE::CephConfig::ceph_remove_keyfile($scfg->{type}, $storeid);
-       }
-    }
-
-    return;
-}
-
-sub on_delete_hook {
-    my ($class, $storeid, $scfg) = @_;
-    PVE::CephConfig::ceph_remove_keyfile($scfg->{type}, $storeid);
-    return;
-}
-
-sub status {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    $cache->{mountdata} //= PVE::ProcFSTools::parse_proc_mounts();
-
-    return undef if !cephfs_is_mounted($scfg, $storeid, $cache->{mountdata});
-
-    return $class->SUPER::status($storeid, $scfg, $cache);
-}
-
-sub activate_storage {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    $cache->{mountdata} //= PVE::ProcFSTools::parse_proc_mounts();
-
-    # NOTE: mkpath may hang if storage is mounted but not reachable
-    if (!cephfs_is_mounted($scfg, $storeid, $cache->{mountdata})) {
-       my $path = $scfg->{path};
-
-       mkpath $path if !(defined($scfg->{mkdir}) && !$scfg->{mkdir});
-
-       die "unable to activate storage '$storeid' - " .
-           "directory '$path' does not exist\n" if ! -d $path;
-
-       cephfs_mount($scfg, $storeid);
-    }
-
-    $class->SUPER::activate_storage($storeid, $scfg, $cache);
-}
-
-sub deactivate_storage {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    $cache->{mountdata} //= PVE::ProcFSTools::parse_proc_mounts();
-
-    my $path = $scfg->{path};
-
-    if (cephfs_is_mounted($scfg, $storeid, $cache->{mountdata})) {
-       run_command(['/bin/umount', $path], errmsg => 'umount error');
-    }
-}
-
-# FIXME remove on the next APIAGE reset.
-# Deprecated, use get_volume_attribute instead.
-sub get_volume_notes {
-    my $class = shift;
-    PVE::Storage::DirPlugin::get_volume_notes($class, @_);
-}
-
-# FIXME remove on the next APIAGE reset.
-# Deprecated, use update_volume_attribute instead.
-sub update_volume_notes {
-    my $class = shift;
-    PVE::Storage::DirPlugin::update_volume_notes($class, @_);
-}
-
-sub get_volume_attribute {
-    return PVE::Storage::DirPlugin::get_volume_attribute(@_);
-}
-
-sub update_volume_attribute {
-    return PVE::Storage::DirPlugin::update_volume_attribute(@_);
-}
-
-1;
diff --git a/PVE/Storage/DirPlugin.pm b/PVE/Storage/DirPlugin.pm
deleted file mode 100644 (file)
index 9e305f5..0000000
+++ /dev/null
@@ -1,239 +0,0 @@
-package PVE::Storage::DirPlugin;
-
-use strict;
-use warnings;
-
-use Cwd;
-use Encode qw(decode encode);
-use File::Path;
-use IO::File;
-use POSIX;
-
-use PVE::Storage::Plugin;
-use PVE::JSONSchema qw(get_standard_option);
-
-use base qw(PVE::Storage::Plugin);
-
-# Configuration
-
-sub type {
-    return 'dir';
-}
-
-sub plugindata {
-    return {
-       content => [ { images => 1, rootdir => 1, vztmpl => 1, iso => 1, backup => 1, snippets => 1, none => 1 },
-                    { images => 1,  rootdir => 1 }],
-       format => [ { raw => 1, qcow2 => 1, vmdk => 1, subvol => 1 } , 'raw' ],
-    };
-}
-
-sub properties {
-    return {
-       path => {
-           description => "File system path.",
-           type => 'string', format => 'pve-storage-path',
-       },
-       mkdir => {
-           description => "Create the directory if it doesn't exist.",
-           type => 'boolean',
-           default => 'yes',
-       },
-       is_mountpoint => {
-           description =>
-               "Assume the given path is an externally managed mountpoint " .
-               "and consider the storage offline if it is not mounted. ".
-               "Using a boolean (yes/no) value serves as a shortcut to using the target path in this field.",
-           type => 'string',
-           default => 'no',
-       },
-       bwlimit => get_standard_option('bwlimit'),
-    };
-}
-
-sub options {
-    return {
-       path => { fixed => 1 },
-       'content-dirs' => { optional => 1 },
-       nodes => { optional => 1 },
-       shared => { optional => 1 },
-       disable => { optional => 1 },
-       maxfiles => { optional => 1 },
-       'prune-backups' => { optional => 1 },
-       'max-protected-backups' => { optional => 1 },
-       content => { optional => 1 },
-       format => { optional => 1 },
-       mkdir => { optional => 1 },
-       is_mountpoint => { optional => 1 },
-       bwlimit => { optional => 1 },
-       preallocation => { optional => 1 },
-   };
-}
-
-# Storage implementation
-#
-
-# NOTE: should ProcFSTools::is_mounted accept an optional cache like this?
-sub path_is_mounted {
-    my ($mountpoint, $mountdata) = @_;
-
-    $mountpoint = Cwd::realpath($mountpoint); # symlinks
-    return 0 if !defined($mountpoint); # path does not exist
-
-    $mountdata = PVE::ProcFSTools::parse_proc_mounts() if !$mountdata;
-    return 1 if grep { $_->[1] eq $mountpoint } @$mountdata;
-    return undef;
-}
-
-sub parse_is_mountpoint {
-    my ($scfg) = @_;
-    my $is_mp = $scfg->{is_mountpoint};
-    return undef if !defined $is_mp;
-    if (defined(my $bool = PVE::JSONSchema::parse_boolean($is_mp))) {
-       return $bool ? $scfg->{path} : undef;
-    }
-    return $is_mp; # contains a path
-}
-
-# FIXME move into 'get_volume_attribute' when removing 'get_volume_notes'
-my $get_volume_notes_impl = sub {
-    my ($class, $scfg, $storeid, $volname, $timeout) = @_;
-
-    my ($vtype) = $class->parse_volname($volname);
-    return if $vtype ne 'backup';
-
-    my $path = $class->filesystem_path($scfg, $volname);
-    $path .= $class->SUPER::NOTES_EXT;
-
-    if (-f $path) {
-       my $data = PVE::Tools::file_get_contents($path);
-       return eval { decode('UTF-8', $data, 1) } // $data;
-    }
-
-    return '';
-};
-
-# FIXME remove on the next APIAGE reset.
-# Deprecated, use get_volume_attribute instead.
-sub get_volume_notes {
-    my ($class, $scfg, $storeid, $volname, $timeout) = @_;
-    return $get_volume_notes_impl->($class, $scfg, $storeid, $volname, $timeout);
-}
-
-# FIXME move into 'update_volume_attribute' when removing 'update_volume_notes'
-my $update_volume_notes_impl = sub {
-    my ($class, $scfg, $storeid, $volname, $notes, $timeout) = @_;
-
-    my ($vtype) = $class->parse_volname($volname);
-    die "only backups can have notes\n" if $vtype ne 'backup';
-
-    my $path = $class->filesystem_path($scfg, $volname);
-    $path .= $class->SUPER::NOTES_EXT;
-
-    if (defined($notes) && $notes ne '') {
-       my $encoded = encode('UTF-8', $notes);
-       PVE::Tools::file_set_contents($path, $encoded);
-    } else {
-       unlink $path or $! == ENOENT or die "could not delete notes - $!\n";
-    }
-    return;
-};
-
-# FIXME remove on the next APIAGE reset.
-# Deprecated, use update_volume_attribute instead.
-sub update_volume_notes {
-    my ($class, $scfg, $storeid, $volname, $notes, $timeout) = @_;
-    return $update_volume_notes_impl->($class, $scfg, $storeid, $volname, $notes, $timeout);
-}
-
-sub get_volume_attribute {
-    my ($class, $scfg, $storeid, $volname, $attribute) = @_;
-
-    if ($attribute eq 'notes') {
-       return $get_volume_notes_impl->($class, $scfg, $storeid, $volname);
-    }
-
-    my ($vtype) = $class->parse_volname($volname);
-    return if $vtype ne 'backup';
-
-    if ($attribute eq 'protected') {
-       my $path = $class->filesystem_path($scfg, $volname);
-       return -e PVE::Storage::protection_file_path($path) ? 1 : 0;
-    }
-
-    return;
-}
-
-sub update_volume_attribute {
-    my ($class, $scfg, $storeid, $volname, $attribute, $value) = @_;
-
-    if ($attribute eq 'notes') {
-       return $update_volume_notes_impl->($class, $scfg, $storeid, $volname, $value);
-    }
-
-    my ($vtype) = $class->parse_volname($volname);
-    die "only backups support attribute '$attribute'\n" if $vtype ne 'backup';
-
-    if ($attribute eq 'protected') {
-       my $path = $class->filesystem_path($scfg, $volname);
-       my $protection_path = PVE::Storage::protection_file_path($path);
-
-       return if !((-e $protection_path) xor $value); # protection status already correct
-
-       if ($value) {
-           my $fh = IO::File->new($protection_path, O_CREAT, 0644)
-               or die "unable to create protection file '$protection_path' - $!\n";
-           close($fh);
-       } else {
-           unlink $protection_path or $! == ENOENT
-               or die "could not delete protection file '$protection_path' - $!\n";
-       }
-
-       return;
-    }
-
-    die "attribute '$attribute' is not supported for storage type '$scfg->{type}'\n";
-}
-
-sub status {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    if (defined(my $mp = parse_is_mountpoint($scfg))) {
-       $cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts()
-           if !$cache->{mountdata};
-
-       return undef if !path_is_mounted($mp, $cache->{mountdata});
-    }
-
-    return $class->SUPER::status($storeid, $scfg, $cache);
-}
-
-
-sub activate_storage {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    my $path = $scfg->{path};
-    if (!defined($scfg->{mkdir}) || $scfg->{mkdir}) {
-       mkpath $path;
-    }
-
-    my $mp = parse_is_mountpoint($scfg);
-    if (defined($mp) && !path_is_mounted($mp, $cache->{mountdata})) {
-       die "unable to activate storage '$storeid' - " .
-           "directory is expected to be a mount point but is not mounted: '$mp'\n";
-    }
-
-    $class->SUPER::activate_storage($storeid, $scfg, $cache);
-}
-
-sub check_config {
-    my ($self, $sectionId, $config, $create, $skipSchemaCheck) = @_;
-    my $opts = PVE::SectionConfig::check_config($self, $sectionId, $config, $create, $skipSchemaCheck);
-    return $opts if !$create;
-    if ($opts->{path} !~ m@^/[-/a-zA-Z0-9_.]+$@) {
-       die "illegal path for directory storage: $opts->{path}\n";
-    }
-    return $opts;
-}
-
-1;
diff --git a/PVE/Storage/GlusterfsPlugin.pm b/PVE/Storage/GlusterfsPlugin.pm
deleted file mode 100644 (file)
index ad386d2..0000000
+++ /dev/null
@@ -1,354 +0,0 @@
-package PVE::Storage::GlusterfsPlugin;
-
-use strict;
-use warnings;
-use IO::File;
-use File::Path;
-use PVE::Tools qw(run_command);
-use PVE::ProcFSTools;
-use PVE::Network;
-use PVE::Storage::Plugin;
-use PVE::JSONSchema qw(get_standard_option);
-
-use base qw(PVE::Storage::Plugin);
-
-# Glusterfs helper functions
-
-my $server_test_results = {};
-
-my $get_active_server = sub {
-    my ($scfg, $return_default_if_offline) = @_;
-
-    my $defaultserver = $scfg->{server} ? $scfg->{server} : 'localhost';
-
-    if ($return_default_if_offline && !defined($scfg->{server2})) {
-       # avoid delays (there is no backup server anyways)
-       return $defaultserver;
-    }
-
-    my $serverlist = [ $defaultserver ];
-    push @$serverlist, $scfg->{server2} if $scfg->{server2};
-
-    my $ctime = time();
-    foreach my $server (@$serverlist) {
-       my $stat = $server_test_results->{$server};
-       return $server if $stat && $stat->{active} && (($ctime - $stat->{time}) <= 2); 
-    }
-
-    foreach my $server (@$serverlist) {
-       my $status = 0;
-
-       if ($server && $server ne 'localhost' && $server ne '127.0.0.1' && $server ne '::1') {
-           # ping the gluster daemon default port (24007) as heuristic
-           $status = PVE::Network::tcp_ping($server, 24007, 2);
-
-       } else {
-
-           my $parser = sub {
-               my $line = shift;
-
-               if ($line =~ m/Status: Started$/) {
-                   $status = 1;
-               }
-           };
-
-           my $cmd = ['/usr/sbin/gluster', 'volume', 'info', $scfg->{volume}];
-
-           run_command($cmd, errmsg => "glusterfs error", errfunc => sub {}, outfunc => $parser);
-       }
-
-       $server_test_results->{$server} = { time => time(), active => $status };
-       return $server if $status;
-    }
-
-    return $defaultserver if $return_default_if_offline;
-
-    return undef;
-};
-
-sub glusterfs_is_mounted {
-    my ($volume, $mountpoint, $mountdata) = @_;
-
-    $mountdata = PVE::ProcFSTools::parse_proc_mounts() if !$mountdata;
-
-    return $mountpoint if grep {
-       $_->[2] eq 'fuse.glusterfs' &&
-       $_->[0] =~ /^\S+:\Q$volume\E$/ &&
-       $_->[1] eq $mountpoint
-    } @$mountdata;
-    return undef;
-}
-
-sub glusterfs_mount {
-    my ($server, $volume, $mountpoint) = @_;
-
-    my $source = "$server:$volume";
-
-    my $cmd = ['/bin/mount', '-t', 'glusterfs', $source, $mountpoint];
-
-    run_command($cmd, errmsg => "mount error");
-}
-
-# Configuration
-
-sub type {
-    return 'glusterfs';
-}
-
-sub plugindata {
-    return {
-       content => [ { images => 1, vztmpl => 1, iso => 1, backup => 1, snippets => 1},
-                    { images => 1 }],
-       format => [ { raw => 1, qcow2 => 1, vmdk => 1 } , 'raw' ],
-    };
-}
-
-sub properties {
-    return {
-       volume => {
-           description => "Glusterfs Volume.",
-           type => 'string',
-       },
-       server2 => {
-           description => "Backup volfile server IP or DNS name.",
-           type => 'string', format => 'pve-storage-server',
-           requires => 'server',
-       },
-       transport => {
-           description => "Gluster transport: tcp or rdma",
-           type => 'string',
-           enum => ['tcp', 'rdma', 'unix'],
-       },
-    };
-}
-
-sub options {
-    return {
-       path => { fixed => 1 },
-       server => { optional => 1 },
-       server2 => { optional => 1 },
-       volume => { fixed => 1 },
-       transport => { optional => 1 },
-       nodes => { optional => 1 },
-       disable => { optional => 1 },
-       maxfiles => { optional => 1 },
-       'prune-backups' => { optional => 1 },
-       'max-protected-backups' => { optional => 1 },
-       content => { optional => 1 },
-       format => { optional => 1 },
-       mkdir => { optional => 1 },
-       bwlimit => { optional => 1 },
-       preallocation => { optional => 1 },
-    };
-}
-
-
-sub check_config {
-    my ($class, $sectionId, $config, $create, $skipSchemaCheck) = @_;
-
-    $config->{path} = "/mnt/pve/$sectionId" if $create && !$config->{path};
-
-    return $class->SUPER::check_config($sectionId, $config, $create, $skipSchemaCheck);
-}
-
-# Storage implementation
-
-sub parse_name_dir {
-    my $name = shift;
-
-    if ($name =~ m!^((base-)?[^/\s]+\.(raw|qcow2|vmdk))$!) {
-        return ($1, $3, $2);
-    }
-
-    die "unable to parse volume filename '$name'\n";
-}
-
-sub path {
-    my ($class, $scfg, $volname, $storeid, $snapname) = @_;
-
-    my ($vtype, $name, $vmid, undef, undef, $isBase, $format) =
-       $class->parse_volname($volname);
-
-    # Note: qcow2/qed has internal snapshot, so path is always
-    # the same (with or without snapshot => same file).
-    die "can't snapshot this image format\n" 
-       if defined($snapname) && $format !~ m/^(qcow2|qed)$/;
-
-    my $path = undef;
-    if ($vtype eq 'images') {
-
-       my $server = &$get_active_server($scfg, 1);
-       my $glustervolume = $scfg->{volume};
-       my $transport = $scfg->{transport};
-       my $protocol = "gluster";
-
-       if ($transport) {
-           $protocol = "gluster+$transport";
-       }
-
-       $path = "$protocol://$server/$glustervolume/images/$vmid/$name";
-
-    } else {
-       my $dir = $class->get_subdir($scfg, $vtype);
-       $path = "$dir/$name";
-    }
-
-    return wantarray ? ($path, $vmid, $vtype) : $path;
-}
-
-sub clone_image {
-    my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
-
-    die "storage definition has no path\n" if !$scfg->{path};
-
-    my ($vtype, $basename, $basevmid, undef, undef, $isBase, $format) =
-       $class->parse_volname($volname);
-
-    die "clone_image on wrong vtype '$vtype'\n" if $vtype ne 'images';
-
-    die "this storage type does not support clone_image on snapshot\n" if $snap;
-
-    die "this storage type does not support clone_image on subvolumes\n" if $format eq 'subvol';
-
-    die "clone_image only works on base images\n" if !$isBase;
-
-    my $imagedir = $class->get_subdir($scfg, 'images');
-    $imagedir .= "/$vmid";
-
-    mkpath $imagedir;
-
-    my $name = $class->find_free_diskname($storeid, $scfg, $vmid, "qcow2", 1);
-
-    warn "clone $volname: $vtype, $name, $vmid to $name (base=../$basevmid/$basename)\n";
-
-    my $path = "$imagedir/$name";
-
-    die "disk image '$path' already exists\n" if -e $path;
-
-    my $server = &$get_active_server($scfg, 1);
-    my $glustervolume = $scfg->{volume};
-    my $volumepath = "gluster://$server/$glustervolume/images/$vmid/$name";
-
-    my $cmd = ['/usr/bin/qemu-img', 'create', '-b', "../$basevmid/$basename",
-              '-F', $format, '-f', 'qcow2', $volumepath];
-
-    run_command($cmd, errmsg => "unable to create image");
-
-    return "$basevmid/$basename/$vmid/$name";
-}
-
-sub alloc_image {
-    my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
-
-    my $imagedir = $class->get_subdir($scfg, 'images');
-    $imagedir .= "/$vmid";
-
-    mkpath $imagedir;
-
-    $name = $class->find_free_diskname($storeid, $scfg, $vmid, $fmt, 1) if !$name;
-
-    my (undef, $tmpfmt) = parse_name_dir($name);
-
-    die "illegal name '$name' - wrong extension for format ('$tmpfmt != '$fmt')\n"
-        if $tmpfmt ne $fmt;
-
-    my $path = "$imagedir/$name";
-
-    die "disk image '$path' already exists\n" if -e $path;
-
-    my $server = &$get_active_server($scfg, 1);
-    my $glustervolume = $scfg->{volume};
-    my $volumepath = "gluster://$server/$glustervolume/images/$vmid/$name";
-
-    my $cmd = ['/usr/bin/qemu-img', 'create'];
-
-    my $prealloc_opt = PVE::Storage::Plugin::preallocation_cmd_option($scfg, $fmt);
-    push @$cmd, '-o', $prealloc_opt if defined($prealloc_opt);
-
-    push @$cmd, '-f', $fmt, $volumepath, "${size}K";
-
-    eval { run_command($cmd, errmsg => "unable to create image"); };
-    if ($@) {
-       unlink $path;
-       rmdir $imagedir;
-       die "$@";
-    }
-
-    return "$vmid/$name";
-}
-
-sub status {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    $cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts()
-       if !$cache->{mountdata};
-
-    my $path = $scfg->{path};
-
-    my $volume = $scfg->{volume};
-
-    return undef if !glusterfs_is_mounted($volume, $path, $cache->{mountdata});
-
-    return $class->SUPER::status($storeid, $scfg, $cache);
-}
-
-sub activate_storage {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    $cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts()
-       if !$cache->{mountdata};
-
-    my $path = $scfg->{path};
-    my $volume = $scfg->{volume};
-
-    if (!glusterfs_is_mounted($volume, $path, $cache->{mountdata})) {
-       
-       mkpath $path if !(defined($scfg->{mkdir}) && !$scfg->{mkdir});
-
-       die "unable to activate storage '$storeid' - " .
-           "directory '$path' does not exist\n" if ! -d $path;
-
-       my $server = &$get_active_server($scfg, 1);
-
-       glusterfs_mount($server, $volume, $path);
-    }
-
-    $class->SUPER::activate_storage($storeid, $scfg, $cache);
-}
-
-sub deactivate_storage {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    $cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts()
-       if !$cache->{mountdata};
-
-    my $path = $scfg->{path};
-    my $volume = $scfg->{volume};
-
-    if (glusterfs_is_mounted($volume, $path, $cache->{mountdata})) {
-       my $cmd = ['/bin/umount', $path];
-       run_command($cmd, errmsg => 'umount error');
-    }
-}
-
-sub activate_volume {
-    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
-
-    # do nothing by default
-}
-
-sub deactivate_volume {
-    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
-
-    # do nothing by default
-}
-
-sub check_connection {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    my $server = &$get_active_server($scfg);
-
-    return defined($server) ? 1 : 0;
-}
-
-1;
diff --git a/PVE/Storage/ISCSIDirectPlugin.pm b/PVE/Storage/ISCSIDirectPlugin.pm
deleted file mode 100644 (file)
index eb329d4..0000000
+++ /dev/null
@@ -1,255 +0,0 @@
-package PVE::Storage::ISCSIDirectPlugin;
-
-use strict;
-use warnings;
-use IO::File;
-use HTTP::Request;
-use LWP::UserAgent;
-use PVE::Tools qw(run_command file_read_firstline trim dir_glob_regex dir_glob_foreach);
-use PVE::Storage::Plugin;
-use PVE::JSONSchema qw(get_standard_option);
-
-use base qw(PVE::Storage::Plugin);
-
-sub iscsi_ls {
-    my ($scfg, $storeid) = @_;
-
-    my $portal = $scfg->{portal};
-    my $cmd = ['/usr/bin/iscsi-ls', '-s', 'iscsi://'.$portal ];
-    my $list = {};
-    my %unittobytes = (
-       "k"  => 1024,
-       "M" => 1024*1024,
-       "G" => 1024*1024*1024,
-       "T"   => 1024*1024*1024*1024
-    );
-    eval {
-
-           run_command($cmd, errmsg => "iscsi error", errfunc => sub {}, outfunc => sub {
-               my $line = shift;
-               $line = trim($line);
-               if( $line =~ /Lun:(\d+)\s+([A-Za-z0-9\-\_\.\:]*)\s+\(Size:([0-9\.]*)(k|M|G|T)\)/ ) {
-                   my $image = "lun".$1;
-                   my $size = $3;
-                   my $unit = $4;
-                               
-                   $list->{$storeid}->{$image} = {
-                       name => $image,
-                       size => $size * $unittobytes{$unit},
-                       format => 'raw',
-                   };
-               }
-           });
-    };
-
-    my $err = $@;
-    die $err if $err && $err !~ m/TESTUNITREADY failed with SENSE KEY/ ;
-
-    return $list;
-
-}
-
-# Configuration
-
-sub type {
-    return 'iscsidirect';
-}
-
-sub plugindata {
-    return {
-       content => [ {images => 1, none => 1}, { images => 1 }],
-       select_existing => 1,
-    };
-}
-
-sub options {
-    return {
-        portal => { fixed => 1 },
-        target => { fixed => 1 },
-        nodes => { optional => 1},
-        disable => { optional => 1},
-        content => { optional => 1},
-        bwlimit => { optional => 1 },
-    };
-}
-
-# Storage implementation
-
-sub parse_volname {
-    my ($class, $volname) = @_;
-
-
-    if ($volname =~ m/^lun(\d+)$/) {
-       return ('images', $1, undef, undef, undef, undef, 'raw');
-    }
-
-    die "unable to parse iscsi volume name '$volname'\n";
-
-}
-
-sub path {
-    my ($class, $scfg, $volname, $storeid, $snapname) = @_;
-
-    die "volume snapshot is not possible on iscsi device"
-       if defined($snapname);
-
-    my ($vtype, $lun, $vmid) = $class->parse_volname($volname);
-
-    my $target = $scfg->{target};
-    my $portal = $scfg->{portal};
-
-    my $path = "iscsi://$portal/$target/$lun";
-
-    return ($path, $vmid, $vtype);
-}
-
-sub create_base {
-    my ($class, $storeid, $scfg, $volname) = @_;
-
-    die "can't create base images in iscsi storage\n";
-}
-
-sub clone_image {
-    my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
-
-    die "can't clone images in iscsi storage\n";
-}
-
-sub alloc_image {
-    my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
-
-    die "can't allocate space in iscsi storage\n";
-}
-
-sub free_image {
-    my ($class, $storeid, $scfg, $volname, $isBase) = @_;
-
-    die "can't free space in iscsi storage\n";
-}
-
-
-sub list_images {
-    my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
-
-    my $res = [];
-
-    $cache->{directiscsi} = iscsi_ls($scfg,$storeid) if !$cache->{directiscsi};
-
-    # we have no owner for iscsi devices
-
-    my $target = $scfg->{target};
-
-    if (my $dat = $cache->{directiscsi}->{$storeid}) {
-
-        foreach my $volname (keys %$dat) {
-
-            my $volid = "$storeid:$volname";
-
-            if ($vollist) {
-                my $found = grep { $_ eq $volid } @$vollist;
-                next if !$found;
-            } else {
-                # we have no owner for iscsi devices
-                next if defined($vmid);
-            }
-
-            my $info = $dat->{$volname};
-            $info->{volid} = $volid;
-
-            push @$res, $info;
-        }
-    }
-
-    return $res;
-}
-
-
-sub status {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    my $total = 0;
-    my $free = 0;
-    my $used = 0;
-    my $active = 1;
-    return ($total,$free,$used,$active);
-
-    return undef;
-}
-
-sub activate_storage {
-    my ($class, $storeid, $scfg, $cache) = @_;
-    return 1;
-}
-
-sub deactivate_storage {
-    my ($class, $storeid, $scfg, $cache) = @_;
-    return 1;
-}
-
-sub activate_volume {
-    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
-
-    die "volume snapshot is not possible on iscsi device" if $snapname;
-
-    return 1;
-}
-
-sub deactivate_volume {
-    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
-
-    die "volume snapshot is not possible on iscsi device" if $snapname;
-
-    return 1;
-}
-
-sub volume_size_info {
-    my ($class, $scfg, $storeid, $volname, $timeout) = @_;
-
-    my $vollist = iscsi_ls($scfg,$storeid);
-    my $info = $vollist->{$storeid}->{$volname};
-
-    return wantarray ? ($info->{size}, 'raw', 0, undef) : $info->{size};
-}
-
-sub volume_resize {
-    my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
-    die "volume resize is not possible on iscsi device";
-}
-
-sub volume_snapshot {
-    my ($class, $scfg, $storeid, $volname, $snap) = @_;
-    die "volume snapshot is not possible on iscsi device";
-}
-
-sub volume_snapshot_rollback {
-    my ($class, $scfg, $storeid, $volname, $snap) = @_;
-    die "volume snapshot rollback is not possible on iscsi device";
-}
-
-sub volume_snapshot_delete {
-    my ($class, $scfg, $storeid, $volname, $snap) = @_;
-    die "volume snapshot delete is not possible on iscsi device";
-}
-
-sub volume_has_feature {
-    my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
-    
-    my $features = {
-       copy => { current => 1},
-    };
-
-    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
-       $class->parse_volname($volname);
-
-    my $key = undef;
-    if($snapname){
-       $key = 'snap';
-    }else{
-       $key =  $isBase ? 'base' : 'current';
-    }
-    return 1 if $features->{$feature}->{$key};
-
-    return undef;
-}
-
-1;
diff --git a/PVE/Storage/ISCSIPlugin.pm b/PVE/Storage/ISCSIPlugin.pm
deleted file mode 100644 (file)
index a79fcb0..0000000
+++ /dev/null
@@ -1,437 +0,0 @@
-package PVE::Storage::ISCSIPlugin;
-
-use strict;
-use warnings;
-
-use File::stat;
-use IO::Dir;
-use IO::File;
-
-use PVE::JSONSchema qw(get_standard_option);
-use PVE::Storage::Plugin;
-use PVE::Tools qw(run_command file_read_firstline trim dir_glob_regex dir_glob_foreach $IPV4RE $IPV6RE);
-
-use base qw(PVE::Storage::Plugin);
-
-# iscsi helper function
-
-my $ISCSIADM = '/usr/bin/iscsiadm';
-$ISCSIADM = undef if ! -X $ISCSIADM;
-
-sub check_iscsi_support {
-    my $noerr = shift;
-
-    if (!$ISCSIADM) {
-       my $msg = "no iscsi support - please install open-iscsi";
-       if ($noerr) {
-           warn "warning: $msg\n";
-           return 0;
-       }
-
-       die "error: $msg\n";
-    }
-
-    return 1;
-}
-
-sub iscsi_session_list {
-
-    check_iscsi_support ();
-
-    my $cmd = [$ISCSIADM, '--mode', 'session'];
-
-    my $res = {};
-
-    eval {
-       run_command($cmd, errmsg => 'iscsi session scan failed', outfunc => sub {
-           my $line = shift;
-
-           if ($line =~ m/^tcp:\s+\[(\S+)\]\s+\S+\s+(\S+)(\s+\S+)?\s*$/) {
-               my ($session, $target) = ($1, $2);
-               # there can be several sessions per target (multipath)
-               push @{$res->{$target}}, $session;
-           }
-       });
-    };
-    if (my $err = $@) {
-       die $err if $err !~ m/: No active sessions.$/i;
-    }
-
-    return $res;
-}
-
-sub iscsi_test_portal {
-    my ($portal) = @_;
-
-    my ($server, $port) = PVE::Tools::parse_host_and_port($portal);
-    return 0 if !$server;
-    return PVE::Network::tcp_ping($server, $port || 3260, 2);
-}
-
-sub iscsi_discovery {
-    my ($portal) = @_;
-
-    check_iscsi_support ();
-
-    my $res = {};
-    return $res if !iscsi_test_portal($portal); # fixme: raise exception here?
-
-    my $cmd = [$ISCSIADM, '--mode', 'discovery', '--type', 'sendtargets', '--portal', $portal];
-    run_command($cmd, outfunc => sub {
-       my $line = shift;
-
-       if ($line =~ m/^((?:$IPV4RE|\[$IPV6RE\]):\d+)\,\S+\s+(\S+)\s*$/) {
-           my $portal = $1;
-           my $target = $2;
-           # one target can have more than one portal (multipath).
-           push @{$res->{$target}}, $portal;
-       }
-    });
-
-    return $res;
-}
-
-sub iscsi_login {
-    my ($target, $portal_in) = @_;
-
-    check_iscsi_support();
-
-    eval { iscsi_discovery($portal_in); };
-    warn $@ if $@;
-
-    run_command([$ISCSIADM, '--mode', 'node', '--targetname',  $target, '--login']);
-}
-
-sub iscsi_logout {
-    my ($target, $portal) = @_;
-
-    check_iscsi_support();
-
-    run_command([$ISCSIADM, '--mode', 'node', '--targetname', $target, '--logout']);
-}
-
-my $rescan_filename = "/var/run/pve-iscsi-rescan.lock";
-
-sub iscsi_session_rescan {
-    my $session_list = shift;
-
-    check_iscsi_support();
-
-    my $rstat = stat($rescan_filename);
-
-    if (!$rstat) {
-       if (my $fh = IO::File->new($rescan_filename, "a")) {
-           utime undef, undef, $fh;
-           close($fh);
-       }
-    } else {
-       my $atime = $rstat->atime;
-       my $tdiff = time() - $atime;
-       # avoid frequent rescans
-       return if !($tdiff < 0 || $tdiff > 10);
-       utime undef, undef, $rescan_filename;
-    }
-
-    foreach my $session (@$session_list) {
-       my $cmd = [$ISCSIADM, '--mode', 'session', '--sid', $session, '--rescan'];
-       eval { run_command($cmd, outfunc => sub {}); };
-       warn $@ if $@;
-    }
-}
-
-sub load_stable_scsi_paths {
-
-    my $stable_paths = {};
-
-    my $stabledir = "/dev/disk/by-id";
-
-    if (my $dh = IO::Dir->new($stabledir)) {
-       foreach my $tmp (sort $dh->read) {
-           # exclude filenames with part in name (same disk but partitions)
-           # use only filenames with scsi(with multipath i have the same device
-          # with dm-uuid-mpath , dm-name and scsi in name)
-           if($tmp !~ m/-part\d+$/ && ($tmp =~ m/^scsi-/ || $tmp =~ m/^dm-uuid-mpath-/)) {
-                 my $path = "$stabledir/$tmp";
-                 my $bdevdest = readlink($path);
-                if ($bdevdest && $bdevdest =~ m|^../../([^/]+)|) {
-                    $stable_paths->{$1}=$tmp;
-                }
-          }
-       }
-       $dh->close;
-    }
-    return $stable_paths;
-}
-
-sub iscsi_device_list {
-
-    my $res = {};
-
-    my $dirname = '/sys/class/iscsi_session';
-
-    my $stable_paths = load_stable_scsi_paths();
-
-    dir_glob_foreach($dirname, 'session(\d+)', sub {
-       my ($ent, $session) = @_;
-
-       my $target = file_read_firstline("$dirname/$ent/targetname");
-       return if !$target;
-
-       my (undef, $host) = dir_glob_regex("$dirname/$ent/device", 'target(\d+):.*');
-       return if !defined($host);
-
-       dir_glob_foreach("/sys/bus/scsi/devices", "$host:" . '(\d+):(\d+):(\d+)', sub {
-           my ($tmp, $channel, $id, $lun) = @_;
-
-           my $type = file_read_firstline("/sys/bus/scsi/devices/$tmp/type");
-           return if !defined($type) || $type ne '0'; # list disks only
-
-           my $bdev;
-           if (-d "/sys/bus/scsi/devices/$tmp/block") { # newer kernels
-               (undef, $bdev) = dir_glob_regex("/sys/bus/scsi/devices/$tmp/block/", '([A-Za-z]\S*)');
-           } else {
-               (undef, $bdev) = dir_glob_regex("/sys/bus/scsi/devices/$tmp", 'block:(\S+)');
-           }
-           return if !$bdev;
-
-           #check multipath
-           if (-d "/sys/block/$bdev/holders") {
-               my $multipathdev = dir_glob_regex("/sys/block/$bdev/holders", '[A-Za-z]\S*');
-               $bdev = $multipathdev if $multipathdev;
-           }
-
-           my $blockdev = $stable_paths->{$bdev};
-           return if !$blockdev;
-
-           my $size = file_read_firstline("/sys/block/$bdev/size");
-           return if !$size;
-
-           my $volid = "$channel.$id.$lun.$blockdev";
-
-           $res->{$target}->{$volid} = {
-               'format' => 'raw',
-               'size' => int($size * 512),
-               'vmid' => 0, # not assigned to any vm
-               'channel' => int($channel),
-               'id' => int($id),
-               'lun' => int($lun),
-           };
-
-           #print "TEST: $target $session $host,$bus,$tg,$lun $blockdev\n";
-       });
-
-    });
-
-    return $res;
-}
-
-# Configuration
-
-sub type {
-    return 'iscsi';
-}
-
-sub plugindata {
-    return {
-       content => [ {images => 1, none => 1}, { images => 1 }],
-       select_existing => 1,
-    };
-}
-
-sub properties {
-    return {
-       target => {
-           description => "iSCSI target.",
-           type => 'string',
-       },
-       portal => {
-           description => "iSCSI portal (IP or DNS name with optional port).",
-           type => 'string', format => 'pve-storage-portal-dns',
-       },
-    };
-}
-
-sub options {
-    return {
-        portal => { fixed => 1 },
-        target => { fixed => 1 },
-        nodes => { optional => 1},
-       disable => { optional => 1},
-       content => { optional => 1},
-       bwlimit => { optional => 1 },
-    };
-}
-
-# Storage implementation
-
-sub parse_volname {
-    my ($class, $volname) = @_;
-
-    if ($volname =~ m!^\d+\.\d+\.\d+\.(\S+)$!) {
-       return ('images', $1, undef, undef, undef, undef, 'raw');
-    }
-
-    die "unable to parse iscsi volume name '$volname'\n";
-}
-
-sub filesystem_path {
-    my ($class, $scfg, $volname, $snapname) = @_;
-
-    die "snapshot is not possible on iscsi storage\n" if defined($snapname);
-
-    my ($vtype, $name, $vmid) = $class->parse_volname($volname);
-
-    my $path = "/dev/disk/by-id/$name";
-
-    return wantarray ? ($path, $vmid, $vtype) : $path;
-}
-
-sub create_base {
-    my ($class, $storeid, $scfg, $volname) = @_;
-
-    die "can't create base images in iscsi storage\n";
-}
-
-sub clone_image {
-    my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
-
-    die "can't clone images in iscsi storage\n";
-}
-
-sub alloc_image {
-    my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
-
-    die "can't allocate space in iscsi storage\n";
-}
-
-sub free_image {
-    my ($class, $storeid, $scfg, $volname, $isBase) = @_;
-
-    die "can't free space in iscsi storage\n";
-}
-
-# list all luns regardless of set content_types, since we need it for
-# listing in the gui and we can only have images anyway
-sub list_volumes {
-    my ($class, $storeid, $scfg, $vmid, $content_types) = @_;
-
-    my $res = $class->list_images($storeid, $scfg, $vmid);
-
-    for my $item (@$res) {
-       $item->{content} = 'images'; # we only have images
-    }
-
-    return $res;
-}
-
-sub list_images {
-    my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
-
-    my $res = [];
-
-    $cache->{iscsi_devices} = iscsi_device_list() if !$cache->{iscsi_devices};
-
-    # we have no owner for iscsi devices
-
-    my $target = $scfg->{target};
-
-    if (my $dat = $cache->{iscsi_devices}->{$target}) {
-
-       foreach my $volname (keys %$dat) {
-
-           my $volid = "$storeid:$volname";
-
-           if ($vollist) {
-               my $found = grep { $_ eq $volid } @$vollist;
-               next if !$found;
-           } else {
-               # we have no owner for iscsi devices
-               next if defined($vmid);
-           }
-
-           my $info = $dat->{$volname};
-           $info->{volid} = $volid;
-
-           push @$res, $info;
-       }
-    }
-
-    return $res;
-}
-
-sub iscsi_session {
-    my ($cache, $target) = @_;
-    $cache->{iscsi_sessions} = iscsi_session_list() if !$cache->{iscsi_sessions};
-    return $cache->{iscsi_sessions}->{$target};
-}
-
-sub status {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    my $session = iscsi_session($cache, $scfg->{target});
-    my $active = defined($session) ? 1 : 0;
-
-    return (0, 0, 0, $active);
-}
-
-sub activate_storage {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    return if !check_iscsi_support(1);
-
-    my $session = iscsi_session($cache, $scfg->{target});
-
-    if (!defined ($session)) {
-       eval { iscsi_login($scfg->{target}, $scfg->{portal}); };
-       warn $@ if $@;
-    } else {
-       # make sure we get all devices
-       iscsi_session_rescan($session);
-    }
-}
-
-sub deactivate_storage {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    return if !check_iscsi_support(1);
-
-    if (defined(iscsi_session($cache, $scfg->{target}))) {
-       iscsi_logout($scfg->{target}, $scfg->{portal});
-    }
-}
-
-sub check_connection {
-    my ($class, $storeid, $scfg) = @_;
-
-    my $portal = $scfg->{portal};
-    return iscsi_test_portal($portal);
-}
-
-sub volume_resize {
-    my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
-    die "volume resize is not possible on iscsi device";
-}
-
-sub volume_has_feature {
-    my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
-
-    my $features = {
-       copy => { current => 1},
-    };
-
-    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
-       $class->parse_volname($volname);
-
-    my $key = undef;
-    if($snapname){
-       $key = 'snap';
-    }else{
-       $key =  $isBase ? 'base' : 'current';
-    }
-    return 1 if $features->{$feature}->{$key};
-
-    return undef;
-}
-
-
-1;
diff --git a/PVE/Storage/LVMPlugin.pm b/PVE/Storage/LVMPlugin.pm
deleted file mode 100644 (file)
index a706e0c..0000000
+++ /dev/null
@@ -1,741 +0,0 @@
-package PVE::Storage::LVMPlugin;
-
-use strict;
-use warnings;
-
-use IO::File;
-
-use PVE::Tools qw(run_command trim);
-use PVE::Storage::Plugin;
-use PVE::JSONSchema qw(get_standard_option);
-
-use base qw(PVE::Storage::Plugin);
-
-# lvm helper functions
-
-my $ignore_no_medium_warnings = sub {
-    my $line = shift;
-    # ignore those, most of the time they're from (virtual) IPMI/iKVM devices
-    # and just spam the log..
-    if ($line !~ /open failed: No medium found/) {
-       print STDERR "$line\n";
-    }
-};
-
-sub lvm_pv_info {
-    my ($device) = @_;
-
-    die "no device specified" if !$device;
-
-    my $has_label = 0;
-
-    my $cmd = ['/usr/bin/file', '-L', '-s', $device];
-    run_command($cmd, outfunc => sub {
-       my $line = shift;
-       $has_label = 1 if $line =~ m/LVM2/;
-    });
-
-    return undef if !$has_label;
-
-    $cmd = ['/sbin/pvs', '--separator', ':', '--noheadings', '--units', 'k',
-           '--unbuffered', '--nosuffix', '--options',
-           'pv_name,pv_size,vg_name,pv_uuid', $device];
-
-    my $pvinfo;
-    run_command($cmd, outfunc => sub {
-       my $line = shift;
-
-       $line = trim($line);
-
-       my ($pvname, $size, $vgname, $uuid) = split(':', $line);
-
-       die "found multiple pvs entries for device '$device'\n"
-           if $pvinfo;
-
-       $pvinfo = {
-           pvname => $pvname,
-           size => int($size),
-           vgname => $vgname,
-           uuid => $uuid,
-       };
-    });
-
-    return $pvinfo;
-}
-
-sub clear_first_sector {
-    my ($dev) = shift;
-
-    if (my $fh = IO::File->new($dev, "w")) {
-       my $buf = 0 x 512;
-       syswrite $fh, $buf;
-       $fh->close();
-    }
-}
-
-sub lvm_create_volume_group {
-    my ($device, $vgname, $shared) = @_;
-
-    my $res = lvm_pv_info($device);
-
-    if ($res->{vgname}) {
-       return if $res->{vgname} eq $vgname; # already created
-       die "device '$device' is already used by volume group '$res->{vgname}'\n";
-    }
-
-    clear_first_sector($device); # else pvcreate fails
-
-    # we use --metadatasize 250k, which reseults in "pe_start = 512"
-    # so pe_start is aligned on a 128k boundary (advantage for SSDs)
-    my $cmd = ['/sbin/pvcreate', '--metadatasize', '250k', $device];
-
-    run_command($cmd, errmsg => "pvcreate '$device' error");
-
-    $cmd = ['/sbin/vgcreate', $vgname, $device];
-    # push @$cmd, '-c', 'y' if $shared; # we do not use this yet
-
-    run_command($cmd, errmsg => "vgcreate $vgname $device error", errfunc => $ignore_no_medium_warnings, outfunc => $ignore_no_medium_warnings);
-}
-
-sub lvm_destroy_volume_group {
-    my ($vgname) = @_;
-
-    run_command(
-       ['vgremove', '-y', $vgname],
-       errmsg => "unable to remove volume group $vgname",
-       errfunc => $ignore_no_medium_warnings,
-       outfunc => $ignore_no_medium_warnings,
-    );
-}
-
-sub lvm_vgs {
-    my ($includepvs) = @_;
-
-    my $cmd = ['/sbin/vgs', '--separator', ':', '--noheadings', '--units', 'b',
-              '--unbuffered', '--nosuffix', '--options'];
-
-    my $cols = [qw(vg_name vg_size vg_free lv_count)];
-
-    if ($includepvs) {
-       push @$cols, qw(pv_name pv_size pv_free);
-    }
-
-    push @$cmd, join(',', @$cols);
-
-    my $vgs = {};
-    eval {
-       run_command($cmd, outfunc => sub {
-           my $line = shift;
-           $line = trim($line);
-
-           my ($name, $size, $free, $lvcount, $pvname, $pvsize, $pvfree) = split (':', $line);
-
-           $vgs->{$name} //= {
-               size => int ($size),
-               free => int ($free),
-               lvcount => int($lvcount)
-           };
-
-           if (defined($pvname) && defined($pvsize) && defined($pvfree)) {
-               push @{$vgs->{$name}->{pvs}}, {
-                   name => $pvname,
-                   size => int($pvsize),
-                   free => int($pvfree),
-               };
-           }
-       },
-       errfunc => $ignore_no_medium_warnings,
-       );
-    };
-    my $err = $@;
-
-    # just warn (vgs return error code 5 if clvmd does not run)
-    # but output is still OK (list without clustered VGs)
-    warn $err if $err;
-
-    return $vgs;
-}
-
-sub lvm_list_volumes {
-    my ($vgname) = @_;
-
-    my $option_list = 'vg_name,lv_name,lv_size,lv_attr,pool_lv,data_percent,metadata_percent,snap_percent,uuid,tags,metadata_size,time';
-
-    my $cmd = [
-       '/sbin/lvs', '--separator', ':', '--noheadings', '--units', 'b',
-       '--unbuffered', '--nosuffix',
-       '--config', 'report/time_format="%s"',
-       '--options', $option_list,
-    ];
-
-    push @$cmd, $vgname if $vgname;
-
-    my $lvs = {};
-    run_command($cmd, outfunc => sub {
-       my $line = shift;
-
-       $line = trim($line);
-
-       my ($vg_name, $lv_name, $lv_size, $lv_attr, $pool_lv, $data_percent, $meta_percent, $snap_percent, $uuid, $tags, $meta_size, $ctime) = split(':', $line);
-       return if !$vg_name;
-       return if !$lv_name;
-
-       my $lv_type = substr($lv_attr, 0, 1);
-
-       my $d = {
-           lv_size => int($lv_size),
-           lv_state => substr($lv_attr, 4, 1),
-           lv_type => $lv_type,
-       };
-       $d->{pool_lv} = $pool_lv if $pool_lv;
-       $d->{tags} = $tags if $tags;
-       $d->{ctime} = $ctime;
-
-       if ($lv_type eq 't') {
-           $data_percent ||= 0;
-           $meta_percent ||= 0;
-           $snap_percent ||= 0;
-           $d->{metadata_size} = int($meta_size);
-           $d->{metadata_used} = int(($meta_percent * $meta_size)/100);
-           $d->{used} = int(($data_percent * $lv_size)/100);
-       }
-       $lvs->{$vg_name}->{$lv_name} = $d;
-    },
-    errfunc => $ignore_no_medium_warnings,
-    );
-
-    return $lvs;
-}
-
-# Configuration
-
-sub type {
-    return 'lvm';
-}
-
-sub plugindata {
-    return {
-       content => [ {images => 1, rootdir => 1}, { images => 1 }],
-    };
-}
-
-sub properties {
-    return {
-       vgname => {
-           description => "Volume group name.",
-           type => 'string', format => 'pve-storage-vgname',
-       },
-       base => {
-           description => "Base volume. This volume is automatically activated.",
-           type => 'string', format => 'pve-volume-id',
-       },
-       saferemove => {
-           description => "Zero-out data when removing LVs.",
-           type => 'boolean',
-       },
-       saferemove_throughput => {
-           description => "Wipe throughput (cstream -t parameter value).",
-           type => 'string',
-       },
-       tagged_only => {
-           description => "Only use logical volumes tagged with 'pve-vm-ID'.",
-           type => 'boolean',
-       }
-    };
-}
-
-sub options {
-    return {
-       vgname => { fixed => 1 },
-       nodes => { optional => 1 },
-       shared => { optional => 1 },
-       disable => { optional => 1 },
-       saferemove => { optional => 1 },
-       saferemove_throughput => { optional => 1 },
-       content => { optional => 1 },
-       base => { fixed => 1, optional => 1 },
-       tagged_only => { optional => 1 },
-       bwlimit => { optional => 1 },
-    };
-}
-
-# Storage implementation
-
-sub on_add_hook {
-    my ($class, $storeid, $scfg, %param) = @_;
-
-    if (my $base = $scfg->{base}) {
-       my ($baseid, $volname) = PVE::Storage::parse_volume_id($base);
-
-       my $cfg = PVE::Storage::config();
-       my $basecfg = PVE::Storage::storage_config ($cfg, $baseid, 1);
-       die "base storage ID '$baseid' does not exist\n" if !$basecfg;
-
-       # we only support iscsi for now
-       die "unsupported base type '$basecfg->{type}'"
-           if $basecfg->{type} ne 'iscsi';
-
-       my $path = PVE::Storage::path($cfg, $base);
-
-       PVE::Storage::activate_storage($cfg, $baseid);
-
-       lvm_create_volume_group($path, $scfg->{vgname}, $scfg->{shared});
-    }
-
-    return;
-}
-
-sub parse_volname {
-    my ($class, $volname) = @_;
-
-    PVE::Storage::Plugin::parse_lvm_name($volname);
-
-    if ($volname =~ m/^(vm-(\d+)-\S+)$/) {
-       return ('images', $1, $2, undef, undef, undef, 'raw');
-    }
-
-    die "unable to parse lvm volume name '$volname'\n";
-}
-
-sub filesystem_path {
-    my ($class, $scfg, $volname, $snapname) = @_;
-
-    die "lvm snapshot is not implemented"if defined($snapname);
-
-    my ($vtype, $name, $vmid) = $class->parse_volname($volname);
-
-    my $vg = $scfg->{vgname};
-
-    my $path = "/dev/$vg/$name";
-
-    return wantarray ? ($path, $vmid, $vtype) : $path;
-}
-
-sub create_base {
-    my ($class, $storeid, $scfg, $volname) = @_;
-
-    die "can't create base images in lvm storage\n";
-}
-
-sub clone_image {
-    my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
-
-    die "can't clone images in lvm storage\n";
-}
-
-sub find_free_diskname {
-    my ($class, $storeid, $scfg, $vmid, $fmt, $add_fmt_suffix) = @_;
-
-    my $vg = $scfg->{vgname};
-
-    my $lvs = lvm_list_volumes($vg);
-
-    my $disk_list = [ keys %{$lvs->{$vg}} ];
-
-    return PVE::Storage::Plugin::get_next_vm_diskname($disk_list, $storeid, $vmid, undef, $scfg);
-}
-
-sub lvcreate {
-    my ($vg, $name, $size, $tags) = @_;
-
-    if ($size =~ m/\d$/) { # no unit is given
-       $size .= "k"; # default to kilobytes
-    }
-
-    my $cmd = ['/sbin/lvcreate', '-aly', '-Wy', '--yes', '--size', $size, '--name', $name];
-    for my $tag (@$tags) {
-       push @$cmd, '--addtag', $tag;
-    }
-    push @$cmd, $vg;
-
-    run_command($cmd, errmsg => "lvcreate '$vg/$name' error");
-}
-
-sub lvrename {
-    my ($vg, $oldname, $newname) = @_;
-
-    run_command(
-       ['/sbin/lvrename', $vg, $oldname, $newname],
-       errmsg => "lvrename '${vg}/${oldname}' to '${newname}' error",
-    );
-}
-
-sub alloc_image {
-    my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
-
-    die "unsupported format '$fmt'" if $fmt ne 'raw';
-
-    die "illegal name '$name' - should be 'vm-$vmid-*'\n"
-       if  $name && $name !~ m/^vm-$vmid-/;
-
-    my $vgs = lvm_vgs();
-
-    my $vg = $scfg->{vgname};
-
-    die "no such volume group '$vg'\n" if !defined ($vgs->{$vg});
-
-    my $free = int($vgs->{$vg}->{free});
-
-    die "not enough free space ($free < $size)\n" if $free < $size;
-
-    $name = $class->find_free_diskname($storeid, $scfg, $vmid)
-       if !$name;
-
-    lvcreate($vg, $name, $size, ["pve-vm-$vmid"]);
-
-    return $name;
-}
-
-sub free_image {
-    my ($class, $storeid, $scfg, $volname, $isBase) = @_;
-
-    my $vg = $scfg->{vgname};
-
-    # we need to zero out LVM data for security reasons
-    # and to allow thin provisioning
-
-    my $zero_out_worker = sub {
-       print "zero-out data on image $volname (/dev/$vg/del-$volname)\n";
-
-       # wipe throughput up to 10MB/s by default; may be overwritten with saferemove_throughput
-       my $throughput = '-10485760';
-       if ($scfg->{saferemove_throughput}) {
-               $throughput = $scfg->{saferemove_throughput};
-       }
-
-       my $cmd = [
-               '/usr/bin/cstream',
-               '-i', '/dev/zero',
-               '-o', "/dev/$vg/del-$volname",
-               '-T', '10',
-               '-v', '1',
-               '-b', '1048576',
-               '-t', "$throughput"
-       ];
-       eval { run_command($cmd, errmsg => "zero out finished (note: 'No space left on device' is ok here)"); };
-       warn $@ if $@;
-
-       $class->cluster_lock_storage($storeid, $scfg->{shared}, undef, sub {
-           my $cmd = ['/sbin/lvremove', '-f', "$vg/del-$volname"];
-           run_command($cmd, errmsg => "lvremove '$vg/del-$volname' error");
-       });
-       print "successfully removed volume $volname ($vg/del-$volname)\n";
-    };
-
-    my $cmd = ['/sbin/lvchange', '-aly', "$vg/$volname"];
-    run_command($cmd, errmsg => "can't activate LV '$vg/$volname' to zero-out its data");
-    $cmd = ['/sbin/lvchange', '--refresh', "$vg/$volname"];
-    run_command($cmd, errmsg => "can't refresh LV '$vg/$volname' to zero-out its data");
-
-    if ($scfg->{saferemove}) {
-       # avoid long running task, so we only rename here
-       $cmd = ['/sbin/lvrename', $vg, $volname, "del-$volname"];
-       run_command($cmd, errmsg => "lvrename '$vg/$volname' error");
-       return $zero_out_worker;
-    } else {
-       my $tmpvg = $scfg->{vgname};
-       $cmd = ['/sbin/lvremove', '-f', "$tmpvg/$volname"];
-       run_command($cmd, errmsg => "lvremove '$tmpvg/$volname' error");
-    }
-
-    return undef;
-}
-
-my $check_tags = sub {
-    my ($tags) = @_;
-
-    return defined($tags) && $tags =~ /(^|,)pve-vm-\d+(,|$)/;
-};
-
-sub list_images {
-    my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
-
-    my $vgname = $scfg->{vgname};
-
-    $cache->{lvs} = lvm_list_volumes() if !$cache->{lvs};
-
-    my $res = [];
-
-    if (my $dat = $cache->{lvs}->{$vgname}) {
-
-       foreach my $volname (keys %$dat) {
-
-           next if $volname !~ m/^vm-(\d+)-/;
-           my $owner = $1;
-
-           my $info = $dat->{$volname};
-
-           next if $scfg->{tagged_only} && !&$check_tags($info->{tags});
-
-           # Allow mirrored and RAID LVs
-           next if $info->{lv_type} !~ m/^[-mMrR]$/;
-
-           my $volid = "$storeid:$volname";
-
-           if ($vollist) {
-               my $found = grep { $_ eq $volid } @$vollist;
-               next if !$found;
-           } else {
-               next if defined($vmid) && ($owner ne $vmid);
-           }
-
-           push @$res, {
-               volid => $volid, format => 'raw', size => $info->{lv_size}, vmid => $owner,
-               ctime => $info->{ctime},
-           };
-       }
-    }
-
-    return $res;
-}
-
-sub status {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    $cache->{vgs} = lvm_vgs() if !$cache->{vgs};
-
-    my $vgname = $scfg->{vgname};
-
-     if (my $info = $cache->{vgs}->{$vgname}) {
-       return ($info->{size}, $info->{free}, $info->{size} - $info->{free}, 1);
-    }
-
-    return undef;
-}
-
-sub activate_storage {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    $cache->{vgs} = lvm_vgs() if !$cache->{vgs};
-
-    # In LVM2, vgscans take place automatically;
-    # this is just to be sure
-    if ($cache->{vgs} && !$cache->{vgscaned} &&
-       !$cache->{vgs}->{$scfg->{vgname}}) {
-       $cache->{vgscaned} = 1;
-       my $cmd = ['/sbin/vgscan', '--ignorelockingfailure', '--mknodes'];
-       eval { run_command($cmd, outfunc => sub {}); };
-       warn $@ if $@;
-    }
-
-    # we do not acticate any volumes here ('vgchange -aly')
-    # instead, volumes are activate individually later
-}
-
-sub deactivate_storage {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    my $cmd = ['/sbin/vgchange', '-aln', $scfg->{vgname}];
-    run_command($cmd, errmsg => "can't deactivate VG '$scfg->{vgname}'");
-}
-
-sub activate_volume {
-    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
-    #fix me lvmchange is not provided on
-    my $path = $class->path($scfg, $volname, $snapname);
-
-    my $lvm_activate_mode = 'ey';
-
-    my $cmd = ['/sbin/lvchange', "-a$lvm_activate_mode", $path];
-    run_command($cmd, errmsg => "can't activate LV '$path'");
-    $cmd = ['/sbin/lvchange', '--refresh', $path];
-    run_command($cmd, errmsg => "can't refresh LV '$path' for activation");
-}
-
-sub deactivate_volume {
-    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
-
-    my $path = $class->path($scfg, $volname, $snapname);
-    return if ! -b $path;
-
-    my $cmd = ['/sbin/lvchange', '-aln', $path];
-    run_command($cmd, errmsg => "can't deactivate LV '$path'");
-}
-
-sub volume_resize {
-    my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
-
-    $size = ($size/1024/1024) . "M";
-
-    my $path = $class->path($scfg, $volname);
-    my $cmd = ['/sbin/lvextend', '-L', $size, $path];
-
-    $class->cluster_lock_storage($storeid, $scfg->{shared}, undef, sub {
-       run_command($cmd, errmsg => "error resizing volume '$path'");
-    });
-
-    return 1;
-}
-
-sub volume_size_info {
-    my ($class, $scfg, $storeid, $volname, $timeout) = @_;
-    my $path = $class->filesystem_path($scfg, $volname);
-
-    my $cmd = ['/sbin/lvs', '--separator', ':', '--noheadings', '--units', 'b',
-              '--unbuffered', '--nosuffix', '--options', 'lv_size', $path];
-
-    my $size;
-    run_command($cmd, timeout => $timeout, errmsg => "can't get size of '$path'",
-       outfunc => sub {
-           $size = int(shift);
-    });
-    return wantarray ? ($size, 'raw', 0, undef) : $size;
-}
-
-sub volume_snapshot {
-    my ($class, $scfg, $storeid, $volname, $snap) = @_;
-
-    die "lvm snapshot is not implemented";
-}
-
-sub volume_snapshot_rollback {
-    my ($class, $scfg, $storeid, $volname, $snap) = @_;
-
-    die "lvm snapshot rollback is not implemented";
-}
-
-sub volume_snapshot_delete {
-    my ($class, $scfg, $storeid, $volname, $snap) = @_;
-
-    die "lvm snapshot delete is not implemented";
-}
-
-sub volume_has_feature {
-    my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
-
-    my $features = {
-       copy => { base => 1, current => 1},
-       rename => {current => 1},
-    };
-
-    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
-       $class->parse_volname($volname);
-
-    my $key = undef;
-    if($snapname){
-       $key = 'snap';
-    }else{
-       $key =  $isBase ? 'base' : 'current';
-    }
-    return 1 if $features->{$feature}->{$key};
-
-    return undef;
-}
-
-sub volume_export_formats {
-    my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
-    return () if defined($snapshot); # lvm-thin only
-    return volume_import_formats($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots);
-}
-
-sub volume_export {
-    my ($class, $scfg, $storeid, $fh, $volname, $format, $snapshot, $base_snapshot, $with_snapshots) = @_;
-    die "volume export format $format not available for $class\n"
-       if $format ne 'raw+size';
-    die "cannot export volumes together with their snapshots in $class\n"
-       if $with_snapshots;
-    die "cannot export a snapshot in $class\n" if defined($snapshot);
-    die "cannot export an incremental stream in $class\n" if defined($base_snapshot);
-    my $file = $class->path($scfg, $volname, $storeid);
-    my $size;
-    # should be faster than querying LVM, also checks for the device file's availability
-    run_command(['/sbin/blockdev', '--getsize64', $file], outfunc => sub {
-       my ($line) = @_;
-       die "unexpected output from /sbin/blockdev: $line\n" if $line !~ /^(\d+)$/;
-       $size = int($1);
-    });
-    PVE::Storage::Plugin::write_common_header($fh, $size);
-    run_command(['dd', "if=$file", "bs=64k"], output => '>&'.fileno($fh));
-}
-
-sub volume_import_formats {
-    my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
-    return () if $with_snapshots; # not supported
-    return () if defined($base_snapshot); # not supported
-    return ('raw+size');
-}
-
-sub volume_import {
-    my ($class, $scfg, $storeid, $fh, $volname, $format, $snapshot, $base_snapshot, $with_snapshots, $allow_rename) = @_;
-    die "volume import format $format not available for $class\n"
-       if $format ne 'raw+size';
-    die "cannot import volumes together with their snapshots in $class\n"
-       if $with_snapshots;
-    die "cannot import an incremental stream in $class\n" if defined($base_snapshot);
-
-    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $file_format) =
-       $class->parse_volname($volname);
-    die "cannot import format $format into a file of format $file_format\n"
-       if $file_format ne 'raw';
-
-    my $vg = $scfg->{vgname};
-    my $lvs = lvm_list_volumes($vg);
-    if ($lvs->{$vg}->{$volname}) {
-       die "volume $vg/$volname already exists\n" if !$allow_rename;
-       warn "volume $vg/$volname already exists - importing with a different name\n";
-       $name = undef;
-    }
-
-    my ($size) = PVE::Storage::Plugin::read_common_header($fh);
-    $size = int($size/1024);
-
-    eval {
-       my $allocname = $class->alloc_image($storeid, $scfg, $vmid, 'raw', $name, $size);
-       my $oldname = $volname;
-       $volname = $allocname;
-       if (defined($name) && $allocname ne $oldname) {
-           die "internal error: unexpected allocated name: '$allocname' != '$oldname'\n";
-       }
-       my $file = $class->path($scfg, $volname, $storeid)
-           or die "internal error: failed to get path to newly allocated volume $volname\n";
-
-       $class->volume_import_write($fh, $file);
-    };
-    if (my $err = $@) {
-       my $cleanup_worker = eval { $class->free_image($storeid, $scfg, $volname, 0) };
-       warn $@ if $@;
-
-       if ($cleanup_worker) {
-           my $rpcenv = PVE::RPCEnvironment::get();
-           my $authuser = $rpcenv->get_user();
-
-           $rpcenv->fork_worker('imgdel', undef, $authuser, $cleanup_worker);
-       }
-
-       die $err;
-    }
-
-    return "$storeid:$volname";
-}
-
-sub volume_import_write {
-    my ($class, $input_fh, $output_file) = @_;
-    run_command(['dd', "of=$output_file", 'bs=64k'],
-       input => '<&'.fileno($input_fh));
-}
-
-sub rename_volume {
-    my ($class, $scfg, $storeid, $source_volname, $target_vmid, $target_volname) = @_;
-
-    my (
-       undef,
-       $source_image,
-       $source_vmid,
-       $base_name,
-       $base_vmid,
-       undef,
-       $format
-    ) = $class->parse_volname($source_volname);
-    $target_volname = $class->find_free_diskname($storeid, $scfg, $target_vmid, $format)
-       if !$target_volname;
-
-    my $vg = $scfg->{vgname};
-    my $lvs = lvm_list_volumes($vg);
-    die "target volume '${target_volname}' already exists\n"
-       if ($lvs->{$vg}->{$target_volname});
-
-    lvrename($vg, $source_volname, $target_volname);
-    return "${storeid}:${target_volname}";
-}
-
-1;
diff --git a/PVE/Storage/LunCmd/Comstar.pm b/PVE/Storage/LunCmd/Comstar.pm
deleted file mode 100644 (file)
index 527e4ba..0000000
+++ /dev/null
@@ -1,115 +0,0 @@
-package PVE::Storage::LunCmd::Comstar;
-
-use strict;
-use warnings;
-
-use Digest::MD5 qw(md5_hex);
-use PVE::Tools qw(run_command file_read_firstline trim dir_glob_regex dir_glob_foreach);
-
-my @ssh_opts = ('-o', 'BatchMode=yes');
-my @ssh_cmd = ('/usr/bin/ssh', @ssh_opts);
-my $id_rsa_path = '/etc/pve/priv/zfs';
-
-my $get_lun_cmd_map = sub {
-    my ($method) = @_;
-
-    my $stmfadmcmd = "/usr/sbin/stmfadm";
-    my $sbdadmcmd = "/usr/sbin/sbdadm";
-
-    my $cmdmap = {
-        create_lu   => { cmd => $stmfadmcmd, method => 'create-lu' },
-        delete_lu   => { cmd => $stmfadmcmd, method => 'delete-lu' },
-        import_lu   => { cmd => $stmfadmcmd, method => 'import-lu' },
-        modify_lu   => { cmd => $stmfadmcmd, method => 'modify-lu' },
-        add_view    => { cmd => $stmfadmcmd, method => 'add-view' },
-        list_view   => { cmd => $stmfadmcmd, method => 'list-view' },
-        list_lu => { cmd => $sbdadmcmd, method => 'list-lu' },
-    };
-
-    die "unknown command '$method'" unless exists $cmdmap->{$method};
-
-    return $cmdmap->{$method};
-};
-
-sub get_base {
-    return '/dev/zvol/rdsk';
-}
-
-sub run_lun_command {
-    my ($scfg, $timeout, $method, @params) = @_;
-
-    my $msg = '';
-    my $luncmd;
-    my $target;
-    my $guid;
-    $timeout = 10 if !$timeout;
-
-    my $output = sub {
-    my $line = shift;
-    $msg .= "$line\n";
-    };
-
-    if ($method eq 'create_lu') {
-        my $wcd = 'false'; 
-        if ($scfg->{nowritecache}) {
-          $wcd = 'true';
-       }
-        my $prefix = '600144f';
-        my $digest = md5_hex($params[0]);
-        $digest =~ /(\w{7}(.*))/;
-        $guid = "$prefix$2";
-        @params = ('-p', "wcd=$wcd", '-p', "guid=$guid", @params);
-    } elsif ($method eq 'modify_lu') {
-        @params = ('-s', @params);
-    } elsif ($method eq 'list_view') {
-        @params = ('-l', @params);
-    } elsif ($method eq 'list_lu') {
-        $guid = $params[0];
-        @params = undef;
-    } elsif ($method eq 'add_view') {
-        if ($scfg->{comstar_tg}) {
-          unshift @params, $scfg->{comstar_tg};
-          unshift @params, '--target-group';
-       }
-        if ($scfg->{comstar_hg}) {
-          unshift @params, $scfg->{comstar_hg};
-          unshift @params, '--host-group';
-       }
-    }
-
-    my $cmdmap = $get_lun_cmd_map->($method);
-    $luncmd = $cmdmap->{cmd};
-    my $lunmethod = $cmdmap->{method};
-
-    $target = 'root@' . $scfg->{portal};
-
-    my $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, $luncmd, $lunmethod, @params];
-
-    run_command($cmd, outfunc => $output, timeout => $timeout);
-
-    if ($method eq 'list_view') {
-        my @lines = split /\n/, $msg;
-        $msg = undef;
-        foreach my $line (@lines) {
-            if ($line =~ /^\s*LUN\s*:\s*(\d+)$/) {
-                $msg = $1;
-                last;
-            }
-        }
-    } elsif ($method eq 'list_lu') {
-        my $object = $guid;
-        my @lines = split /\n/, $msg;
-        $msg = undef;
-        foreach my $line (@lines) {
-            if ($line =~ /(\w+)\s+\d+\s+$object$/) {
-                $msg = $1;
-                last;
-            }
-        }
-    } elsif ($method eq 'create_lu') {
-        $msg = $guid;
-    }
-
-    return $msg;
-}
-
diff --git a/PVE/Storage/LunCmd/Iet.pm b/PVE/Storage/LunCmd/Iet.pm
deleted file mode 100644 (file)
index 5b09b88..0000000
+++ /dev/null
@@ -1,478 +0,0 @@
-package PVE::Storage::LunCmd::Iet;
-
-# iscsi storage running Debian
-# 1) apt-get install iscsitarget iscsitarget-dkms
-# 2) Create target like (/etc/iet/ietd.conf):
-# Target iqn.2001-04.com.example:tank
-#   Alias           tank
-# 3) Activate daemon (/etc/default/iscsitarget)
-# ISCSITARGET_ENABLE=true
-# 4) service iscsitarget start
-#
-# On one of the proxmox nodes:
-# 1) Login as root
-# 2) ssh-copy-id <ip_of_iscsi_storage>
-
-use strict;
-use warnings;
-
-use PVE::Tools qw(run_command file_read_firstline trim dir_glob_regex dir_glob_foreach);
-
-sub get_base;
-
-# A logical unit can max have 16864 LUNs
-# http://manpages.ubuntu.com/manpages/precise/man5/ietd.conf.5.html
-my $MAX_LUNS = 16864;
-
-my $CONFIG_FILE = '/etc/iet/ietd.conf';
-my $DAEMON = '/usr/sbin/ietadm';
-my $SETTINGS = undef;
-my $CONFIG = undef;
-my $OLD_CONFIG = undef;
-
-my @ssh_opts = ('-o', 'BatchMode=yes');
-my @ssh_cmd = ('/usr/bin/ssh', @ssh_opts);
-my @scp_cmd = ('/usr/bin/scp', @ssh_opts);
-my $id_rsa_path = '/etc/pve/priv/zfs';
-my $ietadm = '/usr/sbin/ietadm';
-
-my $execute_command = sub {
-    my ($scfg, $exec, $timeout, $method, @params) = @_;
-
-    my $msg = '';
-    my $err = undef;
-    my $target;
-    my $cmd;
-    my $res = ();
-
-    $timeout = 10 if !$timeout;
-
-    my $output = sub {
-    my $line = shift;
-    $msg .= "$line\n";
-    };
-
-    my $errfunc = sub {
-    my $line = shift;
-    $err .= "$line";
-    };
-
-    if ($exec eq 'scp') {
-        $target = 'root@[' . $scfg->{portal} . ']';
-        $cmd = [@scp_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", '--', $method, "$target:$params[0]"];
-    } else {
-        $target = 'root@' . $scfg->{portal};
-        $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, '--', $method, @params];
-    }
-
-    eval {
-        run_command($cmd, outfunc => $output, errfunc => $errfunc, timeout => $timeout);
-    };
-    if ($@) {
-        $res = {
-            result => 0,
-            msg => $err,
-        }
-    } else {
-        $res = {
-            result => 1,
-            msg => $msg,
-        }
-    }
-
-    return $res;
-};
-
-my $read_config = sub {
-    my ($scfg, $timeout) = @_;
-
-    my $msg = '';
-    my $err = undef;
-    my $luncmd = 'cat';
-    my $target;
-    $timeout = 10 if !$timeout;
-
-    my $output = sub {
-        my $line = shift;
-        $msg .= "$line\n";
-    };
-
-    my $errfunc = sub {
-        my $line = shift;
-        $err .= "$line";
-    };
-
-    $target = 'root@' . $scfg->{portal};
-
-    my $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, $luncmd, $CONFIG_FILE];
-    eval {
-        run_command($cmd, outfunc => $output, errfunc => $errfunc, timeout => $timeout);
-    };
-    if ($@) {
-        die $err if ($err !~ /No such file or directory/);
-        die "No configuration found. Install iet on $scfg->{portal}" if $msg eq '';
-    }
-
-    return $msg;
-};
-
-my $get_config = sub {
-    my ($scfg) = @_;
-    my @conf = undef;
-
-    my $config = $read_config->($scfg, undef);
-    die "Missing config file" unless $config;
-
-    $OLD_CONFIG = $config;
-
-    return $config;
-};
-
-my $parser = sub {
-    my ($scfg) = @_;
-
-    my $line = 0;
-
-    my $base = get_base;
-    my $config = $get_config->($scfg);
-    my @cfgfile = split "\n", $config;
-
-    my $cfg_target = 0;
-    foreach (@cfgfile) {
-        $line++;
-        if ($_ =~ /^\s*Target\s*([\w\-\:\.]+)\s*$/) {
-            if ($1 eq $scfg->{target} && ! $cfg_target) {
-                # start colect info
-                die "$line: Parse error [$_]" if $SETTINGS;
-                $SETTINGS->{target} = $1;
-                $cfg_target = 1;
-            } elsif ($1 eq $scfg->{target} && $cfg_target) {
-                die "$line: Parse error [$_]";
-            } elsif ($cfg_target) {
-                $cfg_target = 0;
-                $CONFIG .= "$_\n";
-            } else {
-                $CONFIG .= "$_\n";
-            }
-        } else {
-            if ($cfg_target) {
-                $SETTINGS->{text} .= "$_\n";
-                next if ($_ =~ /^\s*#/ || ! $_);
-                my $option = $_;
-                if ($_ =~ /^(\w+)\s*#/) {
-                    $option = $1;
-                }
-                if ($option =~ /^\s*(\w+)\s+(\w+)\s*$/) {
-                    if ($1 eq 'Lun') {
-                        die "$line: Parse error [$_]";
-                    }
-                    $SETTINGS->{$1} = $2;
-                } elsif ($option =~ /^\s*(\w+)\s+(\d+)\s+([\w\-\/=,]+)\s*$/) {
-                    die "$line: Parse error [$option]" unless ($1 eq 'Lun');
-                    my $conf = undef;
-                    my $num = $2;
-                    my @lun = split ',', $3;
-                    die "$line: Parse error [$option]" unless (scalar(@lun) > 1);
-                    foreach (@lun) {
-                        my @lun_opt = split '=', $_;
-                        die "$line: Parse error [$option]" unless (scalar(@lun_opt) == 2);
-                        $conf->{$lun_opt[0]} = $lun_opt[1];
-                    }
-                    if ($conf->{Path} && $conf->{Path} =~ /^$base\/$scfg->{pool}\/([\w\-]+)$/) {
-                        $conf->{include} = 1;
-                    } else {
-                        $conf->{include} = 0;
-                    }
-                    $conf->{lun} = $num;
-                    push @{$SETTINGS->{luns}}, $conf;
-                } else {
-                    die "$line: Parse error [$option]";
-                }
-            } else {
-                $CONFIG .= "$_\n";
-            }
-        }
-    }
-    $CONFIG =~ s/^\s+|\s+$|"\s*//g;
-};
-
-my $update_config = sub {
-    my ($scfg) = @_;
-    my $file = "/tmp/config$$";
-    my $config = '';
-
-    while ((my $option, my $value) = each(%$SETTINGS)) {
-        next if ($option eq 'include' || $option eq 'luns' || $option eq 'Path' || $option eq 'text' || $option eq 'used');
-        if ($option eq 'target') {
-            $config = "\n\nTarget " . $SETTINGS->{target} . "\n" . $config;
-        } else {
-            $config .= "\t$option\t\t\t$value\n";
-        }
-    }
-    foreach my $lun (@{$SETTINGS->{luns}}) {
-        my $lun_opt = '';
-        while ((my $option, my $value) = each(%$lun)) {
-            next if ($option eq 'include' || $option eq 'lun' || $option eq 'Path');
-            if ($lun_opt eq '') {
-            $lun_opt = $option . '=' . $value;
-            } else {
-                $lun_opt .= ',' . $option . '=' . $value;
-            }
-        }
-        $config .= "\tLun $lun->{lun} Path=$lun->{Path},$lun_opt\n";
-    }
-    open(my $fh, '>', $file) or die "Could not open file '$file' $!";
-
-    print $fh $CONFIG;
-    print $fh $config;
-    close $fh;
-
-    my @params = ($CONFIG_FILE);
-    my $res = $execute_command->($scfg, 'scp', undef, $file, @params);
-    unlink $file;
-
-    die $res->{msg} unless $res->{result};
-};
-
-my $get_target_tid = sub {
-    my ($scfg) = @_;
-    my $proc = '/proc/net/iet/volume';
-    my $tid = undef;
-
-    my @params = ($proc);
-    my $res = $execute_command->($scfg, 'ssh', undef, 'cat', @params);
-    die $res->{msg} unless $res->{result};
-    my @cfg = split "\n", $res->{msg};
-
-    foreach (@cfg) {
-        if ($_ =~ /^\s*tid:(\d+)\s+name:([\w\-\:\.]+)\s*$/) {
-            if ($2 && $2 eq $scfg->{target}) {
-                $tid = $1;
-                last;
-            }
-        }
-    }
-
-    return $tid;
-};
-
-my $get_lu_name = sub {
-    my $used = ();
-    my $i;
-
-    if (! exists $SETTINGS->{used}) {
-        for ($i = 0; $i < $MAX_LUNS; $i++) {
-            $used->{$i} = 0;
-        }
-        foreach my $lun (@{$SETTINGS->{luns}}) {
-            $used->{$lun->{lun}} = 1;
-        }
-        $SETTINGS->{used} = $used;
-    }
-
-    $used = $SETTINGS->{used};
-    for ($i = 0; $i < $MAX_LUNS; $i++) {
-        last unless $used->{$i};
-    }
-    $SETTINGS->{used}->{$i} = 1;
-
-    return $i;
-};
-
-my $init_lu_name = sub {
-    my $used = ();
-
-    if (! exists($SETTINGS->{used})) {
-        for (my $i = 0; $i < $MAX_LUNS; $i++) {
-            $used->{$i} = 0;
-        }
-        $SETTINGS->{used} = $used;
-    }
-    foreach my $lun (@{$SETTINGS->{luns}}) {
-        $SETTINGS->{used}->{$lun->{lun}} = 1;
-    }
-};
-
-my $free_lu_name = sub {
-    my ($lu_name) = @_;
-    my $new;
-
-    foreach my $lun (@{$SETTINGS->{luns}}) {
-        if ($lun->{lun} != $lu_name) {
-            push @$new, $lun;
-        }
-    }
-
-    $SETTINGS->{luns} = $new;
-    $SETTINGS->{used}->{$lu_name} = 0;
-};
-
-my $make_lun = sub {
-    my ($scfg, $path) = @_;
-
-    die 'Maximum number of LUNs per target is 16384' if scalar @{$SETTINGS->{luns}} >= $MAX_LUNS;
-
-    my $lun = $get_lu_name->();
-    my $conf = {
-        lun => $lun,
-        Path => $path,
-        Type => 'blockio',
-        include => 1,
-    };
-    push @{$SETTINGS->{luns}}, $conf;
-
-    return $conf;
-};
-
-my $list_view = sub {
-    my ($scfg, $timeout, $method, @params) = @_;
-    my $lun = undef;
-
-    my $object = $params[0];
-    foreach my $lun (@{$SETTINGS->{luns}}) {
-        next unless $lun->{include} == 1;
-        if ($lun->{Path} =~ /^$object$/) {
-            return $lun->{lun} if (defined($lun->{lun}));
-            die "$lun->{Path}: Missing LUN";
-        }
-    }
-
-    return $lun;
-};
-
-my $list_lun = sub {
-    my ($scfg, $timeout, $method, @params) = @_;
-    my $name = undef;
-
-    my $object = $params[0];
-    foreach my $lun (@{$SETTINGS->{luns}}) {
-        next unless $lun->{include} == 1;
-        if ($lun->{Path} =~ /^$object$/) {
-            return $lun->{Path};
-        }
-    }
-
-    return $name;
-};
-
-my $create_lun = sub {
-    my ($scfg, $timeout, $method, @params) = @_;
-
-    if ($list_lun->($scfg, $timeout, $method, @params)) {
-        die "$params[0]: LUN exists";
-    }
-    my $lun = $params[0];
-    $lun = $make_lun->($scfg, $lun);
-    my $tid = $get_target_tid->($scfg);
-    $update_config->($scfg);
-
-    my $path = "Path=$lun->{Path},Type=$lun->{Type}";
-
-    @params = ('--op', 'new', "--tid=$tid", "--lun=$lun->{lun}", '--params', $path);
-    my $res = $execute_command->($scfg, 'ssh', $timeout, $ietadm, @params);
-    do {
-        $free_lu_name->($lun->{lun});
-        $update_config->($scfg);
-        die $res->{msg};
-    } unless $res->{result};
-
-    return $res->{msg};
-};
-
-my $delete_lun = sub {
-    my ($scfg, $timeout, $method, @params) = @_;
-    my $res = {msg => undef};
-
-    my $path = $params[0];
-    my $tid = $get_target_tid->($scfg);
-
-    foreach my $lun (@{$SETTINGS->{luns}}) {
-        if ($lun->{Path} eq $path) {
-            @params = ('--op', 'delete', "--tid=$tid", "--lun=$lun->{lun}");
-            $res = $execute_command->($scfg, 'ssh', $timeout, $ietadm, @params);
-            if ($res->{result}) {
-                $free_lu_name->($lun->{lun});
-                $update_config->($scfg);
-                last;
-            } else {
-                die $res->{msg};
-            }
-        }
-    }
-
-    return $res->{msg};
-};
-
-my $import_lun = sub {
-    my ($scfg, $timeout, $method, @params) = @_;
-
-    return $create_lun->($scfg, $timeout, $method, @params);
-};
-
-my $modify_lun = sub {
-    my ($scfg, $timeout, $method, @params) = @_;
-    my $lun;
-    my $res;
-
-    my $path = $params[1];
-    my $tid = $get_target_tid->($scfg);
-
-    foreach my $cfg (@{$SETTINGS->{luns}}) {
-        if ($cfg->{Path} eq $path) {
-            $lun = $cfg;
-            last;
-        }
-    }
-
-    @params = ('--op', 'delete', "--tid=$tid", "--lun=$lun->{lun}");
-    $res = $execute_command->($scfg, 'ssh', $timeout, $ietadm, @params);
-    die $res->{msg} unless $res->{result};
-
-    $path = "Path=$lun->{Path},Type=$lun->{Type}";
-    @params = ('--op', 'new', "--tid=$tid", "--lun=$lun->{lun}", '--params', $path);
-    $res = $execute_command->($scfg, 'ssh', $timeout, $ietadm, @params);
-    die $res->{msg} unless $res->{result};
-
-    return $res->{msg};
-};
-
-my $add_view = sub {
-    my ($scfg, $timeout, $method, @params) = @_;
-
-    return '';
-};
-
-my $get_lun_cmd_map = sub {
-    my ($method) = @_;
-
-    my $cmdmap = {
-        create_lu   =>  { cmd => $create_lun },
-        delete_lu   =>  { cmd => $delete_lun },
-        import_lu   =>  { cmd => $import_lun },
-        modify_lu   =>  { cmd => $modify_lun },
-        add_view    =>  { cmd => $add_view },
-        list_view   =>  { cmd => $list_view },
-        list_lu     =>  { cmd => $list_lun },
-    };
-
-    die "unknown command '$method'" unless exists $cmdmap->{$method};
-
-    return $cmdmap->{$method};
-};
-
-sub run_lun_command {
-    my ($scfg, $timeout, $method, @params) = @_;
-
-    $parser->($scfg) unless $SETTINGS;
-    my $cmdmap = $get_lun_cmd_map->($method);
-    my $msg = $cmdmap->{cmd}->($scfg, $timeout, $method, @params);
-
-    return $msg;
-}
-
-sub get_base {
-    return '/dev';
-}
-
-1;
-
diff --git a/PVE/Storage/LunCmd/Istgt.pm b/PVE/Storage/LunCmd/Istgt.pm
deleted file mode 100644 (file)
index 2f758f9..0000000
+++ /dev/null
@@ -1,601 +0,0 @@
-package PVE::Storage::LunCmd::Istgt;
-
-# TODO
-# Create initial target and LUN if target is missing ?
-# Create and use list of free LUNs
-
-use strict;
-use warnings;
-
-use PVE::Tools qw(run_command file_read_firstline trim dir_glob_regex dir_glob_foreach);
-
-my @CONFIG_FILES = (
-    '/usr/local/etc/istgt/istgt.conf',  # FreeBSD, FreeNAS
-    '/var/etc/iscsi/istgt.conf'         # NAS4Free
-);
-my @DAEMONS = (
-    '/usr/local/etc/rc.d/istgt',        # FreeBSD, FreeNAS
-    '/var/etc/rc.d/istgt'               # NAS4Free
-);
-
-# A logical unit can max have 63 LUNs
-# https://code.google.com/p/istgt/source/browse/src/istgt_lu.h#39
-my $MAX_LUNS = 64;
-
-my $CONFIG_FILE = undef;
-my $DAEMON = undef;
-my $SETTINGS = undef;
-my $CONFIG = undef;
-my $OLD_CONFIG = undef;
-
-my @ssh_opts = ('-o', 'BatchMode=yes');
-my @ssh_cmd = ('/usr/bin/ssh', @ssh_opts);
-my @scp_cmd = ('/usr/bin/scp', @ssh_opts);
-my $id_rsa_path = '/etc/pve/priv/zfs';
-
-#Current SIGHUP reload limitations (http://www.peach.ne.jp/archives/istgt/):
-#
-#    The parameters other than PG, IG, and LU are not reloaded by SIGHUP.
-#    LU connected by the initiator can't be reloaded by SIGHUP.
-#    PG and IG mapped to LU can't be deleted by SIGHUP.
-#    If you delete an active LU, all connections of the LU are closed by SIGHUP.
-#    Updating IG is not affected until the next login.
-#
-# FreeBSD
-# 1. Alt-F2 to change to native shell (zfsguru)
-# 2. pw mod user root -w yes (change password for root to root)
-# 3. vi /etc/ssh/sshd_config
-# 4. uncomment PermitRootLogin yes
-# 5. change PasswordAuthentication no to PasswordAuthentication yes
-# 5. /etc/rc.d/sshd restart
-# 6. On one of the proxmox nodes login as root and run: ssh-copy-id ip_freebsd_host
-# 7. vi /etc/ssh/sshd_config
-# 8. comment PermitRootLogin yes
-# 9. change PasswordAuthentication yes to PasswordAuthentication no
-# 10. /etc/rc.d/sshd restart
-# 11. Reset passwd -> pw mod user root -w no
-# 12. Alt-Ctrl-F1 to return to zfsguru shell (zfsguru)
-
-sub get_base;
-sub run_lun_command;
-
-my $read_config = sub {
-    my ($scfg, $timeout, $method) = @_;
-
-    my $msg = '';
-    my $err = undef;
-    my $luncmd = 'cat';
-    my $target;
-    $timeout = 10 if !$timeout;
-
-    my $output = sub {
-    my $line = shift;
-    $msg .= "$line\n";
-    };
-
-    my $errfunc = sub {
-    my $line = shift;
-    $err .= "$line";
-    };
-
-    $target = 'root@' . $scfg->{portal};
-
-    my $daemon = 0;
-    foreach my $config (@CONFIG_FILES) {
-        $err = undef;
-        my $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, $luncmd, $config];
-        eval {
-            run_command($cmd, outfunc => $output, errfunc => $errfunc, timeout => $timeout);
-        };
-        do {
-            $err = undef;
-            $DAEMON = $DAEMONS[$daemon];
-            $CONFIG_FILE = $config;
-            last;
-        } unless $@;
-        $daemon++;
-    }
-    die $err if ($err && $err !~ /No such file or directory/);
-    die "No configuration found. Install istgt on $scfg->{portal}" if $msg eq '';
-
-    return $msg;
-};
-
-my $get_config = sub {
-    my ($scfg) = @_;
-    my @conf = undef;
-
-    my $config = $read_config->($scfg, undef, 'get_config');
-    die "Missing config file" unless $config;
-
-    $OLD_CONFIG = $config;
-
-    return $config;
-};
-
-my $parse_size = sub {
-    my ($text) = @_;
-
-    return 0 if !$text;
-
-    if ($text =~ m/^(\d+(\.\d+)?)([TGMK]B)?$/) {
-    my ($size, $reminder, $unit) = ($1, $2, $3);
-    return $size if !$unit;
-    if ($unit eq 'KB') {
-        $size *= 1024;
-    } elsif ($unit eq 'MB') {
-        $size *= 1024*1024;
-    } elsif ($unit eq 'GB') {
-        $size *= 1024*1024*1024;
-    } elsif ($unit eq 'TB') {
-        $size *= 1024*1024*1024*1024;
-    }
-        if ($reminder) {
-            $size = ceil($size);
-        }
-        return $size;
-    } elsif ($text =~ /^auto$/i) {
-        return 'AUTO';
-    } else {
-        return 0;
-    }
-};
-
-my $size_with_unit = sub {
-    my ($size, $n) = (shift, 0);
-
-    return '0KB' if !$size;
-
-    return $size if $size eq 'AUTO';
-
-    if ($size =~ m/^\d+$/) {
-        ++$n and $size /= 1024 until $size < 1024;
-        if ($size =~ /\./) {
-            return sprintf "%.2f%s", $size, ( qw[bytes KB MB GB TB] )[ $n ];
-        } else {
-            return sprintf "%d%s", $size, ( qw[bytes KB MB GB TB] )[ $n ];
-        }
-    }
-    die "$size: Not a number";
-};
-
-my $lun_dumper = sub {
-    my ($lun) = @_;
-    my $config = '';
-
-    $config .= "\n[$lun]\n";
-    $config .=  'TargetName ' . $SETTINGS->{$lun}->{TargetName} . "\n";
-    $config .=  'Mapping ' . $SETTINGS->{$lun}->{Mapping} . "\n";
-    $config .=  'AuthGroup ' . $SETTINGS->{$lun}->{AuthGroup} . "\n";
-    $config .=  'UnitType ' . $SETTINGS->{$lun}->{UnitType} . "\n";
-    $config .=  'QueueDepth ' . $SETTINGS->{$lun}->{QueueDepth} . "\n";
-
-    foreach my $conf (@{$SETTINGS->{$lun}->{luns}}) {
-        $config .=  "$conf->{lun} Storage " . $conf->{Storage};
-        $config .= ' ' . $size_with_unit->($conf->{Size}) . "\n";
-        foreach ($conf->{options}) {
-            if ($_) {
-                $config .=  "$conf->{lun} Option " . $_ . "\n";
-            }
-        }
-    }
-    $config .= "\n";
-
-    return $config;
-};
-
-my $get_lu_name = sub {
-    my ($target) = @_;
-    my $used = ();
-    my $i;
-
-    if (! exists $SETTINGS->{$target}->{used}) {
-        for ($i = 0; $i < $MAX_LUNS; $i++) {
-            $used->{$i} = 0;
-        }
-        foreach my $lun (@{$SETTINGS->{$target}->{luns}}) {
-            $lun->{lun} =~ /^LUN(\d+)$/;
-            $used->{$1} = 1;
-        }
-        $SETTINGS->{$target}->{used} = $used;
-    }
-
-    $used = $SETTINGS->{$target}->{used};
-    for ($i = 0; $i < $MAX_LUNS; $i++) {
-        last unless $used->{$i};
-    }
-    $SETTINGS->{$target}->{used}->{$i} = 1;
-
-    return "LUN$i";
-};
-
-my $init_lu_name = sub {
-    my ($target) = @_;
-    my $used = ();
-
-    if (! exists($SETTINGS->{$target}->{used})) {
-        for (my $i = 0; $i < $MAX_LUNS; $i++) {
-            $used->{$i} = 0;
-        }
-        $SETTINGS->{$target}->{used} = $used;
-    }
-    foreach my $lun (@{$SETTINGS->{$target}->{luns}}) {
-        $lun->{lun} =~ /^LUN(\d+)$/;
-        $SETTINGS->{$target}->{used}->{$1} = 1;
-    }
-};
-
-my $free_lu_name = sub {
-    my ($target, $lu_name) = @_;
-
-    $lu_name =~ /^LUN(\d+)$/;
-    $SETTINGS->{$target}->{used}->{$1} = 0;
-};
-
-my $make_lun = sub {
-    my ($scfg, $path) = @_;
-
-    my $target = $SETTINGS->{current};
-    die 'Maximum number of LUNs per target is 63' if scalar @{$SETTINGS->{$target}->{luns}} >= $MAX_LUNS;
-
-    my @options = ();
-    my $lun = $get_lu_name->($target);
-    if ($scfg->{nowritecache}) {
-        push @options, "WriteCache Disable";
-    }
-    my $conf = {
-        lun => $lun,
-        Storage => $path,
-        Size => 'AUTO',
-        options => @options,
-    };
-    push @{$SETTINGS->{$target}->{luns}}, $conf;
-
-    return $conf->{lun};
-};
-
-my $parser = sub {
-    my ($scfg) = @_;
-
-    my $lun = undef;
-    my $line = 0;
-
-    my $config = $get_config->($scfg);
-    my @cfgfile = split "\n", $config;
-
-    foreach (@cfgfile) {
-        $line++;
-        if ($_ =~ /^\s*\[(PortalGroup\d+)\]\s*/) {
-            $lun = undef;
-            $SETTINGS->{$1} = ();
-        } elsif ($_ =~ /^\s*\[(InitiatorGroup\d+)\]\s*/) {
-            $lun = undef;
-            $SETTINGS->{$1} = ();
-        } elsif ($_ =~ /^\s*PidFile\s+"?([\w\/\.]+)"?\s*/) {
-            $lun = undef;
-            $SETTINGS->{pidfile} = $1;
-        } elsif ($_ =~ /^\s*NodeBase\s+"?([\w\-\.]+)"?\s*/) {
-            $lun = undef;
-            $SETTINGS->{nodebase} = $1;
-        } elsif ($_ =~ /^\s*\[(LogicalUnit\d+)\]\s*/) {
-            $lun = $1;
-            $SETTINGS->{$lun} = ();
-            $SETTINGS->{targets}++;
-        } elsif ($lun) {
-            next if (($_ =~ /^\s*#/) || ($_ =~ /^\s*$/));
-            if ($_ =~ /^\s*(\w+)\s+(.+)\s*/) {
-                my $arg1 = $1;
-                my $arg2 = $2;
-                $arg2 =~ s/^\s+|\s+$|"\s*//g;
-                if ($arg2 =~ /^Storage\s*(.+)/i) {
-                    $SETTINGS->{$lun}->{$arg1}->{storage} = $1;
-                } elsif ($arg2 =~ /^Option\s*(.+)/i) {
-                    push @{$SETTINGS->{$lun}->{$arg1}->{options}}, $1;
-                } else {
-                    $SETTINGS->{$lun}->{$arg1} = $arg2;
-                }
-            } else {
-                die "$line: parse error [$_]";
-            }
-        }
-        $CONFIG .= "$_\n" unless $lun;
-    }
-
-    $CONFIG =~ s/\n$//;
-    die "$scfg->{target}: Target not found" unless $SETTINGS->{targets};
-    my $max = $SETTINGS->{targets};
-    my $base = get_base;
-
-    for (my $i = 1; $i <= $max; $i++) {
-        my $target = $SETTINGS->{nodebase}.':'.$SETTINGS->{"LogicalUnit$i"}->{TargetName};
-        if ($target eq $scfg->{target}) {
-            my $lu = ();
-            while ((my $key, my $val) = each(%{$SETTINGS->{"LogicalUnit$i"}})) {
-                if ($key =~ /^LUN\d+/) {
-                    $val->{storage} =~ /^([\w\/\-]+)\s+(\w+)/;
-                    my $storage = $1;
-                    my $size = $parse_size->($2);
-                    my $conf = undef;
-                    my @options = ();
-                    if ($val->{options}) {
-                        @options = @{$val->{options}};
-                    }
-                    if ($storage =~ /^$base\/$scfg->{pool}\/([\w\-]+)$/) {
-                        $conf = {
-                            lun => $key,
-                            Storage => $storage,
-                            Size => $size,
-                            options => @options,
-                        }
-                    }
-                    push @$lu, $conf if $conf;
-                    delete $SETTINGS->{"LogicalUnit$i"}->{$key};
-                }
-            }
-            $SETTINGS->{"LogicalUnit$i"}->{luns} = $lu;
-            $SETTINGS->{current} = "LogicalUnit$i";
-            $init_lu_name->("LogicalUnit$i");
-        } else {
-            $CONFIG .= $lun_dumper->("LogicalUnit$i");
-            delete $SETTINGS->{"LogicalUnit$i"};
-            $SETTINGS->{targets}--;
-        }
-    }
-    die "$scfg->{target}: Target not found" unless $SETTINGS->{targets} > 0;
-};
-
-my $list_lun = sub {
-    my ($scfg, $timeout, $method, @params) = @_;
-    my $name = undef;
-
-    my $object = $params[0];
-    for my $key (keys %$SETTINGS)  {
-        next unless $key =~ /^LogicalUnit\d+$/;
-        foreach my $lun (@{$SETTINGS->{$key}->{luns}}) {
-            if ($lun->{Storage} =~ /^$object$/) {
-                return $lun->{Storage};
-            }
-        }
-    }
-
-    return $name;
-};
-
-my $create_lun = sub {
-    my ($scfg, $timeout, $method, @params) = @_;
-    my $res = ();
-    my $file = "/tmp/config$$";
-
-    if ($list_lun->($scfg, $timeout, $method, @params)) {
-        die "$params[0]: LUN exists";
-    }
-    my $lun = $params[0];
-    $lun = $make_lun->($scfg, $lun);
-    my $config = $lun_dumper->($SETTINGS->{current});
-    open(my $fh, '>', $file) or die "Could not open file '$file' $!";
-
-    print $fh $CONFIG;
-    print $fh $config;
-    close $fh;
-    @params = ($CONFIG_FILE);
-    $res = {
-        cmd => 'scp',
-        method => $file,
-        params => \@params,
-        msg => $lun,
-        post_exe => sub {
-            unlink $file;
-        },
-    };
-
-    return $res;
-};
-
-my $delete_lun = sub {
-    my ($scfg, $timeout, $method, @params) = @_;
-    my $res = ();
-    my $file = "/tmp/config$$";
-
-    my $target = $SETTINGS->{current};
-    my $luns = ();
-
-    foreach my $conf (@{$SETTINGS->{$target}->{luns}}) {
-        if ($conf->{Storage} =~ /^$params[0]$/) {
-            $free_lu_name->($target, $conf->{lun});
-        } else {
-            push @$luns, $conf;
-        }
-    }
-    $SETTINGS->{$target}->{luns} = $luns;
-
-    my $config = $lun_dumper->($SETTINGS->{current});
-    open(my $fh, '>', $file) or die "Could not open file '$file' $!";
-
-    print $fh $CONFIG;
-    print $fh $config;
-    close $fh;
-    @params = ($CONFIG_FILE);
-    $res = {
-        cmd => 'scp',
-        method => $file,
-        params => \@params,
-        post_exe => sub {
-            unlink $file;
-            run_lun_command($scfg, undef, 'add_view', 'restart');
-        },
-    };
-
-    return $res;
-};
-
-my $import_lun = sub {
-    my ($scfg, $timeout, $method, @params) = @_;
-
-    my $res = $create_lun->($scfg, $timeout, $method, @params);
-
-    return $res;
-};
-
-my $add_view = sub {
-    my ($scfg, $timeout, $method, @params) = @_;
-    my $cmdmap;
-
-    if (@params && $params[0] eq 'restart') {
-        @params = ('onerestart', '>&', '/dev/null');
-        $cmdmap = {
-            cmd => 'ssh',
-            method => $DAEMON,
-            params => \@params,
-        };
-    } else {
-        @params = ('-HUP', '`cat '. "$SETTINGS->{pidfile}`");
-        $cmdmap = {
-            cmd => 'ssh',
-            method => 'kill',
-            params => \@params,
-        };
-    }
-
-    return $cmdmap;
-};
-
-my $modify_lun = sub {
-    my ($scfg, $timeout, $method, @params) = @_;
-
-    # Current SIGHUP reload limitations
-    # LU connected by the initiator can't be reloaded by SIGHUP.
-    # Until above limitation persists modifying a LUN will require
-    # a restart of the daemon breaking all current connections
-    #die 'Modify a connected LUN is not currently supported by istgt';
-    @params = ('restart', @params);
-
-    return $add_view->($scfg, $timeout, $method, @params);
-};
-
-my $list_view = sub {
-    my ($scfg, $timeout, $method, @params) = @_;
-    my $lun = undef;
-
-    my $object = $params[0];
-    for my $key (keys %$SETTINGS)  {
-        next unless $key =~ /^LogicalUnit\d+$/;
-        foreach my $lun (@{$SETTINGS->{$key}->{luns}}) {
-            if ($lun->{Storage} =~ /^$object$/) {
-                if ($lun->{lun} =~ /^LUN(\d+)/) {
-                    return $1;
-                }
-                die "$lun->{Storage}: Missing LUN";
-            }
-        }
-    }
-
-    return $lun;
-};
-
-my $get_lun_cmd_map = sub {
-    my ($method) = @_;
-
-    my $cmdmap = {
-        create_lu   => { cmd => $create_lun },
-        delete_lu   => { cmd => $delete_lun },
-        import_lu   => { cmd => $import_lun },
-        modify_lu   => { cmd => $modify_lun },
-        add_view    => { cmd => $add_view },
-        list_view   => { cmd => $list_view },
-        list_lu     => { cmd => $list_lun },
-    };
-
-    die "unknown command '$method'" unless exists $cmdmap->{$method};
-
-    return $cmdmap->{$method};
-};
-
-sub run_lun_command {
-    my ($scfg, $timeout, $method, @params) = @_;
-
-    my $msg = '';
-    my $luncmd;
-    my $target;
-    my $cmd;
-    my $res;
-    $timeout = 10 if !$timeout;
-    my $is_add_view = 0;
-
-    my $output = sub {
-    my $line = shift;
-    $msg .= "$line\n";
-    };
-
-    $target = 'root@' . $scfg->{portal};
-
-    $parser->($scfg) unless $SETTINGS;
-    my $cmdmap = $get_lun_cmd_map->($method);
-    if ($method eq 'add_view') {
-        $is_add_view = 1 ;
-        $timeout = 15;
-    }
-    if (ref $cmdmap->{cmd} eq 'CODE') {
-        $res = $cmdmap->{cmd}->($scfg, $timeout, $method, @params);
-        if (ref $res) {
-            $method = $res->{method};
-            @params = @{$res->{params}};
-            if ($res->{cmd} eq 'scp') {
-                $cmd = [@scp_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $method, "$target:$params[0]"];
-            } else {
-                $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, $method, @params];
-            }
-        } else {
-            return $res;
-        }
-    } else {
-        $luncmd = $cmdmap->{cmd};
-        $method = $cmdmap->{method};
-        $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, $luncmd, $method, @params];
-    }
-
-    eval {
-        run_command($cmd, outfunc => $output, timeout => $timeout);
-    };
-    if ($@ && $is_add_view) {
-        my $err = $@;
-        if ($OLD_CONFIG) {
-            my $err1 = undef;
-            my $file = "/tmp/config$$";
-            open(my $fh, '>', $file) or die "Could not open file '$file' $!";
-            print $fh $OLD_CONFIG;
-            close $fh;
-            $cmd = [@scp_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $file, $CONFIG_FILE];
-            eval {
-                run_command($cmd, outfunc => $output, timeout => $timeout);
-            };
-            $err1 = $@ if $@;
-            unlink $file;
-            die "$err\n$err1" if $err1;
-            eval {
-                run_lun_command($scfg, undef, 'add_view', 'restart');
-            };
-            die "$err\n$@" if ($@);
-        }
-        die $err;
-    } elsif ($@) {
-        die $@;
-    } elsif ($is_add_view) {
-        $OLD_CONFIG = undef;
-    }
-
-    if ($res->{post_exe} && ref $res->{post_exe} eq 'CODE') {
-        $res->{post_exe}->();
-    }
-
-    if ($res->{msg}) {
-        $msg = $res->{msg};
-    }
-
-    return $msg;
-}
-
-sub get_base {
-    return '/dev/zvol';
-}
-
-1;
diff --git a/PVE/Storage/LunCmd/LIO.pm b/PVE/Storage/LunCmd/LIO.pm
deleted file mode 100644 (file)
index 9264e46..0000000
+++ /dev/null
@@ -1,420 +0,0 @@
-package PVE::Storage::LunCmd::LIO;
-
-# lightly based on code from Iet.pm
-#
-# additional changes:
-# -----------------------------------------------------------------
-# Copyright (c) 2018 BestSolution.at EDV Systemhaus GmbH
-# All Rights Reserved.
-#
-# This software is released under the terms of the
-#
-#            "GNU Affero General Public License"
-#
-# and may only be distributed and used under the terms of the
-# mentioned license. You should have received a copy of the license
-# along with this software product, if not you can download it from
-# https://www.gnu.org/licenses/agpl-3.0.en.html
-#
-# Author: udo.rader@bestsolution.at
-# -----------------------------------------------------------------
-
-use strict;
-use warnings;
-use PVE::Tools qw(run_command);
-use JSON;
-
-sub get_base;
-
-# targetcli constants
-# config file location differs from distro to distro
-my @CONFIG_FILES = (
-       '/etc/rtslib-fb-target/saveconfig.json',        # Debian 9.x et al
-       '/etc/target/saveconfig.json' ,                 # ArchLinux, CentOS
-);
-my $BACKSTORE = '/backstores/block';
-
-my $SETTINGS = undef;
-my $SETTINGS_TIMESTAMP = 0;
-my $SETTINGS_MAXAGE = 15; # in seconds
-
-my @ssh_opts = ('-o', 'BatchMode=yes');
-my @ssh_cmd = ('/usr/bin/ssh', @ssh_opts);
-my $id_rsa_path = '/etc/pve/priv/zfs';
-my $targetcli = '/usr/bin/targetcli';
-
-my $execute_remote_command = sub {
-    my ($scfg, $timeout, $remote_command, @params) = @_;
-
-    my $msg = '';
-    my $err = undef;
-    my $target;
-    my $cmd;
-    my $res = ();
-
-    $timeout = 10 if !$timeout;
-
-    my $output = sub { $msg .= "$_[0]\n" };
-    my $errfunc = sub { $err .= "$_[0]\n" };
-
-    $target = 'root@' . $scfg->{portal};
-    $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, '--', $remote_command, @params];
-
-    eval {
-       run_command($cmd, outfunc => $output, errfunc => $errfunc, timeout => $timeout);
-    };
-    if ($@) {
-       $res = {
-           result => 0,
-           msg => $err,
-       }
-    } else {
-       $res = {
-           result => 1,
-           msg => $msg,
-       }
-    }
-
-    return $res;
-};
-
-# fetch targetcli configuration from the portal
-my $read_config = sub {
-    my ($scfg, $timeout) = @_;
-
-    my $msg = '';
-    my $err = undef;
-    my $luncmd = 'cat';
-    my $target;
-    my $retry = 1;
-
-    $timeout = 10 if !$timeout;
-
-    my $output = sub { $msg .= "$_[0]\n" };
-    my $errfunc = sub { $err .= "$_[0]\n" };
-
-    $target = 'root@' . $scfg->{portal};
-
-    foreach my $oneFile (@CONFIG_FILES) {
-       my $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, $luncmd, $oneFile];
-       eval {
-           run_command($cmd, outfunc => $output, errfunc => $errfunc, timeout => $timeout);
-       };
-       if ($@) {
-           die $err if ($err !~ /No such file or directory/);
-       }
-       return $msg if $msg ne '';
-    }
-
-    die "No configuration found. Install targetcli on $scfg->{portal}\n" if $msg eq '';
-
-    return $msg;
-};
-
-my $get_config = sub {
-    my ($scfg) = @_;
-    my @conf = undef;
-
-    my $config = $read_config->($scfg, undef);
-    die "Missing config file" unless $config;
-
-    return $config;
-};
-
-# Return settings of a specific target
-my $get_target_settings = sub {
-   my ($scfg) = @_;
-
-   my $id = "$scfg->{portal}.$scfg->{target}";
-   return undef if !$SETTINGS;
-   return $SETTINGS->{$id};
-};
-
-# fetches and parses targetcli config from the portal
-my $parser = sub {
-    my ($scfg) = @_;
-    my $tpg = $scfg->{lio_tpg} || die "Target Portal Group not set, aborting!\n";
-    my $tpg_tag;
-
-    if ($tpg =~ /^tpg(\d+)$/) {
-       $tpg_tag = $1;
-    } else {
-       die "Target Portal Group has invalid value, must contain string 'tpg' and a suffix number, eg 'tpg17'\n";
-    }
-
-    my $config = $get_config->($scfg);
-    my $jsonconfig = JSON->new->utf8->decode($config);
-
-    my $haveTarget = 0;
-    foreach my $target (@{$jsonconfig->{targets}}) {
-       # only interested in iSCSI targets
-       next if !($target->{fabric} eq 'iscsi' && $target->{wwn} eq $scfg->{target});
-       # find correct TPG
-       foreach my $tpg (@{$target->{tpgs}}) {
-           if ($tpg->{tag} == $tpg_tag) {
-               my $res = [];
-               foreach my $lun (@{$tpg->{luns}}) {
-                   my ($idx, $storage_object);
-                   if ($lun->{index} =~ /^(\d+)$/) {
-                       $idx = $1;
-                   }
-                   if ($lun->{storage_object} =~ m|^($BACKSTORE/.*)$|) {
-                       $storage_object = $1;
-                   }
-                   die "Invalid lun definition in config!\n"
-                       if !(defined($idx) && defined($storage_object));
-                   push @$res, { index => $idx, storage_object => $storage_object };
-               }
-
-               my $id = "$scfg->{portal}.$scfg->{target}";
-               $SETTINGS->{$id}->{luns} = $res;
-               $haveTarget = 1;
-               last;
-           }
-       }
-    }
-
-    # seriously unhappy if the target server lacks iSCSI target configuration ...
-    if (!$haveTarget) {
-       die "target portal group tpg$tpg_tag not found!\n";
-    }
-};
-
-# Get prefix for backstores
-my $get_backstore_prefix = sub {
-    my ($scfg) = @_;
-    my $pool = $scfg->{pool};
-    $pool =~ s/\//-/g;
-    return $pool . '-';
-};
-
-# removes the given lu_name from the local list of luns
-my $free_lu_name = sub {
-    my ($scfg, $lu_name) = @_;
-
-    my $new = [];
-    my $target = $get_target_settings->($scfg);
-    foreach my $lun (@{$target->{luns}}) {
-       if ($lun->{storage_object} ne "$BACKSTORE/$lu_name") {
-           push @$new, $lun;
-       }
-    }
-
-    $target->{luns} = $new;
-};
-
-# locally registers a new lun
-my $register_lun = sub {
-    my ($scfg, $idx, $volname) = @_;
-
-    my $conf = {
-       index => $idx,
-       storage_object => "$BACKSTORE/$volname",
-       is_new => 1,
-    };
-    my $target = $get_target_settings->($scfg);
-    push @{$target->{luns}}, $conf;
-
-    return $conf;
-};
-
-# extracts the ZFS volume name from a device path
-my $extract_volname = sub {
-    my ($scfg, $lunpath) = @_;
-    my $volname = undef;
-
-    my $base = get_base;
-    if ($lunpath =~ /^$base\/$scfg->{pool}\/([\w\-]+)$/) {
-       $volname = $1;
-       my $prefix = $get_backstore_prefix->($scfg);
-       my $target = $get_target_settings->($scfg);
-       foreach my $lun (@{$target->{luns}}) {
-           # If we have a lun with the pool prefix matching this vol, then return this one
-           # like pool-pve-vm-100-disk-0
-           # Else, just fallback to the old name scheme which is vm-100-disk-0
-           if ($lun->{storage_object} =~ /^$BACKSTORE\/($prefix$volname)$/) {
-               return $1;
-           }
-       }
-    }
-
-    return $volname;
-};
-
-# retrieves the LUN index for a particular object
-my $list_view = sub {
-    my ($scfg, $timeout, $method, @params) = @_;
-    my $lun = undef;
-
-    my $object = $params[0];
-    my $volname = $extract_volname->($scfg, $object);
-    my $target = $get_target_settings->($scfg);
-
-    return undef if !defined($volname); # nothing to search for..
-
-    foreach my $lun (@{$target->{luns}}) {
-       if ($lun->{storage_object} eq "$BACKSTORE/$volname") {
-           return $lun->{index};
-       }
-    }
-
-    return $lun;
-};
-
-# determines, if the given object exists on the portal
-my $list_lun = sub {
-    my ($scfg, $timeout, $method, @params) = @_;
-
-    my $object = $params[0];
-    my $volname = $extract_volname->($scfg, $object);
-    my $target = $get_target_settings->($scfg);
-
-    foreach my $lun (@{$target->{luns}}) {
-       if ($lun->{storage_object} eq "$BACKSTORE/$volname") {
-           return $object;
-       }
-    }
-
-    return undef;
-};
-
-# adds a new LUN to the target
-my $create_lun = sub {
-    my ($scfg, $timeout, $method, @params) = @_;
-
-    if ($list_lun->($scfg, $timeout, $method, @params)) {
-       die "$params[0]: LUN already exists!";
-    }
-
-    my $device = $params[0];
-    my $volname = $extract_volname->($scfg, $device);
-    # Here we create a new device, so we didn't get the volname prefixed with the pool name
-    # as extract_volname couldn't find a matching vol yet
-    $volname = $get_backstore_prefix->($scfg) . $volname;
-    my $tpg = $scfg->{lio_tpg} || die "Target Portal Group not set, aborting!\n";
-
-    # step 1: create backstore for device
-    my @cliparams = ($BACKSTORE, 'create', "name=$volname", "dev=$device" );
-    my $res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams);
-    die $res->{msg} if !$res->{result};
-
-    # step 2: enable unmap support on the backstore
-    @cliparams = ($BACKSTORE . '/' . $volname, 'set', 'attribute', 'emulate_tpu=1' );
-    $res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams);
-    die $res->{msg} if !$res->{result};
-
-    # step 3: register lun with target
-    # targetcli /iscsi/iqn.2018-04.at.bestsolution.somehost:target/tpg1/luns/ create /backstores/block/foobar
-    @cliparams = ("/iscsi/$scfg->{target}/$tpg/luns/", 'create', "$BACKSTORE/$volname" );
-    $res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams);
-    die $res->{msg} if !$res->{result};
-
-    # targetcli responds with "Created LUN 99"
-    # not calculating the index ourselves, because the index at the portal might have
-    # changed without our knowledge, so relying on the number that targetcli returns
-    my $lun_idx;
-    if ($res->{msg} =~ /LUN (\d+)/) {
-       $lun_idx = $1;
-    } else {
-       die "unable to determine new LUN index: $res->{msg}";
-    }
-
-    $register_lun->($scfg, $lun_idx, $volname);
-
-    # step 3: unfortunately, targetcli doesn't always save changes, no matter
-    #         if auto_save_on_exit is true or not. So saving to be safe ...
-    $execute_remote_command->($scfg, $timeout, $targetcli, 'saveconfig');
-
-    return $res->{msg};
-};
-
-my $delete_lun = sub {
-    my ($scfg, $timeout, $method, @params) = @_;
-    my $res = {msg => undef};
-
-    my $tpg = $scfg->{lio_tpg} || die "Target Portal Group not set, aborting!\n";
-
-    my $path = $params[0];
-    my $volname = $extract_volname->($scfg, $path);
-    my $target = $get_target_settings->($scfg);
-
-    foreach my $lun (@{$target->{luns}}) {
-       next if $lun->{storage_object} ne "$BACKSTORE/$volname";
-
-       # step 1: delete the lun
-       my @cliparams = ("/iscsi/$scfg->{target}/$tpg/luns/", 'delete', "lun$lun->{index}" );
-       my $res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams);
-       do {
-           die $res->{msg};
-       } unless $res->{result};
-
-       # step 2: delete the backstore
-       @cliparams = ($BACKSTORE, 'delete', $volname);
-       $res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams);
-       do {
-           die $res->{msg};
-       } unless $res->{result};
-
-       # step 3: save to be safe ...
-       $execute_remote_command->($scfg, $timeout, $targetcli, 'saveconfig');
-
-       # update internal cache
-       $free_lu_name->($scfg, $volname);
-
-       last;
-    }
-
-    return $res->{msg};
-};
-
-my $import_lun = sub {
-    my ($scfg, $timeout, $method, @params) = @_;
-
-    return $create_lun->($scfg, $timeout, $method, @params);
-};
-
-# needed for example when the underlying ZFS volume has been resized
-my $modify_lun = sub {
-    my ($scfg, $timeout, $method, @params) = @_;
-    # Nothing to do on volume modification for LIO
-    return undef;
-};
-
-my $add_view = sub {
-    my ($scfg, $timeout, $method, @params) = @_;
-
-    return '';
-};
-
-my %lun_cmd_map = (
-    create_lu   =>  $create_lun,
-    delete_lu   =>  $delete_lun,
-    import_lu   =>  $import_lun,
-    modify_lu   =>  $modify_lun,
-    add_view    =>  $add_view,
-    list_view   =>  $list_view,
-    list_lu     =>  $list_lun,
-);
-
-sub run_lun_command {
-    my ($scfg, $timeout, $method, @params) = @_;
-
-    # fetch configuration from target if we haven't yet or if it is stale
-    my $timediff = time - $SETTINGS_TIMESTAMP;
-    my $target = $get_target_settings->($scfg);
-    if (!$target || $timediff > $SETTINGS_MAXAGE) {
-       $SETTINGS_TIMESTAMP = time;
-       $parser->($scfg);
-    }
-
-    die "unknown command '$method'" unless exists $lun_cmd_map{$method};
-    my $msg = $lun_cmd_map{$method}->($scfg, $timeout, $method, @params);
-
-    return $msg;
-}
-
-sub get_base {
-    return '/dev';
-}
-
-1;
diff --git a/PVE/Storage/LunCmd/Makefile b/PVE/Storage/LunCmd/Makefile
deleted file mode 100644 (file)
index a7209d1..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-SOURCES=Comstar.pm Istgt.pm Iet.pm LIO.pm
-
-.PHONY: install
-install:
-       for i in ${SOURCES}; do install -D -m 0644 $$i ${DESTDIR}${PERLDIR}/PVE/Storage/LunCmd/$$i; done
diff --git a/PVE/Storage/LvmThinPlugin.pm b/PVE/Storage/LvmThinPlugin.pm
deleted file mode 100644 (file)
index 1d2e37c..0000000
+++ /dev/null
@@ -1,393 +0,0 @@
-package PVE::Storage::LvmThinPlugin;
-
-use strict;
-use warnings;
-
-use IO::File;
-
-use PVE::Tools qw(run_command trim);
-use PVE::Storage::Plugin;
-use PVE::Storage::LVMPlugin;
-use PVE::JSONSchema qw(get_standard_option);
-
-# see: man lvmthin
-# lvcreate -n ThinDataLV -L LargeSize VG
-# lvconvert --type thin-pool VG/ThinDataLV
-# lvcreate -n pvepool -L 20G pve
-# lvconvert --type thin-pool pve/pvepool
-
-# NOTE: volumes which were created as linked clones of another base volume
-# are currently not tracking this relationship in their volume IDs. this is
-# generally not a problem, as LVM thin allows deletion of such base volumes
-# without affecting the linked clones. this leads to increased disk usage
-# when migrating LVM-thin volumes, which is normally prevented for linked clones.
-
-use base qw(PVE::Storage::LVMPlugin);
-
-sub type {
-    return 'lvmthin';
-}
-
-sub plugindata {
-    return {
-       content => [ {images => 1, rootdir => 1}, { images => 1, rootdir => 1}],
-    };
-}
-
-sub properties {
-    return {
-       thinpool => {
-           description => "LVM thin pool LV name.",
-           type => 'string', format => 'pve-storage-vgname',
-       },
-    };
-}
-
-sub options {
-    return {
-       thinpool => { fixed => 1 },
-       vgname => { fixed => 1 },
-        nodes => { optional => 1 },
-       disable => { optional => 1 },
-       content => { optional => 1 },
-       bwlimit => { optional => 1 },
-    };
-}
-
-# NOTE: the fourth and fifth element of the returned array are always
-# undef, even if the volume is a linked clone of another volume. see note
-# at beginning of file.
-sub parse_volname {
-    my ($class, $volname) = @_;
-
-    PVE::Storage::Plugin::parse_lvm_name($volname);
-
-    if ($volname =~ m/^((vm|base)-(\d+)-\S+)$/) {
-       return ('images', $1, $3, undef, undef, $2 eq 'base', 'raw');
-    }
-
-    die "unable to parse lvm volume name '$volname'\n";
-}
-
-sub filesystem_path {
-    my ($class, $scfg, $volname, $snapname) = @_;
-
-    my ($vtype, $name, $vmid) = $class->parse_volname($volname);
-
-    my $vg = $scfg->{vgname};
-
-    my $path = defined($snapname) ? "/dev/$vg/snap_${name}_$snapname": "/dev/$vg/$name";
-
-    return wantarray ? ($path, $vmid, $vtype) : $path;
-}
-
-sub alloc_image {
-    my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
-
-    die "unsupported format '$fmt'" if $fmt ne 'raw';
-
-    die "illegal name '$name' - should be 'vm-$vmid-*'\n"
-       if  $name && $name !~ m/^vm-$vmid-/;
-
-    my $vgs = PVE::Storage::LVMPlugin::lvm_vgs();
-
-    my $vg = $scfg->{vgname};
-
-    die "no such volume group '$vg'\n" if !defined ($vgs->{$vg});
-
-    $name = $class->find_free_diskname($storeid, $scfg, $vmid)
-       if !$name;
-
-    my $cmd = ['/sbin/lvcreate', '-aly', '-V', "${size}k", '--name', $name,
-              '--thinpool', "$vg/$scfg->{thinpool}" ];
-
-    run_command($cmd, errmsg => "lvcreate '$vg/$name' error");
-
-    return $name;
-}
-
-sub free_image {
-    my ($class, $storeid, $scfg, $volname, $isBase) = @_;
-
-    my $vg = $scfg->{vgname};
-
-    my $lvs = PVE::Storage::LVMPlugin::lvm_list_volumes($vg);
-
-    if (my $dat = $lvs->{$scfg->{vgname}}) {
-
-       # remove all volume snapshots first
-       foreach my $lv (keys %$dat) {
-           next if $lv !~ m/^snap_${volname}_${PVE::JSONSchema::CONFIGID_RE}$/;
-           my $cmd = ['/sbin/lvremove', '-f', "$vg/$lv"];
-           run_command($cmd, errmsg => "lvremove snapshot '$vg/$lv' error");
-       }
-
-       # finally remove original (if exists)
-       if ($dat->{$volname}) {
-           my $cmd = ['/sbin/lvremove', '-f', "$vg/$volname"];
-           run_command($cmd, errmsg => "lvremove '$vg/$volname' error");
-       }
-    }
-
-    return undef;
-}
-
-sub list_images {
-    my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
-
-    my $vgname = $scfg->{vgname};
-
-    $cache->{lvs} = PVE::Storage::LVMPlugin::lvm_list_volumes() if !$cache->{lvs};
-
-    my $res = [];
-
-    if (my $dat = $cache->{lvs}->{$vgname}) {
-
-       foreach my $volname (keys %$dat) {
-
-           next if $volname !~ m/^(vm|base)-(\d+)-/;
-           my $owner = $2;
-
-           my $info = $dat->{$volname};
-
-           next if $info->{lv_type} ne 'V';
-
-           next if $info->{pool_lv} ne $scfg->{thinpool};
-
-           my $volid = "$storeid:$volname";
-
-           if ($vollist) {
-               my $found = grep { $_ eq $volid } @$vollist;
-               next if !$found;
-           } else {
-               next if defined($vmid) && ($owner ne $vmid);
-           }
-
-           push @$res, {
-               volid => $volid, format => 'raw', size => $info->{lv_size}, vmid => $owner,
-               ctime => $info->{ctime},
-           };
-       }
-    }
-
-    return $res;
-}
-
-sub list_thinpools {
-    my ($vg) = @_;
-
-    my $lvs = PVE::Storage::LVMPlugin::lvm_list_volumes($vg);
-    my $thinpools = [];
-
-    foreach my $vg (keys %$lvs) {
-       foreach my $lvname (keys %{$lvs->{$vg}}) {
-           next if $lvs->{$vg}->{$lvname}->{lv_type} ne 't';
-           my $lv = $lvs->{$vg}->{$lvname};
-           $lv->{lv} = $lvname;
-           $lv->{vg} = $vg;
-           push @$thinpools, $lv;
-       }
-    }
-
-    return $thinpools;
-}
-
-sub status {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    my $lvs = $cache->{lvs} ||= PVE::Storage::LVMPlugin::lvm_list_volumes();
-
-    return if !$lvs->{$scfg->{vgname}};
-
-    my $info = $lvs->{$scfg->{vgname}}->{$scfg->{thinpool}};
-
-    return if !$info || $info->{lv_type} ne 't' || !$info->{lv_size};
-
-    return (
-       $info->{lv_size},
-       $info->{lv_size} - $info->{used},
-       $info->{used},
-       $info->{lv_state} eq 'a' ? 1 : 0,
-    );
-}
-
-my $activate_lv = sub {
-    my ($vg, $lv, $cache) = @_;
-
-    my $lvs = $cache->{lvs} ||= PVE::Storage::LVMPlugin::lvm_list_volumes();
-
-    die "no such logical volume $vg/$lv\n" if !$lvs->{$vg} || !$lvs->{$vg}->{$lv};
-
-    return if $lvs->{$vg}->{$lv}->{lv_state} eq 'a';
-
-    run_command(['lvchange', '-ay', '-K', "$vg/$lv"], errmsg => "activating LV '$vg/$lv' failed");
-
-    $lvs->{$vg}->{$lv}->{lv_state} = 'a'; # update cache
-
-    return;
-};
-
-sub activate_storage {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    $class->SUPER::activate_storage($storeid, $scfg, $cache);
-
-    $activate_lv->($scfg->{vgname}, $scfg->{thinpool}, $cache);
-}
-
-sub activate_volume {
-    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
-
-    my $vg = $scfg->{vgname};
-    my $lv = $snapname ? "snap_${volname}_$snapname" : $volname;
-
-    $activate_lv->($vg, $lv, $cache);
-}
-
-sub deactivate_volume {
-    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
-
-    return if !$snapname && $volname !~ /^base-/; # other volumes are kept active
-
-    my $vg = $scfg->{vgname};
-    my $lv = $snapname ? "snap_${volname}_$snapname" : $volname;
-
-    run_command(['lvchange', '-an', "$vg/$lv"], errmsg => "deactivate_volume '$vg/$lv' error");
-
-    $cache->{lvs}->{$vg}->{$lv}->{lv_state} = '-' # update cache
-       if $cache->{lvs} && $cache->{lvs}->{$vg} && $cache->{lvs}->{$vg}->{$lv};
-
-    return;
-}
-
-sub clone_image {
-    my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
-
-    my $vg = $scfg->{vgname};
-
-    my $lv;
-
-    if ($snap) {
-       $lv = "$vg/snap_${volname}_$snap";
-    } else {
-       my ($vtype, undef, undef, undef, undef, $isBase, $format) =
-           $class->parse_volname($volname);
-
-       die "clone_image only works on base images\n" if !$isBase;
-
-       $lv = "$vg/$volname";
-    }
-
-    my $name = $class->find_free_diskname($storeid, $scfg, $vmid);
-
-    my $cmd = ['/sbin/lvcreate', '-n', $name, '-prw', '-kn', '-s', $lv];
-    run_command($cmd, errmsg => "clone image '$lv' error");
-
-    return $name;
-}
-
-sub create_base {
-    my ($class, $storeid, $scfg, $volname) = @_;
-
-    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
-        $class->parse_volname($volname);
-
-    die "create_base not possible with base image\n" if $isBase;
-
-    my $vg = $scfg->{vgname};
-    my $lvs = PVE::Storage::LVMPlugin::lvm_list_volumes($vg);
-
-    if (my $dat = $lvs->{$vg}) {
-       # to avoid confusion, reject if we find volume snapshots
-       foreach my $lv (keys %$dat) {
-           die "unable to create base volume - found snaphost '$lv'\n"
-               if $lv =~ m/^snap_${volname}_(\w+)$/;
-       }
-    }
-
-    my $newname = $name;
-    $newname =~ s/^vm-/base-/;
-
-    my $cmd = ['/sbin/lvrename', $vg, $volname, $newname];
-    run_command($cmd, errmsg => "lvrename '$vg/$volname' => '$vg/$newname' error");
-
-    # set inactive, read-only and activationskip flags
-    $cmd = ['/sbin/lvchange', '-an', '-pr', '-ky', "$vg/$newname"];
-    eval { run_command($cmd); };
-    warn $@ if $@;
-
-    my $newvolname = $newname;
-
-    return $newvolname;
-}
-
-# sub volume_resize {} reuse code from parent class
-
-sub volume_snapshot {
-    my ($class, $scfg, $storeid, $volname, $snap) = @_;
-
-    my $vg = $scfg->{vgname};
-    my $snapvol = "snap_${volname}_$snap";
-
-    my $cmd = ['/sbin/lvcreate', '-n', $snapvol, '-pr', '-s', "$vg/$volname"];
-    run_command($cmd, errmsg => "lvcreate snapshot '$vg/$snapvol' error");
-
-}
-
-sub volume_snapshot_rollback {
-    my ($class, $scfg, $storeid, $volname, $snap) = @_;
-
-    my $vg = $scfg->{vgname};
-    my $snapvol = "snap_${volname}_$snap";
-
-    my $cmd = ['/sbin/lvremove', '-f', "$vg/$volname"];
-    run_command($cmd, errmsg => "lvremove '$vg/$volname' error");
-
-    $cmd = ['/sbin/lvcreate', '-kn', '-n', $volname, '-s', "$vg/$snapvol"];
-    run_command($cmd, errmsg => "lvm rollback '$vg/$snapvol' error");
-}
-
-sub volume_snapshot_delete {
-    my ($class, $scfg, $storeid, $volname, $snap) = @_;
-
-    my $vg = $scfg->{vgname};
-    my $snapvol = "snap_${volname}_$snap";
-
-    my $cmd = ['/sbin/lvremove', '-f', "$vg/$snapvol"];
-    run_command($cmd, errmsg => "lvremove snapshot '$vg/$snapvol' error");
-}
-
-sub volume_has_feature {
-    my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
-
-    my $features = {
-       snapshot => { current => 1 },
-       clone => { base => 1, snap => 1},
-       template => { current => 1},
-       copy => { base => 1, current => 1, snap => 1},
-       sparseinit => { base => 1, current => 1},
-       rename => {current => 1},
-    };
-
-    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
-       $class->parse_volname($volname);
-
-    my $key = undef;
-    if($snapname){
-       $key = 'snap';
-    }else{
-       $key =  $isBase ? 'base' : 'current';
-    }
-    return 1 if $features->{$feature}->{$key};
-
-    return undef;
-}
-
-# used in LVMPlugin->volume_import
-sub volume_import_write {
-    my ($class, $input_fh, $output_file) = @_;
-    run_command(['dd', "of=$output_file", 'conv=sparse', 'bs=64k'],
-       input => '<&'.fileno($input_fh));
-}
-
-1;
diff --git a/PVE/Storage/Makefile b/PVE/Storage/Makefile
deleted file mode 100644 (file)
index 857c485..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-SOURCES= \
-       Plugin.pm \
-       DirPlugin.pm \
-       LVMPlugin.pm \
-       NFSPlugin.pm \
-       CIFSPlugin.pm \
-       ISCSIPlugin.pm \
-       CephFSPlugin.pm \
-       RBDPlugin.pm \
-       ISCSIDirectPlugin.pm \
-       GlusterfsPlugin.pm \
-       ZFSPoolPlugin.pm \
-       ZFSPlugin.pm \
-       PBSPlugin.pm \
-       BTRFSPlugin.pm \
-       LvmThinPlugin.pm
-
-.PHONY: install
-install:
-       for i in ${SOURCES}; do install -D -m 0644 $$i ${DESTDIR}${PERLDIR}/PVE/Storage/$$i; done
-       make -C LunCmd install
diff --git a/PVE/Storage/NFSPlugin.pm b/PVE/Storage/NFSPlugin.pm
deleted file mode 100644 (file)
index c2d4176..0000000
+++ /dev/null
@@ -1,228 +0,0 @@
-package PVE::Storage::NFSPlugin;
-
-use strict;
-use warnings;
-use IO::File;
-use Net::IP;
-use File::Path;
-
-use PVE::Network;
-use PVE::Tools qw(run_command);
-use PVE::ProcFSTools;
-use PVE::Storage::Plugin;
-use PVE::JSONSchema qw(get_standard_option);
-
-use base qw(PVE::Storage::Plugin);
-
-# NFS helper functions
-
-sub nfs_is_mounted {
-    my ($server, $export, $mountpoint, $mountdata) = @_;
-
-    $server = "[$server]" if Net::IP::ip_is_ipv6($server);
-    my $source = "$server:$export";
-
-    $mountdata = PVE::ProcFSTools::parse_proc_mounts() if !$mountdata;
-    return $mountpoint if grep {
-       $_->[2] =~ /^nfs/ &&
-       $_->[0] =~ m|^\Q$source\E/?$| &&
-       $_->[1] eq $mountpoint
-    } @$mountdata;
-    return undef;
-}
-
-sub nfs_mount {
-    my ($server, $export, $mountpoint, $options) = @_;
-
-    $server = "[$server]" if Net::IP::ip_is_ipv6($server);
-    my $source = "$server:$export";
-
-    my $cmd = ['/bin/mount', '-t', 'nfs', $source, $mountpoint];
-    if ($options) {
-       push @$cmd, '-o', $options;
-    } 
-
-    run_command($cmd, errmsg => "mount error");
-}
-
-# Configuration
-
-sub type {
-    return 'nfs';
-}
-
-sub plugindata {
-    return {
-       content => [ { images => 1, rootdir => 1, vztmpl => 1, iso => 1, backup => 1, snippets => 1 },
-                    { images => 1 }],
-       format => [ { raw => 1, qcow2 => 1, vmdk => 1 } , 'raw' ],
-    };
-}   
-
-sub properties {
-    return {
-       export => {
-           description => "NFS export path.",
-           type => 'string', format => 'pve-storage-path',
-       },
-       server => {
-           description => "Server IP or DNS name.",
-           type => 'string', format => 'pve-storage-server',
-       },
-       options => {
-           description => "NFS mount options (see 'man nfs')",
-           type => 'string',  format => 'pve-storage-options',
-       },
-    };
-}
-
-sub options {
-    return {
-       path => { fixed => 1 },
-       'content-dirs' => { optional => 1 },
-       server => { fixed => 1 },
-       export => { fixed => 1 },
-       nodes => { optional => 1 },
-       disable => { optional => 1 },
-       maxfiles => { optional => 1 },
-       'prune-backups' => { optional => 1 },
-       'max-protected-backups' => { optional => 1 },
-       options => { optional => 1 },
-       content => { optional => 1 },
-       format => { optional => 1 },
-       mkdir => { optional => 1 },
-       bwlimit => { optional => 1 },
-       preallocation => { optional => 1 },
-    };
-}
-
-
-sub check_config {
-    my ($class, $sectionId, $config, $create, $skipSchemaCheck) = @_;
-
-    $config->{path} = "/mnt/pve/$sectionId" if $create && !$config->{path};
-
-    return $class->SUPER::check_config($sectionId, $config, $create, $skipSchemaCheck);
-}
-
-# Storage implementation
-
-sub status {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    $cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts()
-       if !$cache->{mountdata};
-
-    my $path = $scfg->{path};
-    my $server = $scfg->{server};
-    my $export = $scfg->{export};
-
-    return undef if !nfs_is_mounted($server, $export, $path, $cache->{mountdata}); 
-
-    return $class->SUPER::status($storeid, $scfg, $cache);
-}
-
-sub activate_storage {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    $cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts()
-       if !$cache->{mountdata};
-
-    my $path = $scfg->{path};
-    my $server = $scfg->{server};
-    my $export = $scfg->{export};
-
-    if (!nfs_is_mounted($server, $export, $path, $cache->{mountdata})) {
-       # NOTE: only call mkpath when not mounted (avoid hang when NFS server is offline
-       mkpath $path if !(defined($scfg->{mkdir}) && !$scfg->{mkdir});
-
-       die "unable to activate storage '$storeid' - " .
-           "directory '$path' does not exist\n" if ! -d $path;
-
-       nfs_mount($server, $export, $path, $scfg->{options});
-    }
-
-    $class->SUPER::activate_storage($storeid, $scfg, $cache);
-}
-
-sub deactivate_storage {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    $cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts()
-       if !$cache->{mountdata};
-
-    my $path = $scfg->{path};
-    my $server = $scfg->{server};
-    my $export = $scfg->{export};
-
-    if (nfs_is_mounted($server, $export, $path, $cache->{mountdata})) {    
-       my $cmd = ['/bin/umount', $path];
-       run_command($cmd, errmsg => 'umount error'); 
-    }
-}
-
-sub check_connection {
-    my ($class, $storeid, $scfg) = @_;
-
-    my $server = $scfg->{server};
-    my $opts = $scfg->{options};
-
-    my $cmd;
-
-    my $is_v4 = defined($opts) && $opts =~ /vers=4.*/;
-    if ($is_v4) {
-       my $ip = PVE::JSONSchema::pve_verify_ip($server, 1);
-       if (!defined($ip)) {
-           $ip = PVE::Network::get_ip_from_hostname($server);
-       }
-
-       my $transport = PVE::JSONSchema::pve_verify_ipv4($ip, 1) ? 'tcp' : 'tcp6';
-
-       # nfsv4 uses a pseudo-filesystem always beginning with /
-       # no exports are listed
-       $cmd = ['/usr/sbin/rpcinfo', '-T', $transport, $ip, 'nfs', '4'];
-    } else {
-       $cmd = ['/sbin/showmount', '--no-headers', '--exports', $server];
-    }
-
-    eval { run_command($cmd, timeout => 10, outfunc => sub {}, errfunc => sub {}) };
-    if (my $err = $@) {
-       if ($is_v4) {
-           my $port = 2049;
-           $port = $1 if defined($opts) && $opts =~ /port=(\d+)/;
-
-           # rpcinfo is expected to work when the port is 0 (see 'man 5 nfs') and tcp_ping()
-           # defaults to port 7 when passing in 0.
-           return 0 if $port == 0;
-
-           return PVE::Network::tcp_ping($server, $port, 2);
-       }
-       return 0;
-    }
-
-    return 1;
-}
-
-# FIXME remove on the next APIAGE reset.
-# Deprecated, use get_volume_attribute instead.
-sub get_volume_notes {
-    my $class = shift;
-    PVE::Storage::DirPlugin::get_volume_notes($class, @_);
-}
-
-# FIXME remove on the next APIAGE reset.
-# Deprecated, use update_volume_attribute instead.
-sub update_volume_notes {
-    my $class = shift;
-    PVE::Storage::DirPlugin::update_volume_notes($class, @_);
-}
-
-sub get_volume_attribute {
-    return PVE::Storage::DirPlugin::get_volume_attribute(@_);
-}
-
-sub update_volume_attribute {
-    return PVE::Storage::DirPlugin::update_volume_attribute(@_);
-}
-
-1;
diff --git a/PVE/Storage/PBSPlugin.pm b/PVE/Storage/PBSPlugin.pm
deleted file mode 100644 (file)
index 4320974..0000000
+++ /dev/null
@@ -1,981 +0,0 @@
-package PVE::Storage::PBSPlugin;
-
-# Plugin to access Proxmox Backup Server
-
-use strict;
-use warnings;
-
-use Fcntl qw(F_GETFD F_SETFD FD_CLOEXEC);
-use IO::File;
-use JSON;
-use MIME::Base64 qw(decode_base64);
-use POSIX qw(mktime strftime ENOENT);
-use POSIX::strptime;
-
-use PVE::APIClient::LWP;
-use PVE::JSONSchema qw(get_standard_option);
-use PVE::Network;
-use PVE::PBSClient;
-use PVE::Storage::Plugin;
-use PVE::Tools qw(run_command file_read_firstline trim dir_glob_regex dir_glob_foreach $IPV6RE);
-
-use base qw(PVE::Storage::Plugin);
-
-# Configuration
-
-sub type {
-    return 'pbs';
-}
-
-sub plugindata {
-    return {
-       content => [ {backup => 1, none => 1}, { backup => 1 }],
-    };
-}
-
-sub properties {
-    return {
-       datastore => {
-           description => "Proxmox Backup Server datastore name.",
-           type => 'string',
-       },
-       # openssl s_client -connect <host>:8007 2>&1 |openssl x509 -fingerprint -sha256
-       fingerprint => get_standard_option('fingerprint-sha256'),
-       'encryption-key' => {
-           description => "Encryption key. Use 'autogen' to generate one automatically without passphrase.",
-           type => 'string',
-       },
-       'master-pubkey' => {
-           description => "Base64-encoded, PEM-formatted public RSA key. Used to encrypt a copy of the encryption-key which will be added to each encrypted backup.",
-           type => 'string',
-       },
-       port => {
-           description => "For non default port.",
-           type => 'integer',
-           minimum => 1,
-           maximum => 65535,
-           default => 8007,
-       },
-    };
-}
-
-sub options {
-    return {
-       server => { fixed => 1 },
-       datastore => { fixed => 1 },
-       namespace => { optional => 1 },
-       port => { optional => 1 },
-       nodes => { optional => 1},
-       disable => { optional => 1},
-       content => { optional => 1},
-       username => { optional => 1 },
-       password => { optional => 1 },
-       'encryption-key' => { optional => 1 },
-       'master-pubkey' => { optional => 1 },
-       maxfiles => { optional => 1 },
-       'prune-backups' => { optional => 1 },
-       'max-protected-backups' => { optional => 1 },
-       fingerprint => { optional => 1 },
-    };
-}
-
-# Helpers
-
-sub pbs_password_file_name {
-    my ($scfg, $storeid) = @_;
-
-    return "/etc/pve/priv/storage/${storeid}.pw";
-}
-
-sub pbs_set_password {
-    my ($scfg, $storeid, $password) = @_;
-
-    my $pwfile = pbs_password_file_name($scfg, $storeid);
-    mkdir "/etc/pve/priv/storage";
-
-    PVE::Tools::file_set_contents($pwfile, "$password\n");
-}
-
-sub pbs_delete_password {
-    my ($scfg, $storeid) = @_;
-
-    my $pwfile = pbs_password_file_name($scfg, $storeid);
-
-    unlink $pwfile;
-}
-
-sub pbs_get_password {
-    my ($scfg, $storeid) = @_;
-
-    my $pwfile = pbs_password_file_name($scfg, $storeid);
-
-    return PVE::Tools::file_read_firstline($pwfile);
-}
-
-sub pbs_encryption_key_file_name {
-    my ($scfg, $storeid) = @_;
-
-    return "/etc/pve/priv/storage/${storeid}.enc";
-}
-
-sub pbs_set_encryption_key {
-    my ($scfg, $storeid, $key) = @_;
-
-    my $pwfile = pbs_encryption_key_file_name($scfg, $storeid);
-    mkdir "/etc/pve/priv/storage";
-
-    PVE::Tools::file_set_contents($pwfile, "$key\n");
-}
-
-sub pbs_delete_encryption_key {
-    my ($scfg, $storeid) = @_;
-
-    my $pwfile = pbs_encryption_key_file_name($scfg, $storeid);
-
-    if (!unlink $pwfile) {
-       return if $! == ENOENT;
-       die "failed to delete encryption key! $!\n";
-    }
-    delete $scfg->{'encryption-key'};
-}
-
-sub pbs_get_encryption_key {
-    my ($scfg, $storeid) = @_;
-
-    my $pwfile = pbs_encryption_key_file_name($scfg, $storeid);
-
-    return PVE::Tools::file_get_contents($pwfile);
-}
-
-# Returns a file handle if there is an encryption key, or `undef` if there is not. Dies on error.
-sub pbs_open_encryption_key {
-    my ($scfg, $storeid) = @_;
-
-    my $encryption_key_file = pbs_encryption_key_file_name($scfg, $storeid);
-
-    my $keyfd;
-    if (!open($keyfd, '<', $encryption_key_file)) {
-       if ($! == ENOENT) {
-           my $encryption_fp = $scfg->{'encryption-key'};
-           die "encryption configured ('$encryption_fp') but no encryption key file found!\n"
-               if $encryption_fp;
-           return undef;
-       }
-       die "failed to open encryption key: $encryption_key_file: $!\n";
-    }
-
-    return $keyfd;
-}
-
-sub pbs_master_pubkey_file_name {
-    my ($scfg, $storeid) = @_;
-
-    return "/etc/pve/priv/storage/${storeid}.master.pem";
-}
-
-sub pbs_set_master_pubkey {
-    my ($scfg, $storeid, $key) = @_;
-
-    my $pwfile = pbs_master_pubkey_file_name($scfg, $storeid);
-    mkdir "/etc/pve/priv/storage";
-
-    PVE::Tools::file_set_contents($pwfile, "$key\n");
-}
-
-sub pbs_delete_master_pubkey {
-    my ($scfg, $storeid) = @_;
-
-    my $pwfile = pbs_master_pubkey_file_name($scfg, $storeid);
-
-    if (!unlink $pwfile) {
-       return if $! == ENOENT;
-       die "failed to delete master public key! $!\n";
-    }
-    delete $scfg->{'master-pubkey'};
-}
-
-sub pbs_get_master_pubkey {
-    my ($scfg, $storeid) = @_;
-
-    my $pwfile = pbs_master_pubkey_file_name($scfg, $storeid);
-
-    return PVE::Tools::file_get_contents($pwfile);
-}
-
-# Returns a file handle if there is a master key, or `undef` if there is not. Dies on error.
-sub pbs_open_master_pubkey {
-    my ($scfg, $storeid) = @_;
-
-    my $master_pubkey_file = pbs_master_pubkey_file_name($scfg, $storeid);
-
-    my $keyfd;
-    if (!open($keyfd, '<', $master_pubkey_file)) {
-       if ($! == ENOENT) {
-           die "master public key configured but no key file found!\n"
-               if $scfg->{'master-pubkey'};
-           return undef;
-       }
-       die "failed to open master public key: $master_pubkey_file: $!\n";
-    }
-
-    return $keyfd;
-}
-
-sub print_volid {
-    my ($storeid, $btype, $bid, $btime) = @_;
-
-    my $time_str = strftime("%FT%TZ", gmtime($btime));
-    my $volname = "backup/${btype}/${bid}/${time_str}";
-
-    return "${storeid}:${volname}";
-}
-
-my sub ns : prototype($$) {
-    my ($scfg, $name) = @_;
-    my $ns = $scfg->{namespace};
-    return defined($ns) ? ($name, $ns) : ();
-}
-
-# essentially the inverse of print_volid
-my sub api_param_from_volname : prototype($$$) {
-    my ($class, $scfg, $volname) = @_;
-
-    my $name = ($class->parse_volname($volname))[1];
-
-    my ($btype, $bid, $timestr) = split('/', $name);
-
-    my @tm = (POSIX::strptime($timestr, "%FT%TZ"));
-    # expect sec, min, hour, mday, mon, year
-    die "error parsing time from '$volname'" if grep { !defined($_) } @tm[0..5];
-
-    my $btime;
-    {
-       local $ENV{TZ} = 'UTC'; # $timestr is UTC
-
-       # Fill in isdst to avoid undef warning. No daylight saving time for UTC.
-       $tm[8] //= 0;
-
-       my $since_epoch = mktime(@tm) or die "error converting time from '$volname'\n";
-       $btime = int($since_epoch);
-    }
-
-    return {
-       (ns($scfg, 'ns')),
-       'backup-type' => $btype,
-       'backup-id' => $bid,
-       'backup-time' => $btime,
-    };
-}
-
-my $USE_CRYPT_PARAMS = {
-    backup => 1,
-    restore => 1,
-    'upload-log' => 1,
-};
-
-my $USE_MASTER_KEY = {
-    backup => 1,
-};
-
-my sub do_raw_client_cmd {
-    my ($scfg, $storeid, $client_cmd, $param, %opts) = @_;
-
-    my $use_crypto = $USE_CRYPT_PARAMS->{$client_cmd};
-    my $use_master = $USE_MASTER_KEY->{$client_cmd};
-
-    my $client_exe = '/usr/bin/proxmox-backup-client';
-    die "executable not found '$client_exe'! Proxmox backup client not installed?\n"
-       if ! -x $client_exe;
-
-    my $repo = PVE::PBSClient::get_repository($scfg);
-
-    my $userns_cmd = delete $opts{userns_cmd};
-
-    my $cmd = [];
-
-    push @$cmd, @$userns_cmd if defined($userns_cmd);
-
-    push @$cmd, $client_exe, $client_cmd;
-
-    # This must live in the top scope to not get closed before the `run_command`
-    my ($keyfd, $master_fd);
-    if ($use_crypto) {
-       if (defined($keyfd = pbs_open_encryption_key($scfg, $storeid))) {
-           my $flags = fcntl($keyfd, F_GETFD, 0)
-               // die "failed to get file descriptor flags: $!\n";
-           fcntl($keyfd, F_SETFD, $flags & ~FD_CLOEXEC)
-               or die "failed to remove FD_CLOEXEC from encryption key file descriptor\n";
-           push @$cmd, '--crypt-mode=encrypt', '--keyfd='.fileno($keyfd);
-           if ($use_master && defined($master_fd = pbs_open_master_pubkey($scfg, $storeid))) {
-               my $flags = fcntl($master_fd, F_GETFD, 0)
-                   // die "failed to get file descriptor flags: $!\n";
-               fcntl($master_fd, F_SETFD, $flags & ~FD_CLOEXEC)
-                   or die "failed to remove FD_CLOEXEC from master public key file descriptor\n";
-               push @$cmd, '--master-pubkey-fd='.fileno($master_fd);
-           }
-       } else {
-           push @$cmd, '--crypt-mode=none';
-       }
-    }
-
-    push @$cmd, @$param if defined($param);
-
-    push @$cmd, "--repository", $repo;
-    if ($client_cmd ne 'status' && defined(my $ns = $scfg->{namespace})) {
-       push @$cmd, '--ns', $ns;
-    }
-
-    local $ENV{PBS_PASSWORD} = pbs_get_password($scfg, $storeid);
-
-    local $ENV{PBS_FINGERPRINT} = $scfg->{fingerprint};
-
-    # no ascii-art on task logs
-    local $ENV{PROXMOX_OUTPUT_NO_BORDER} = 1;
-    local $ENV{PROXMOX_OUTPUT_NO_HEADER} = 1;
-
-    if (my $logfunc = $opts{logfunc}) {
-       $logfunc->("run: " . join(' ', @$cmd));
-    }
-
-    run_command($cmd, %opts);
-}
-
-# FIXME: External perl code should NOT have access to this.
-#
-# There should be separate functions to
-# - make backups
-# - restore backups
-# - restore files
-# with a sane API
-sub run_raw_client_cmd {
-    my ($scfg, $storeid, $client_cmd, $param, %opts) = @_;
-    return do_raw_client_cmd($scfg, $storeid, $client_cmd, $param, %opts);
-}
-
-sub run_client_cmd {
-    my ($scfg, $storeid, $client_cmd, $param, $no_output) = @_;
-
-    my $json_str = '';
-    my $outfunc = sub { $json_str .= "$_[0]\n" };
-
-    $param = [] if !defined($param);
-    $param = [ $param ] if !ref($param);
-
-    $param = [@$param, '--output-format=json'] if !$no_output;
-
-    do_raw_client_cmd($scfg, $storeid, $client_cmd, $param,
-                     outfunc => $outfunc, errmsg => 'proxmox-backup-client failed');
-
-    return undef if $no_output;
-
-    my $res = decode_json($json_str);
-
-    return $res;
-}
-
-# Storage implementation
-
-sub extract_vzdump_config {
-    my ($class, $scfg, $volname, $storeid) = @_;
-
-    my ($vtype, $name, $vmid, undef, undef, undef, $format) = $class->parse_volname($volname);
-
-    my $config = '';
-    my $outfunc = sub { $config .= "$_[0]\n" };
-
-    my $config_name;
-    if ($format eq 'pbs-vm') {
-       $config_name = 'qemu-server.conf';
-    } elsif  ($format eq 'pbs-ct') {
-       $config_name = 'pct.conf';
-    } else {
-       die "unable to extract configuration for backup format '$format'\n";
-    }
-
-    do_raw_client_cmd($scfg, $storeid, 'restore', [ $name, $config_name, '-' ],
-                     outfunc => $outfunc, errmsg => 'proxmox-backup-client failed');
-
-    return $config;
-}
-
-sub prune_backups {
-    my ($class, $scfg, $storeid, $keep, $vmid, $type, $dryrun, $logfunc) = @_;
-
-    $logfunc //= sub { print "$_[1]\n" };
-
-    $type = 'vm' if defined($type) && $type eq 'qemu';
-    $type = 'ct' if defined($type) && $type eq 'lxc';
-
-    my $backup_groups = {};
-
-    if (defined($vmid) && defined($type)) {
-       # no need to get the list of volumes, we only got a single backup group anyway
-       $backup_groups->{"$type/$vmid"} = 1;
-    } else {
-       my $backups = eval { $class->list_volumes($storeid, $scfg, $vmid, ['backup']) };
-       die "failed to get list of all backups to prune - $@" if $@;
-
-       foreach my $backup (@{$backups}) {
-           (my $backup_type = $backup->{format}) =~ s/^pbs-//;
-           next if defined($type) && $backup_type ne $type;
-
-           my $backup_group = "$backup_type/$backup->{vmid}";
-           $backup_groups->{$backup_group} = 1;
-       }
-    }
-
-    my @param;
-
-    my $keep_all = delete $keep->{'keep-all'};
-
-    if (!$keep_all) {
-       foreach my $opt (keys %{$keep}) {
-           next if $keep->{$opt} == 0;
-           push @param, "--$opt";
-           push @param, "$keep->{$opt}";
-       }
-    } else { # no need to pass anything to PBS
-       $keep = { 'keep-all' => 1 };
-    }
-
-    push @param, '--dry-run' if $dryrun;
-
-    my $prune_list = [];
-    my $failed;
-
-    foreach my $backup_group (keys %{$backup_groups}) {
-       $logfunc->('info', "running 'proxmox-backup-client prune' for '$backup_group'")
-           if !$dryrun;
-       eval {
-           my $res = run_client_cmd($scfg, $storeid, 'prune', [ $backup_group, @param ]);
-
-           foreach my $backup (@{$res}) {
-               die "result from proxmox-backup-client is not as expected\n"
-                   if !defined($backup->{'backup-time'})
-                   || !defined($backup->{'backup-type'})
-                   || !defined($backup->{'backup-id'})
-                   || !defined($backup->{'keep'});
-
-               my $ctime = $backup->{'backup-time'};
-               my $type = $backup->{'backup-type'};
-               my $vmid = $backup->{'backup-id'};
-               my $volid = print_volid($storeid, $type, $vmid, $ctime);
-
-               my $mark = $backup->{keep} ? 'keep' : 'remove';
-               $mark = 'protected' if $backup->{protected};
-
-               push @{$prune_list}, {
-                   ctime => $ctime,
-                   mark => $mark,
-                   type => $type eq 'vm' ? 'qemu' : 'lxc',
-                   vmid => $vmid,
-                   volid => $volid,
-               };
-           }
-       };
-       if (my $err = $@) {
-           $logfunc->('err', "prune '$backup_group': $err\n");
-           $failed = 1;
-       }
-    }
-    die "error pruning backups - check log\n" if $failed;
-
-    return $prune_list;
-}
-
-my $autogen_encryption_key = sub {
-    my ($scfg, $storeid) = @_;
-    my $encfile = pbs_encryption_key_file_name($scfg, $storeid);
-    if (-f $encfile) {
-       rename $encfile, "$encfile.old";
-    }
-    my $cmd = ['proxmox-backup-client', 'key', 'create', '--kdf', 'none', $encfile];
-    run_command($cmd, errmsg => 'failed to create encryption key');
-    return PVE::Tools::file_get_contents($encfile);
-};
-
-sub on_add_hook {
-    my ($class, $storeid, $scfg, %param) = @_;
-
-    my $res = {};
-
-    if (defined(my $password = $param{password})) {
-       pbs_set_password($scfg, $storeid, $password);
-    } else {
-       pbs_delete_password($scfg, $storeid);
-    }
-
-    if (defined(my $encryption_key = $param{'encryption-key'})) {
-       my $decoded_key;
-       if ($encryption_key eq 'autogen') {
-           $res->{'encryption-key'} = $autogen_encryption_key->($scfg, $storeid);
-           $decoded_key = decode_json($res->{'encryption-key'});
-       } else {
-           $decoded_key = eval { decode_json($encryption_key) };
-           if ($@ || !exists($decoded_key->{data})) {
-               die "Value does not seems like a valid, JSON formatted encryption key!\n";
-           }
-           pbs_set_encryption_key($scfg, $storeid, $encryption_key);
-           $res->{'encryption-key'} = $encryption_key;
-       }
-       $scfg->{'encryption-key'} = $decoded_key->{fingerprint} || 1;
-    } else {
-       pbs_delete_encryption_key($scfg, $storeid);
-    }
-
-    if (defined(my $master_key = delete $param{'master-pubkey'})) {
-       die "'master-pubkey' can only be used together with 'encryption-key'\n"
-           if !defined($scfg->{'encryption-key'});
-
-       my $decoded = decode_base64($master_key);
-       pbs_set_master_pubkey($scfg, $storeid, $decoded);
-       $scfg->{'master-pubkey'} = 1;
-    } else {
-       pbs_delete_master_pubkey($scfg, $storeid);
-    }
-
-    return $res;
-}
-
-sub on_update_hook {
-    my ($class, $storeid, $scfg, %param) = @_;
-
-    my $res = {};
-
-    if (exists($param{password})) {
-       if (defined($param{password})) {
-           pbs_set_password($scfg, $storeid, $param{password});
-       } else {
-           pbs_delete_password($scfg, $storeid);
-       }
-    }
-
-    if (exists($param{'encryption-key'})) {
-       if (defined(my $encryption_key = delete($param{'encryption-key'}))) {
-           my $decoded_key;
-           if ($encryption_key eq 'autogen') {
-               $res->{'encryption-key'} = $autogen_encryption_key->($scfg, $storeid);
-               $decoded_key = decode_json($res->{'encryption-key'});
-           } else {
-               $decoded_key = eval { decode_json($encryption_key) };
-               if ($@ || !exists($decoded_key->{data})) {
-                   die "Value does not seems like a valid, JSON formatted encryption key!\n";
-               }
-               pbs_set_encryption_key($scfg, $storeid, $encryption_key);
-               $res->{'encryption-key'} = $encryption_key;
-           }
-           $scfg->{'encryption-key'} = $decoded_key->{fingerprint} || 1;
-       } else {
-           pbs_delete_encryption_key($scfg, $storeid);
-           delete $scfg->{'encryption-key'};
-       }
-    }
-
-    if (exists($param{'master-pubkey'})) {
-       if (defined(my $master_key = delete($param{'master-pubkey'}))) {
-           my $decoded = decode_base64($master_key);
-
-           pbs_set_master_pubkey($scfg, $storeid, $decoded);
-           $scfg->{'master-pubkey'} = 1;
-       } else {
-           pbs_delete_master_pubkey($scfg, $storeid);
-       }
-    }
-
-    return $res;
-}
-
-sub on_delete_hook {
-    my ($class, $storeid, $scfg) = @_;
-
-    pbs_delete_password($scfg, $storeid);
-    pbs_delete_encryption_key($scfg, $storeid);
-    pbs_delete_master_pubkey($scfg, $storeid);
-
-    return;
-}
-
-sub parse_volname {
-    my ($class, $volname) = @_;
-
-    if ($volname =~ m!^backup/([^\s_]+)/([^\s_]+)/([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z)$!) {
-       my $btype = $1;
-       my $bid = $2;
-       my $btime = $3;
-       my $format = "pbs-$btype";
-
-       my $name = "$btype/$bid/$btime";
-
-       if ($bid =~ m/^\d+$/) {
-           return ('backup', $name, $bid, undef, undef, undef, $format);
-       } else {
-           return ('backup', $name, undef, undef, undef, undef, $format);
-       }
-    }
-
-    die "unable to parse PBS volume name '$volname'\n";
-}
-
-sub path {
-    my ($class, $scfg, $volname, $storeid, $snapname) = @_;
-
-    die "volume snapshot is not possible on pbs storage"
-       if defined($snapname);
-
-    my ($vtype, $name, $vmid) = $class->parse_volname($volname);
-
-    my $repo = PVE::PBSClient::get_repository($scfg);
-
-    # artificial url - we currently do not use that anywhere
-    my $path = "pbs://$repo/$name";
-    if (defined(my $ns = $scfg->{namespace})) {
-       $ns =~ s|/|%2f|g; # other characters to escape aren't allowed in the namespace schema
-       $path .= "?ns=$ns";
-    }
-
-    return ($path, $vmid, $vtype);
-}
-
-sub create_base {
-    my ($class, $storeid, $scfg, $volname) = @_;
-
-    die "can't create base images in pbs storage\n";
-}
-
-sub clone_image {
-    my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
-
-    die "can't clone images in pbs storage\n";
-}
-
-sub alloc_image {
-    my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
-
-    die "can't allocate space in pbs storage\n";
-}
-
-sub free_image {
-    my ($class, $storeid, $scfg, $volname, $isBase) = @_;
-
-    my ($vtype, $name, $vmid) = $class->parse_volname($volname);
-
-    run_client_cmd($scfg, $storeid, "forget", [ $name ], 1);
-
-    return;
-}
-
-
-sub list_images {
-    my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
-
-    my $res = [];
-
-    return $res;
-}
-
-my sub snapshot_files_encrypted {
-    my ($files) = @_;
-    return 0 if !$files;
-
-    my $any;
-    my $all = 1;
-    for my $file (@$files) {
-       my $fn = $file->{filename};
-       next if $fn eq 'client.log.blob' || $fn eq 'index.json.blob';
-
-       my $crypt = $file->{'crypt-mode'};
-
-       $all = 0 if !$crypt || $crypt ne 'encrypt';
-       $any ||= defined($crypt) && $crypt eq 'encrypt';
-    }
-    return $any && $all;
-}
-
-# TODO: use a client with native rust/proxmox-backup bindings to profit from
-# API schema checks and types
-my sub pbs_api_connect {
-    my ($scfg, $password, $timeout) = @_;
-
-    my $params = {};
-
-    my $user = $scfg->{username} // 'root@pam';
-
-    if (my $tokenid = PVE::AccessControl::pve_verify_tokenid($user, 1)) {
-       $params->{apitoken} = "PBSAPIToken=${tokenid}:${password}";
-    } else {
-       $params->{password} = $password;
-       $params->{username} = $user;
-    }
-
-    if (my $fp = $scfg->{fingerprint}) {
-       $params->{cached_fingerprints}->{uc($fp)} = 1;
-    }
-
-    my $conn = PVE::APIClient::LWP->new(
-       %$params,
-       host => $scfg->{server},
-       port => $scfg->{port} // 8007,
-       timeout => ($timeout // 7), # cope with a 401 (3s api delay) and high latency
-       cookie_name => 'PBSAuthCookie',
-    );
-
-    return $conn;
-}
-
-sub list_volumes {
-    my ($class, $storeid, $scfg, $vmid, $content_types) = @_;
-
-    my $res = [];
-
-    return $res if !grep { $_ eq 'backup' } @$content_types;
-
-    my $password = pbs_get_password($scfg, $storeid);
-    my $conn = pbs_api_connect($scfg, $password, 120);
-    my $datastore = $scfg->{datastore};
-
-    my $param = {};
-    $param->{'backup-id'} = "$vmid" if defined($vmid);
-    $param->{'ns'} = "$scfg->{namespace}" if defined($scfg->{namespace});
-    my $data = eval { $conn->get("/api2/json/admin/datastore/$datastore/snapshots", $param); };
-    die "error listing snapshots - $@" if $@;
-
-    foreach my $item (@$data) {
-       my $btype = $item->{"backup-type"};
-       my $bid = $item->{"backup-id"};
-       my $epoch = $item->{"backup-time"};
-       my $size = $item->{size} // 1;
-
-       next if !($btype eq 'vm' || $btype eq 'ct');
-       next if $bid !~ m/^\d+$/;
-       next if defined($vmid) && $bid ne $vmid;
-
-       my $volid = print_volid($storeid, $btype, $bid, $epoch);
-
-       my $info = {
-           volid => $volid,
-           format => "pbs-$btype",
-           size => $size,
-           content => 'backup',
-           vmid => int($bid),
-           ctime => $epoch,
-           subtype => $btype eq 'vm' ? 'qemu' : 'lxc', # convert to PVE backup type
-       };
-
-       $info->{verification} = $item->{verification} if defined($item->{verification});
-       $info->{notes} = $item->{comment} if defined($item->{comment});
-       $info->{protected} = 1 if $item->{protected};
-       if (defined($item->{fingerprint})) {
-           $info->{encrypted} = $item->{fingerprint};
-       } elsif (snapshot_files_encrypted($item->{files})) {
-           $info->{encrypted} = '1';
-       }
-
-       push @$res, $info;
-    }
-
-    return $res;
-}
-
-sub status {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    my $total = 0;
-    my $free = 0;
-    my $used = 0;
-    my $active = 0;
-
-    eval {
-       my $res = run_client_cmd($scfg, $storeid, "status");
-
-       $active = 1;
-       $total = $res->{total};
-       $used = $res->{used};
-       $free = $res->{avail};
-    };
-    if (my $err = $@) {
-       warn $err;
-    }
-
-    return ($total, $free, $used, $active);
-}
-
-# can also be used for not (yet) added storages, pass $scfg with
-# {
-#   server
-#   user
-#   port          (optional default to 8007)
-#   fingerprint   (optional for trusted certs)
-# }
-sub scan_datastores {
-    my ($scfg, $password) = @_;
-
-    my $conn = pbs_api_connect($scfg, $password);
-
-    my $response = eval { $conn->get('/api2/json/admin/datastore', {}) };
-    die "error fetching datastores - $@" if $@;
-
-    return $response;
-}
-
-sub activate_storage {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    my $password = pbs_get_password($scfg, $storeid);
-
-    my $datastores = eval { scan_datastores($scfg, $password) };
-    die "$storeid: $@" if $@;
-
-    my $datastore = $scfg->{datastore};
-
-    for my $ds (@$datastores) {
-       if ($ds->{store} eq $datastore) {
-           return 1;
-       }
-    }
-
-    die "$storeid: Cannot find datastore '$datastore', check permissions and existence!\n";
-}
-
-sub deactivate_storage {
-    my ($class, $storeid, $scfg, $cache) = @_;
-    return 1;
-}
-
-sub activate_volume {
-    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
-
-    die "volume snapshot is not possible on pbs device" if $snapname;
-
-    return 1;
-}
-
-sub deactivate_volume {
-    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
-
-    die "volume snapshot is not possible on pbs device" if $snapname;
-
-    return 1;
-}
-
-# FIXME remove on the next APIAGE reset.
-# Deprecated, use get_volume_attribute instead.
-sub get_volume_notes {
-    my ($class, $scfg, $storeid, $volname, $timeout) = @_;
-
-    my (undef, $name,  undef, undef, undef, undef, $format) = $class->parse_volname($volname);
-
-    my $data = run_client_cmd($scfg, $storeid, "snapshot", [ "notes", "show", $name ]);
-
-    return $data->{notes};
-}
-
-# FIXME remove on the next APIAGE reset.
-# Deprecated, use update_volume_attribute instead.
-sub update_volume_notes {
-    my ($class, $scfg, $storeid, $volname, $notes, $timeout) = @_;
-
-    my (undef, $name,  undef, undef, undef, undef, $format) = $class->parse_volname($volname);
-
-    run_client_cmd($scfg, $storeid, "snapshot", [ "notes", "update", $name, $notes ], 1);
-
-    return undef;
-}
-
-sub get_volume_attribute {
-    my ($class, $scfg, $storeid, $volname, $attribute) = @_;
-
-    if ($attribute eq 'notes') {
-       return $class->get_volume_notes($scfg, $storeid, $volname);
-    }
-
-    if ($attribute eq 'protected') {
-       my $param = api_param_from_volname($class, $scfg, $volname);
-
-       my $password = pbs_get_password($scfg, $storeid);
-       my $conn = pbs_api_connect($scfg, $password);
-       my $datastore = $scfg->{datastore};
-
-       my $res = eval { $conn->get("/api2/json/admin/datastore/$datastore/$attribute", $param); };
-       if (my $err = $@) {
-           return if $err->{code} == 404; # not supported
-           die $err;
-       }
-       return $res;
-    }
-
-    return;
-}
-
-sub update_volume_attribute {
-    my ($class, $scfg, $storeid, $volname, $attribute, $value) = @_;
-
-    if ($attribute eq 'notes') {
-       return $class->update_volume_notes($scfg, $storeid, $volname, $value);
-    }
-
-    if ($attribute eq 'protected') {
-       my $param = api_param_from_volname($class, $scfg, $volname);
-       $param->{$attribute} = $value;
-
-       my $password = pbs_get_password($scfg, $storeid);
-       my $conn = pbs_api_connect($scfg, $password);
-       my $datastore = $scfg->{datastore};
-
-       eval { $conn->put("/api2/json/admin/datastore/$datastore/$attribute", $param); };
-       if (my $err = $@) {
-           die "Server is not recent enough to support feature '$attribute'\n"
-               if $err->{code} == 404;
-           die $err;
-       }
-       return;
-    }
-
-    die "attribute '$attribute' is not supported for storage type '$scfg->{type}'\n";
-}
-
-sub volume_size_info {
-    my ($class, $scfg, $storeid, $volname, $timeout) = @_;
-
-    my ($vtype, $name,  undef, undef, undef, undef, $format) = $class->parse_volname($volname);
-
-    my $data = run_client_cmd($scfg, $storeid, "files", [ $name ]);
-
-    my $size = 0;
-    foreach my $info (@$data) {
-       if ($info->{size} && $info->{size} =~ /^(\d+)$/) { # untaints
-           $size += $1;
-       }
-    }
-
-    my $used = $size;
-
-    return wantarray ? ($size, $format, $used, undef) : $size;
-}
-
-sub volume_resize {
-    my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
-    die "volume resize is not possible on pbs device";
-}
-
-sub volume_snapshot {
-    my ($class, $scfg, $storeid, $volname, $snap) = @_;
-    die "volume snapshot is not possible on pbs device";
-}
-
-sub volume_snapshot_rollback {
-    my ($class, $scfg, $storeid, $volname, $snap) = @_;
-    die "volume snapshot rollback is not possible on pbs device";
-}
-
-sub volume_snapshot_delete {
-    my ($class, $scfg, $storeid, $volname, $snap) = @_;
-    die "volume snapshot delete is not possible on pbs device";
-}
-
-sub volume_has_feature {
-    my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
-
-    return undef;
-}
-
-1;
diff --git a/PVE/Storage/Plugin.pm b/PVE/Storage/Plugin.pm
deleted file mode 100644 (file)
index c323085..0000000
+++ /dev/null
@@ -1,1701 +0,0 @@
-package PVE::Storage::Plugin;
-
-use strict;
-use warnings;
-
-use Encode qw(decode);
-use Fcntl ':mode';
-use File::chdir;
-use File::Path;
-use File::Basename;
-use File::stat qw();
-
-use PVE::Tools qw(run_command);
-use PVE::JSONSchema qw(get_standard_option register_standard_option);
-use PVE::Cluster qw(cfs_register_file);
-
-use JSON;
-
-use base qw(PVE::SectionConfig);
-
-use constant COMPRESSOR_RE => 'gz|lzo|zst';
-
-use constant LOG_EXT => ".log";
-use constant NOTES_EXT => ".notes";
-
-our @COMMON_TAR_FLAGS = qw(
-    --one-file-system
-    -p --sparse --numeric-owner --acls
-    --xattrs --xattrs-include=user.* --xattrs-include=security.capability
-    --warning=no-file-ignored --warning=no-xattr-write
-);
-
-our @SHARED_STORAGE = (
-    'iscsi',
-    'nfs',
-    'cifs',
-    'rbd',
-    'cephfs',
-    'iscsidirect',
-    'glusterfs',
-    'zfs',
-    'drbd',
-    'pbs',
-);
-
-our $QCOW2_PREALLOCATION = {
-    off => 1,
-    metadata => 1,
-    falloc => 1,
-    full => 1,
-};
-
-our $RAW_PREALLOCATION = {
-    off => 1,
-    falloc => 1,
-    full => 1,
-};
-
-our $MAX_VOLUMES_PER_GUEST = 1024;
-
-cfs_register_file ('storage.cfg',
-                  sub { __PACKAGE__->parse_config(@_); },
-                  sub { __PACKAGE__->write_config(@_); });
-
-my %prune_option = (
-    optional => 1,
-    type => 'integer', minimum => '0',
-    format_description => 'N',
-);
-
-our $prune_backups_format = {
-    'keep-all' => {
-       type => 'boolean',
-       description => 'Keep all backups. Conflicts with the other options when true.',
-       optional => 1,
-    },
-    'keep-last' => {
-       %prune_option,
-       description => 'Keep the last <N> backups.',
-    },
-    'keep-hourly' => {
-       %prune_option,
-       description => 'Keep backups for the last <N> different hours. If there is more' .
-                      'than one backup for a single hour, only the latest one is kept.'
-    },
-    'keep-daily' => {
-       %prune_option,
-       description => 'Keep backups for the last <N> different days. If there is more' .
-                      'than one backup for a single day, only the latest one is kept.'
-    },
-    'keep-weekly' => {
-       %prune_option,
-       description => 'Keep backups for the last <N> different weeks. If there is more' .
-                      'than one backup for a single week, only the latest one is kept.'
-    },
-    'keep-monthly' => {
-       %prune_option,
-       description => 'Keep backups for the last <N> different months. If there is more' .
-                      'than one backup for a single month, only the latest one is kept.'
-    },
-    'keep-yearly' => {
-       %prune_option,
-       description => 'Keep backups for the last <N> different years. If there is more' .
-                      'than one backup for a single year, only the latest one is kept.'
-    },
-};
-PVE::JSONSchema::register_format('prune-backups', $prune_backups_format, \&validate_prune_backups);
-sub validate_prune_backups {
-    my ($prune_backups) = @_;
-
-    my $res = { $prune_backups->%* };
-
-    my $keep_all = delete $res->{'keep-all'};
-
-    if (scalar(grep { $_ > 0 } values %{$res}) == 0) {
-       $res = { 'keep-all' => 1 };
-    } elsif ($keep_all) {
-       die "keep-all cannot be set together with other options.\n";
-    }
-
-    return $res;
-}
-register_standard_option('prune-backups', {
-    description => "The retention options with shorter intervals are processed first " .
-                  "with --keep-last being the very first one. Each option covers a " .
-                  "specific period of time. We say that backups within this period " .
-                  "are covered by this option. The next option does not take care " .
-                  "of already covered backups and only considers older backups.",
-    optional => 1,
-    type => 'string',
-    format => 'prune-backups',
-});
-
-my $defaultData = {
-    propertyList => {
-       type => { description => "Storage type." },
-       storage => get_standard_option('pve-storage-id',
-           { completion => \&PVE::Storage::complete_storage }),
-       nodes => get_standard_option('pve-node-list', { optional => 1 }),
-       content => {
-           description => "Allowed content types.\n\nNOTE: the value " .
-               "'rootdir' is used for Containers, and value 'images' for VMs.\n",
-           type => 'string', format => 'pve-storage-content-list',
-           optional => 1,
-           completion => \&PVE::Storage::complete_content_type,
-       },
-       disable => {
-           description => "Flag to disable the storage.",
-           type => 'boolean',
-           optional => 1,
-       },
-       maxfiles => {
-           description => "Deprecated: use 'prune-backups' instead. " .
-               "Maximal number of backup files per VM. Use '0' for unlimited.",
-           type => 'integer',
-           minimum => 0,
-           optional => 1,
-       },
-       'prune-backups' => get_standard_option('prune-backups'),
-       'max-protected-backups' => {
-           description => "Maximal number of protected backups per guest. Use '-1' for unlimited.",
-           type => 'integer',
-           minimum => -1,
-           optional => 1,
-           default => "Unlimited for users with Datastore.Allocate privilege, 5 for other users",
-       },
-       shared => {
-           description => "Mark storage as shared.",
-           type => 'boolean',
-           optional => 1,
-       },
-       subdir => {
-           description => "Subdir to mount.",
-           type => 'string', format => 'pve-storage-path',
-           optional => 1,
-       },
-       'format' => {
-           description => "Default image format.",
-           type => 'string', format => 'pve-storage-format',
-           optional => 1,
-       },
-       preallocation => {
-           description => "Preallocation mode for raw and qcow2 images. " .
-               "Using 'metadata' on raw images results in preallocation=off.",
-           type => 'string', enum => ['off', 'metadata', 'falloc', 'full'],
-           default => 'metadata',
-           optional => 1,
-       },
-       'content-dirs' => {
-           description => "Overrides for default content type directories.",
-           type => "string", format => "pve-dir-override-list",
-           optional => 1,
-       },
-    },
-};
-
-sub content_hash_to_string {
-    my $hash = shift;
-
-    my @cta;
-    foreach my $ct (keys %$hash) {
-       push @cta, $ct if $hash->{$ct};
-    }
-
-    return join(',', @cta);
-}
-
-sub valid_content_types {
-    my ($type) = @_;
-
-    my $def = $defaultData->{plugindata}->{$type};
-
-    return {} if !$def;
-
-    return $def->{content}->[0];
-}
-
-sub dirs_hash_to_string {
-    my $hash = shift;
-
-    return join(',', map { "$_=$hash->{$_}" } sort keys %$hash);
-}
-
-sub default_format {
-    my ($scfg) = @_;
-
-    my $type = $scfg->{type};
-    my $def = $defaultData->{plugindata}->{$type};
-
-    my $def_format = 'raw';
-    my $valid_formats = [ $def_format ];
-
-    if (defined($def->{format})) {
-       $def_format = $scfg->{format} || $def->{format}->[1];
-       $valid_formats = [ sort keys %{$def->{format}->[0]} ];
-    }
-
-    return wantarray ? ($def_format, $valid_formats) : $def_format;
-}
-
-PVE::JSONSchema::register_format('pve-storage-path', \&verify_path);
-sub verify_path {
-    my ($path, $noerr) = @_;
-
-    # fixme: exclude more shell meta characters?
-    # we need absolute paths
-    if ($path !~ m|^/[^;\(\)]+|) {
-       return undef if $noerr;
-       die "value does not look like a valid absolute path\n";
-    }
-    return $path;
-}
-
-PVE::JSONSchema::register_format('pve-storage-server', \&verify_server);
-sub verify_server {
-    my ($server, $noerr) = @_;
-
-    if (!(PVE::JSONSchema::pve_verify_ip($server, 1) ||
-          PVE::JSONSchema::pve_verify_dns_name($server, 1)))
-    {
-       return undef if $noerr;
-       die "value does not look like a valid server name or IP address\n";
-    }
-    return $server;
-}
-
-PVE::JSONSchema::register_format('pve-storage-vgname', \&parse_lvm_name);
-sub parse_lvm_name {
-    my ($name, $noerr) = @_;
-
-    if ($name !~ m/^[a-z0-9][a-z0-9\-\_\.]*[a-z0-9]$/i) {
-       return undef if $noerr;
-       die "lvm name '$name' contains illegal characters\n";
-    }
-
-    return $name;
-}
-
-# fixme: do we need this
-#PVE::JSONSchema::register_format('pve-storage-portal', \&verify_portal);
-#sub verify_portal {
-#    my ($portal, $noerr) = @_;
-#
-#    # IP with optional port
-#    if ($portal !~ m/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d+)?$/) {
-#      return undef if $noerr;
-#      die "value does not look like a valid portal address\n";
-#    }
-#    return $portal;
-#}
-
-PVE::JSONSchema::register_format('pve-storage-portal-dns', \&verify_portal_dns);
-sub verify_portal_dns {
-    my ($portal, $noerr) = @_;
-
-    # IP or DNS name with optional port
-    if (!PVE::Tools::parse_host_and_port($portal)) {
-       return undef if $noerr;
-       die "value does not look like a valid portal address\n";
-    }
-    return $portal;
-}
-
-PVE::JSONSchema::register_format('pve-storage-content', \&verify_content);
-sub verify_content {
-    my ($ct, $noerr) = @_;
-
-    my $valid_content = valid_content_types('dir'); # dir includes all types
-
-    if (!$valid_content->{$ct}) {
-       return undef if $noerr;
-       die "invalid content type '$ct'\n";
-    }
-
-    return $ct;
-}
-
-PVE::JSONSchema::register_format('pve-storage-format', \&verify_format);
-sub verify_format {
-    my ($fmt, $noerr) = @_;
-
-    if ($fmt !~ m/(raw|qcow2|vmdk|subvol)/) {
-       return undef if $noerr;
-       die "invalid format '$fmt'\n";
-    }
-
-    return $fmt;
-}
-
-PVE::JSONSchema::register_format('pve-storage-options', \&verify_options);
-sub verify_options {
-    my ($value, $noerr) = @_;
-
-    # mount options (see man fstab)
-    if ($value !~ m/^\S+$/) {
-       return undef if $noerr;
-       die "invalid options '$value'\n";
-    }
-
-    return $value;
-}
-
-PVE::JSONSchema::register_format('pve-volume-id', \&parse_volume_id);
-sub parse_volume_id {
-    my ($volid, $noerr) = @_;
-
-    if ($volid =~ m/^([a-z][a-z0-9\-\_\.]*[a-z0-9]):(.+)$/i) {
-       return wantarray ? ($1, $2) : $1;
-    }
-    return undef if $noerr;
-    die "unable to parse volume ID '$volid'\n";
-}
-
-PVE::JSONSchema::register_format('pve-dir-override', \&verify_dir_override);
-sub verify_dir_override {
-    my ($value, $noerr) = @_;
-
-    if ($value =~ m/^([a-z]+)=([^.]*(?:\.?[^.]+)+)$/) {
-       my ($content_type, $relative_path) = ($1, $2);
-       if (verify_content($content_type, $noerr)) {
-           # linux has 4k max-path, but limit total length to lower as its concat'd for full path
-           if (length($relative_path) < 1023 && !(grep { length($_) >= 255 } split('/', $relative_path))) {
-               return $value;
-           }
-       }
-    }
-
-    return undef if $noerr;
-    die "invalid override '$value'\n";
-}
-
-sub private {
-    return $defaultData;
-}
-
-sub parse_section_header {
-    my ($class, $line) = @_;
-
-    if ($line =~ m/^(\S+):\s*(\S+)\s*$/) {
-       my ($type, $storeid) = (lc($1), $2);
-       my $errmsg = undef; # set if you want to skip whole section
-       eval { PVE::JSONSchema::parse_storage_id($storeid); };
-       $errmsg = $@ if $@;
-       my $config = {}; # to return additional attributes
-       return ($type, $storeid, $errmsg, $config);
-    }
-    return undef;
-}
-
-sub decode_value {
-    my ($class, $type, $key, $value) = @_;
-
-    my $def = $defaultData->{plugindata}->{$type};
-
-    if ($key eq 'content') {
-       my $valid_content = $def->{content}->[0];
-
-       my $res = {};
-
-       foreach my $c (PVE::Tools::split_list($value)) {
-           if (!$valid_content->{$c}) {
-               warn "storage does not support content type '$c'\n";
-               next;
-           }
-           $res->{$c} = 1;
-       }
-
-       if ($res->{none} && scalar (keys %$res) > 1) {
-           die "unable to combine 'none' with other content types\n";
-       }
-
-       if (scalar(keys $res->%*) == 0 && !$valid_content->{none}) {
-           die "storage does not support content type 'none'\n";
-       }
-
-       return $res;
-    } elsif ($key eq 'format') {
-       my $valid_formats = $def->{format}->[0];
-
-       if (!$valid_formats->{$value}) {
-           warn "storage does not support format '$value'\n";
-           next;
-       }
-
-       return $value;
-    } elsif ($key eq 'nodes') {
-       my $res = {};
-
-       foreach my $node (PVE::Tools::split_list($value)) {
-           if (PVE::JSONSchema::pve_verify_node_name($node)) {
-               $res->{$node} = 1;
-           }
-       }
-
-       # fixme:
-       # no node restrictions for local storage
-       #if ($storeid && $storeid eq 'local' && scalar(keys(%$res))) {
-       #    die "storage '$storeid' does not allow node restrictions\n";
-       #}
-
-       return $res;
-    } elsif ($key eq 'content-dirs') {
-       my $valid_content = $def->{content}->[0];
-       my $res = {};
-
-       foreach my $dir (PVE::Tools::split_list($value)) {
-           my ($content, $path) = split(/=/, $dir, 2);
-
-           if (!$valid_content->{$content}) {
-               warn "storage does not support content type '$content'\n";
-               next;
-           }
-
-           $res->{$content} = $path;
-       }
-
-       return $res;
-    }
-
-    return $value;
-}
-
-sub encode_value {
-    my ($class, $type, $key, $value) = @_;
-
-    if ($key eq 'nodes') {
-        return join(',', keys(%$value));
-    } elsif ($key eq 'content') {
-       my $res = content_hash_to_string($value) || 'none';
-       return $res;
-    } elsif ($key eq 'content-dirs') {
-       my $res = dirs_hash_to_string($value);
-       return $res;
-    }
-
-    return $value;
-}
-
-sub parse_config {
-    my ($class, $filename, $raw) = @_;
-
-    my $cfg = $class->SUPER::parse_config($filename, $raw);
-    my $ids = $cfg->{ids};
-
-    # make sure we have a reasonable 'local:' storage
-    # we want 'local' to be always the same 'type' (on all cluster nodes)
-    if (!$ids->{local} || $ids->{local}->{type} ne 'dir' ||
-       ($ids->{local}->{path} && $ids->{local}->{path} ne '/var/lib/vz')) {
-       $ids->{local} = {
-           type => 'dir',
-           priority => 0, # force first entry
-           path => '/var/lib/vz',
-           'prune-backups' => 'keep-all=1',
-           content => {
-               backup => 1,
-               images => 1,
-               iso => 1,
-               rootdir => 1,
-               snippets => 1,
-               vztmpl => 1,
-           },
-       };
-    }
-
-    # make sure we have a path
-    $ids->{local}->{path} = '/var/lib/vz' if !$ids->{local}->{path};
-
-    # remove node restrictions for local storage
-    delete($ids->{local}->{nodes});
-
-    foreach my $storeid (keys %$ids) {
-       my $d = $ids->{$storeid};
-       my $type = $d->{type};
-
-       my $def = $defaultData->{plugindata}->{$type};
-
-       if ($def->{content}) {
-           $d->{content} = $def->{content}->[1] if !$d->{content};
-       }
-       if (grep { $_ eq $type }  @SHARED_STORAGE) {
-           $d->{shared} = 1;
-       }
-    }
-
-    return $cfg;
-}
-
-sub preallocation_cmd_option {
-    my ($scfg, $fmt) = @_;
-
-    my $prealloc = $scfg->{preallocation};
-
-    if ($fmt eq 'qcow2') {
-       $prealloc = $prealloc // 'metadata';
-
-       die "preallocation mode '$prealloc' not supported by format '$fmt'\n"
-           if !$QCOW2_PREALLOCATION->{$prealloc};
-
-       return "preallocation=$prealloc";
-    } elsif ($fmt eq 'raw') {
-       $prealloc = $prealloc // 'off';
-       $prealloc = 'off' if $prealloc eq 'metadata';
-
-       die "preallocation mode '$prealloc' not supported by format '$fmt'\n"
-           if !$RAW_PREALLOCATION->{$prealloc};
-
-       return "preallocation=$prealloc";
-    }
-
-    return;
-}
-
-# Storage implementation
-
-# called during addition of storage (before the new storage config got written)
-# die to abort addition if there are (grave) problems
-# NOTE: runs in a storage config *locked* context
-sub on_add_hook {
-    my ($class, $storeid, $scfg, %param) = @_;
-
-    # do nothing by default
-    return undef;
-}
-
-# called during storage configuration update (before the updated storage config got written)
-# die to abort the update if there are (grave) problems
-# NOTE: runs in a storage config *locked* context
-sub on_update_hook {
-    my ($class, $storeid, $scfg, %param) = @_;
-
-    # do nothing by default
-    return undef;
-}
-
-# called during deletion of storage (before the new storage config got written)
-# and if the activate check on addition fails, to cleanup all storage traces
-# which on_add_hook may have created.
-# die to abort deletion if there are (very grave) problems
-# NOTE: runs in a storage config *locked* context
-sub on_delete_hook {
-    my ($class, $storeid, $scfg) = @_;
-
-    # do nothing by default
-    return undef;
-}
-
-sub cluster_lock_storage {
-    my ($class, $storeid, $shared, $timeout, $func, @param) = @_;
-
-    my $res;
-    if (!$shared) {
-       my $lockid = "pve-storage-$storeid";
-       my $lockdir = "/var/lock/pve-manager";
-       mkdir $lockdir;
-       $res = PVE::Tools::lock_file("$lockdir/$lockid", $timeout, $func, @param);
-       die $@ if $@;
-    } else {
-       $res = PVE::Cluster::cfs_lock_storage($storeid, $timeout, $func, @param);
-       die $@ if $@;
-    }
-    return $res;
-}
-
-sub parse_name_dir {
-    my $name = shift;
-
-    if ($name =~ m!^((base-)?[^/\s]+\.(raw|qcow2|vmdk|subvol))$!) {
-       return ($1, $3, $2); # (name, format, isBase)
-    }
-
-    die "unable to parse volume filename '$name'\n";
-}
-
-sub parse_volname {
-    my ($class, $volname) = @_;
-
-    if ($volname =~ m!^(\d+)/(\S+)/(\d+)/(\S+)$!) {
-       my ($basedvmid, $basename) = ($1, $2);
-       parse_name_dir($basename);
-       my ($vmid, $name) = ($3, $4);
-       my (undef, $format, $isBase) = parse_name_dir($name);
-       return ('images', $name, $vmid, $basename, $basedvmid, $isBase, $format);
-    } elsif ($volname =~ m!^(\d+)/(\S+)$!) {
-       my ($vmid, $name) = ($1, $2);
-       my (undef, $format, $isBase) = parse_name_dir($name);
-       return ('images', $name, $vmid, undef, undef, $isBase, $format);
-    } elsif ($volname =~ m!^iso/([^/]+$PVE::Storage::ISO_EXT_RE_0)$!) {
-       return ('iso', $1);
-    } elsif ($volname =~ m!^vztmpl/([^/]+$PVE::Storage::VZTMPL_EXT_RE_1)$!) {
-       return ('vztmpl', $1);
-    } elsif ($volname =~ m!^rootdir/(\d+)$!) {
-       return ('rootdir', $1, $1);
-    } elsif ($volname =~ m!^backup/([^/]+$PVE::Storage::BACKUP_EXT_RE_2)$!) {
-       my $fn = $1;
-       if ($fn =~ m/^vzdump-(openvz|lxc|qemu)-(\d+)-.+/) {
-           return ('backup', $fn, $2);
-       }
-       return ('backup', $fn);
-    } elsif ($volname =~ m!^snippets/([^/]+)$!) {
-       return ('snippets', $1);
-    }
-
-    die "unable to parse directory volume name '$volname'\n";
-}
-
-my $vtype_subdirs = {
-    images => 'images',
-    rootdir => 'private',
-    iso => 'template/iso',
-    vztmpl => 'template/cache',
-    backup => 'dump',
-    snippets => 'snippets',
-};
-
-sub get_vtype_subdirs {
-    return $vtype_subdirs;
-}
-
-sub get_subdir {
-    my ($class, $scfg, $vtype) = @_;
-
-    my $path = $scfg->{path};
-
-    die "storage definition has no path\n" if !$path;
-    die "unknown vtype '$vtype'\n" if !exists($vtype_subdirs->{$vtype});
-
-    my $subdir = $scfg->{"content-dirs"}->{$vtype} // $vtype_subdirs->{$vtype};
-
-    return "$path/$subdir";
-}
-
-sub filesystem_path {
-    my ($class, $scfg, $volname, $snapname) = @_;
-
-    my ($vtype, $name, $vmid, undef, undef, $isBase, $format) =
-       $class->parse_volname($volname);
-
-    # Note: qcow2/qed has internal snapshot, so path is always
-    # the same (with or without snapshot => same file).
-    die "can't snapshot this image format\n"
-       if defined($snapname) && $format !~ m/^(qcow2|qed)$/;
-
-    my $dir = $class->get_subdir($scfg, $vtype);
-
-    $dir .= "/$vmid" if $vtype eq 'images';
-
-    my $path = "$dir/$name";
-
-    return wantarray ? ($path, $vmid, $vtype) : $path;
-}
-
-sub path {
-    my ($class, $scfg, $volname, $storeid, $snapname) = @_;
-
-    return $class->filesystem_path($scfg, $volname, $snapname);
-}
-
-sub create_base {
-    my ($class, $storeid, $scfg, $volname) = @_;
-
-    # this only works for file based storage types
-    die "storage definition has no path\n" if !$scfg->{path};
-
-    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) =
-       $class->parse_volname($volname);
-
-    die "create_base on wrong vtype '$vtype'\n" if $vtype ne 'images';
-
-    die "create_base not possible with base image\n" if $isBase;
-
-    my $path = $class->filesystem_path($scfg, $volname);
-
-    my ($size, undef, $used, $parent) = file_size_info($path);
-    die "file_size_info on '$volname' failed\n" if !($format && defined($size));
-
-    die "volname '$volname' contains wrong information about parent\n"
-       if $basename && (!$parent || $parent ne "../$basevmid/$basename");
-
-    my $newname = $name;
-    $newname =~ s/^vm-/base-/;
-
-    my $newvolname = $basename ? "$basevmid/$basename/$vmid/$newname" :
-       "$vmid/$newname";
-
-    my $newpath = $class->filesystem_path($scfg, $newvolname);
-
-    die "file '$newpath' already exists\n" if -f $newpath;
-
-    rename($path, $newpath) ||
-       die "rename '$path' to '$newpath' failed - $!\n";
-
-    # We try to protect base volume
-
-    chmod(0444, $newpath); # nobody should write anything
-
-    # also try to set immutable flag
-    eval { run_command(['/usr/bin/chattr', '+i', $newpath]); };
-    warn $@ if $@;
-
-    return $newvolname;
-}
-
-my $get_vm_disk_number = sub {
-    my ($disk_name, $scfg, $vmid, $suffix) = @_;
-
-    my $disk_regex = qr/(vm|base)-$vmid-disk-(\d+)$suffix/;
-
-    my $type = $scfg->{type};
-    my $def = { %{$defaultData->{plugindata}->{$type}} };
-
-    my $valid = $def->{format}[0];
-    if ($valid->{subvol}) {
-       $disk_regex = qr/(vm|base|subvol|basevol)-$vmid-disk-(\d+)/;
-    }
-
-    if ($disk_name =~ m/$disk_regex/) {
-       return $2;
-    }
-
-    return undef;
-};
-
-sub get_next_vm_diskname {
-    my ($disk_list, $storeid, $vmid, $fmt, $scfg, $add_fmt_suffix) = @_;
-
-    $fmt //= '';
-    my $prefix = ($fmt eq 'subvol') ? 'subvol' : 'vm';
-    my $suffix = $add_fmt_suffix ? ".$fmt" : '';
-
-    my $disk_ids = {};
-    foreach my $disk (@$disk_list) {
-       my $disknum = $get_vm_disk_number->($disk, $scfg, $vmid, $suffix);
-       $disk_ids->{$disknum} = 1 if defined($disknum);
-    }
-
-    for (my $i = 0; $i < $MAX_VOLUMES_PER_GUEST; $i++) {
-       if (!$disk_ids->{$i}) {
-           return "$prefix-$vmid-disk-$i$suffix";
-       }
-    }
-
-    die "unable to allocate an image name for VM $vmid in storage '$storeid'\n"
-}
-
-sub find_free_diskname {
-    my ($class, $storeid, $scfg, $vmid, $fmt, $add_fmt_suffix) = @_;
-
-    my $disks = $class->list_images($storeid, $scfg, $vmid);
-
-    my $disk_list = [ map { $_->{volid} } @$disks ];
-
-    return get_next_vm_diskname($disk_list, $storeid, $vmid, $fmt, $scfg, $add_fmt_suffix);
-}
-
-sub clone_image {
-    my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
-
-    # this only works for file based storage types
-    die "storage definition has no path\n" if !$scfg->{path};
-
-    my ($vtype, $basename, $basevmid, undef, undef, $isBase, $format) =
-       $class->parse_volname($volname);
-
-    die "clone_image on wrong vtype '$vtype'\n" if $vtype ne 'images';
-
-    die "this storage type does not support clone_image on snapshot\n" if $snap;
-
-    die "this storage type does not support clone_image on subvolumes\n" if $format eq 'subvol';
-
-    die "clone_image only works on base images\n" if !$isBase;
-
-    my $imagedir = $class->get_subdir($scfg, 'images');
-    $imagedir .= "/$vmid";
-
-    mkpath $imagedir;
-
-    my $name = $class->find_free_diskname($storeid, $scfg, $vmid, "qcow2", 1);
-
-    warn "clone $volname: $vtype, $name, $vmid to $name (base=../$basevmid/$basename)\n";
-
-    my $newvol = "$basevmid/$basename/$vmid/$name";
-
-    my $path = $class->filesystem_path($scfg, $newvol);
-
-    # Note: we use relative paths, so we need to call chdir before qemu-img
-    eval {
-       local $CWD = $imagedir;
-
-       my $cmd = ['/usr/bin/qemu-img', 'create', '-b', "../$basevmid/$basename",
-                  '-F', $format, '-f', 'qcow2', $path];
-
-       run_command($cmd);
-    };
-    my $err = $@;
-
-    die $err if $err;
-
-    return $newvol;
-}
-
-sub alloc_image {
-    my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
-
-    my $imagedir = $class->get_subdir($scfg, 'images');
-    $imagedir .= "/$vmid";
-
-    mkpath $imagedir;
-
-    $name = $class->find_free_diskname($storeid, $scfg, $vmid, $fmt, 1) if !$name;
-
-    my (undef, $tmpfmt) = parse_name_dir($name);
-
-    die "illegal name '$name' - wrong extension for format ('$tmpfmt != '$fmt')\n"
-       if $tmpfmt ne $fmt;
-
-    my $path = "$imagedir/$name";
-
-    die "disk image '$path' already exists\n" if -e $path;
-
-    if ($fmt eq 'subvol') {
-       # only allow this if size = 0, so that user knows what he is doing
-       die "storage does not support subvol quotas\n" if $size != 0;
-
-       my $old_umask = umask(0022);
-       my $err;
-       mkdir($path) or $err = "unable to create subvol '$path' - $!\n";
-       umask $old_umask;
-       die $err if $err;
-    } else {
-       my $cmd = ['/usr/bin/qemu-img', 'create'];
-
-       my $prealloc_opt = preallocation_cmd_option($scfg, $fmt);
-       push @$cmd, '-o', $prealloc_opt if defined($prealloc_opt);
-
-       push @$cmd, '-f', $fmt, $path, "${size}K";
-
-       eval { run_command($cmd, errmsg => "unable to create image"); };
-       if ($@) {
-           unlink $path;
-           rmdir $imagedir;
-           die "$@";
-       }
-    }
-
-    return "$vmid/$name";
-}
-
-sub free_image {
-    my ($class, $storeid, $scfg, $volname, $isBase, $format) = @_;
-
-    die "cannot remove protected volume '$volname' on '$storeid'\n"
-       if $class->get_volume_attribute($scfg, $storeid, $volname, 'protected');
-
-    my $path = $class->filesystem_path($scfg, $volname);
-
-    if ($isBase) {
-       # try to remove immutable flag
-       eval { run_command(['/usr/bin/chattr', '-i', $path]); };
-       warn $@ if $@;
-    }
-
-    if (defined($format) && ($format eq 'subvol')) {
-       File::Path::remove_tree($path);
-    } else {
-       if (!(-f $path || -l $path)) {
-           warn "disk image '$path' does not exist\n";
-           return undef;
-       }
-
-       unlink($path) || die "unlink '$path' failed - $!\n";
-    }
-
-    # try to cleanup directory to not clutter storage with empty $vmid dirs if
-    # all images from a guest got deleted
-    my $dir = dirname($path);
-    rmdir($dir);
-
-    return undef;
-}
-
-sub file_size_info {
-    my ($filename, $timeout) = @_;
-
-    my $st = File::stat::stat($filename);
-
-    if (!defined($st)) {
-       my $extramsg = -l $filename ? ' - dangling symlink?' : '';
-       warn "failed to stat '$filename'$extramsg\n";
-       return undef;
-    }
-
-    if (S_ISDIR($st->mode)) {
-       return wantarray ? (0, 'subvol', 0, undef, $st->ctime) : 1;
-    }
-
-    my $json = '';
-    eval {
-       run_command(['/usr/bin/qemu-img', 'info', '--output=json', $filename],
-           timeout => $timeout,
-           outfunc => sub { $json .= shift },
-           errfunc => sub { warn "$_[0]\n" }
-       );
-    };
-    warn $@ if $@;
-
-    my $info = eval { decode_json($json) };
-    if (my $err = $@) {
-       warn "could not parse qemu-img info command output for '$filename' - $err\n";
-       return wantarray ? (undef, undef, undef, undef, $st->ctime) : undef;
-    }
-
-    my ($size, $format, $used, $parent) = $info->@{qw(virtual-size format actual-size backing-filename)};
-
-    ($size) = ($size =~ /^(\d+)$/) or die "size '$size' not an integer\n"; # untaint
-    # coerce back from string
-    $size = int($size);
-    ($used) = ($used =~ /^(\d+)$/) or die "used '$used' not an integer\n"; # untaint
-    # coerce back from string
-    $used = int($used);
-    ($format) = ($format =~ /^(\S+)$/) or die "format '$format' includes whitespace\n"; # untaint
-    if (defined($parent)) {
-       ($parent) = ($parent =~ /^(\S+)$/) or die "parent '$parent' includes whitespace\n"; # untaint
-    }
-    return wantarray ? ($size, $format, $used, $parent, $st->ctime) : $size;
-}
-
-# FIXME remove on the next APIAGE reset.
-# Deprecated, use get_volume_attribute instead.
-sub get_volume_notes {
-    my ($class, $scfg, $storeid, $volname, $timeout) = @_;
-
-    die "volume notes are not supported for $class";
-}
-
-# FIXME remove on the next APIAGE reset.
-# Deprecated, use update_volume_attribute instead.
-sub update_volume_notes {
-    my ($class, $scfg, $storeid, $volname, $notes, $timeout) = @_;
-
-    die "volume notes are not supported for $class";
-}
-
-# Returns undef if the attribute is not supported for the volume.
-# Should die if there is an error fetching the attribute.
-# Possible attributes:
-# notes     - user-provided comments/notes.
-# protected - not to be removed by free_image, and for backups, ignored when pruning.
-sub get_volume_attribute {
-    my ($class, $scfg, $storeid, $volname, $attribute) = @_;
-
-    if ($attribute eq 'notes') {
-        my $notes = eval { $class->get_volume_notes($scfg, $storeid, $volname); };
-        if (my $err = $@) {
-            return if $err =~ m/^volume notes are not supported/;
-            die $err;
-        }
-        return $notes;
-    }
-
-    return;
-}
-
-# Dies if the attribute is not supported for the volume.
-sub update_volume_attribute {
-    my ($class, $scfg, $storeid, $volname, $attribute, $value) = @_;
-
-    if ($attribute eq 'notes') {
-       $class->update_volume_notes($scfg, $storeid, $volname, $value);
-    }
-
-    die "attribute '$attribute' is not supported for storage type '$scfg->{type}'\n";
-}
-
-sub volume_size_info {
-    my ($class, $scfg, $storeid, $volname, $timeout) = @_;
-    my $path = $class->filesystem_path($scfg, $volname);
-    return file_size_info($path, $timeout);
-
-}
-
-sub volume_resize {
-    my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
-
-    die "can't resize this image format\n" if $volname !~ m/\.(raw|qcow2)$/;
-
-    return 1 if $running;
-
-    my $path = $class->filesystem_path($scfg, $volname);
-
-    my $format = ($class->parse_volname($volname))[6];
-
-    my $cmd = ['/usr/bin/qemu-img', 'resize', '-f', $format, $path , $size];
-
-    run_command($cmd, timeout => 10);
-
-    return undef;
-}
-
-sub volume_snapshot {
-    my ($class, $scfg, $storeid, $volname, $snap) = @_;
-
-    die "can't snapshot this image format\n" if $volname !~ m/\.(qcow2|qed)$/;
-
-    my $path = $class->filesystem_path($scfg, $volname);
-
-    my $cmd = ['/usr/bin/qemu-img', 'snapshot','-c', $snap, $path];
-
-    run_command($cmd);
-
-    return undef;
-}
-
-# Asserts that a rollback to $snap on $volname is possible.
-# If certain snapshots are preventing the rollback and $blockers is an array
-# reference, the snapshot names can be pushed onto $blockers prior to dying.
-sub volume_rollback_is_possible {
-    my ($class, $scfg, $storeid, $volname, $snap, $blockers) = @_;
-
-    return 1;
-}
-
-sub volume_snapshot_rollback {
-    my ($class, $scfg, $storeid, $volname, $snap) = @_;
-
-    die "can't rollback snapshot this image format\n" if $volname !~ m/\.(qcow2|qed)$/;
-
-    my $path = $class->filesystem_path($scfg, $volname);
-
-    my $cmd = ['/usr/bin/qemu-img', 'snapshot','-a', $snap, $path];
-
-    run_command($cmd);
-
-    return undef;
-}
-
-sub volume_snapshot_delete {
-    my ($class, $scfg, $storeid, $volname, $snap, $running) = @_;
-
-    die "can't delete snapshot for this image format\n" if $volname !~ m/\.(qcow2|qed)$/;
-
-    return 1 if $running;
-
-    my $path = $class->filesystem_path($scfg, $volname);
-
-    $class->deactivate_volume($storeid, $scfg, $volname, $snap, {});
-
-    my $cmd = ['/usr/bin/qemu-img', 'snapshot','-d', $snap, $path];
-
-    run_command($cmd);
-
-    return undef;
-}
-
-sub volume_snapshot_needs_fsfreeze {
-
-    return 0;
-}
-sub storage_can_replicate {
-    my ($class, $scfg, $storeid, $format) = @_;
-
-    return 0;
-}
-
-sub volume_has_feature {
-    my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running, $opts) = @_;
-
-    my $features = {
-       snapshot => {
-           current => { qcow2 => 1 },
-           snap => { qcow2 => 1 },
-       },
-       clone => {
-           base => { qcow2 => 1, raw => 1, vmdk => 1 },
-       },
-       template => {
-           current => { qcow2 => 1, raw => 1, vmdk => 1, subvol => 1 },
-       },
-       copy => {
-           base => { qcow2 => 1, raw => 1, vmdk => 1 },
-           current => { qcow2 => 1, raw => 1, vmdk => 1 },
-           snap => { qcow2 => 1 },
-       },
-       sparseinit => {
-           base => { qcow2 => 1, raw => 1, vmdk => 1 },
-           current => { qcow2 => 1, raw => 1, vmdk => 1 },
-       },
-       rename => {
-           current => {qcow2 => 1, raw => 1, vmdk => 1},
-       },
-    };
-
-    if ($feature eq 'clone') {
-       if (
-           defined($opts->{valid_target_formats})
-           && !(grep { $_ eq 'qcow2' } @{$opts->{valid_target_formats}})
-       ) {
-           return 0; # clone_image creates a qcow2 volume
-       }
-    } elsif ($feature eq 'rename') {
-       return 0 if $class->can('api') && $class->api() < 10;
-    }
-
-
-    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) = $class->parse_volname($volname);
-
-    my $key = undef;
-    if($snapname){
-        $key = 'snap';
-    }else{
-        $key =  $isBase ? 'base' : 'current';
-    }
-
-    return 1 if defined($features->{$feature}->{$key}->{$format});
-
-    return undef;
-}
-
-sub list_images {
-    my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
-
-    my $imagedir = $class->get_subdir($scfg, 'images');
-
-    my ($defFmt, $vaidFmts) = default_format($scfg);
-    my $fmts = join ('|', @$vaidFmts);
-
-    my $res = [];
-
-    foreach my $fn (<$imagedir/[0-9][0-9]*/*>) {
-
-       next if $fn !~ m!^(/.+/(\d+)/([^/]+\.($fmts)))$!;
-       $fn = $1; # untaint
-
-       my $owner = $2;
-       my $name = $3;
-
-       next if !$vollist && defined($vmid) && ($owner ne $vmid);
-
-       my ($size, $format, $used, $parent, $ctime) = file_size_info($fn);
-       next if !($format && defined($size));
-
-       my $volid;
-       if ($parent && $parent =~ m!^../(\d+)/([^/]+\.($fmts))$!) {
-           my ($basevmid, $basename) = ($1, $2);
-           $volid = "$storeid:$basevmid/$basename/$owner/$name";
-       } else {
-           $volid = "$storeid:$owner/$name";
-       }
-
-       if ($vollist) {
-           my $found = grep { $_ eq $volid } @$vollist;
-           next if !$found;
-       }
-
-        my $info = {
-           volid => $volid, format => $format,
-           size => $size, vmid => $owner, used => $used, parent => $parent
-       };
-
-        $info->{ctime} = $ctime if $ctime;
-
-        push @$res, $info;
-    }
-
-    return $res;
-}
-
-# list templates ($tt = <iso|vztmpl|backup|snippets>)
-my $get_subdir_files = sub {
-    my ($sid, $path, $tt, $vmid) = @_;
-
-    my $res = [];
-
-    foreach my $fn (<$path/*>) {
-       my $st = File::stat::stat($fn);
-
-       next if (!$st || S_ISDIR($st->mode));
-
-       my $info;
-
-       if ($tt eq 'iso') {
-           next if $fn !~ m!/([^/]+$PVE::Storage::ISO_EXT_RE_0)$!i;
-
-           $info = { volid => "$sid:iso/$1", format => 'iso' };
-
-       } elsif ($tt eq 'vztmpl') {
-           next if $fn !~ m!/([^/]+$PVE::Storage::VZTMPL_EXT_RE_1)$!;
-
-           $info = { volid => "$sid:vztmpl/$1", format => "t$2" };
-
-       } elsif ($tt eq 'backup') {
-           next if $fn !~ m!/([^/]+$PVE::Storage::BACKUP_EXT_RE_2)$!;
-           my $original = $fn;
-           my $format = $2;
-           $fn = $1;
-
-           # only match for VMID now, to avoid false positives (VMID in parent directory name)
-           next if defined($vmid) && $fn !~ m/\S+-$vmid-\S+/;
-
-           $info = { volid => "$sid:backup/$fn", format => $format };
-
-           my $archive_info = eval { PVE::Storage::archive_info($fn) } // {};
-
-           $info->{ctime} = $archive_info->{ctime} if defined($archive_info->{ctime});
-           $info->{subtype} = $archive_info->{type} // 'unknown';
-
-           if (defined($vmid) || $fn =~ m!\-([1-9][0-9]{2,8})\-[^/]+\.${format}$!) {
-               $info->{vmid} = $vmid // $1;
-           }
-
-           my $notes_fn = $original.NOTES_EXT;
-           if (-f $notes_fn) {
-               my $notes = PVE::Tools::file_read_firstline($notes_fn);
-               $info->{notes} = eval { decode('UTF-8', $notes, 1) } // $notes if defined($notes);
-           }
-
-           $info->{protected} = 1 if -e PVE::Storage::protection_file_path($original);
-       } elsif ($tt eq 'snippets') {
-
-           $info = {
-               volid => "$sid:snippets/". basename($fn),
-               format => 'snippet',
-           };
-       }
-
-       $info->{size} = $st->size;
-       $info->{ctime} //= $st->ctime;
-
-       push @$res, $info;
-    }
-
-    return $res;
-};
-
-# If attributes are set on a volume, they should be included in the result.
-# See get_volume_attribute for a list of possible attributes.
-sub list_volumes {
-    my ($class, $storeid, $scfg, $vmid, $content_types) = @_;
-
-    my $res = [];
-    my $vmlist = PVE::Cluster::get_vmlist();
-    foreach my $type (@$content_types) {
-       my $data;
-
-       if ($type eq 'images' || $type eq 'rootdir') {
-           $data = $class->list_images($storeid, $scfg, $vmid);
-       } elsif ($scfg->{path}) {
-           my $path = $class->get_subdir($scfg, $type);
-
-           if ($type eq 'iso' && !defined($vmid)) {
-               $data = $get_subdir_files->($storeid, $path, 'iso');
-           } elsif ($type eq 'vztmpl'&& !defined($vmid)) {
-               $data = $get_subdir_files->($storeid, $path, 'vztmpl');
-           } elsif ($type eq 'backup') {
-               $data = $get_subdir_files->($storeid, $path, 'backup', $vmid);
-           } elsif ($type eq 'snippets') {
-               $data = $get_subdir_files->($storeid, $path, 'snippets');
-           }
-       }
-
-       next if !$data;
-
-       foreach my $item (@$data) {
-           if ($type eq 'images' || $type eq 'rootdir') {
-               my $vminfo = $vmlist->{ids}->{$item->{vmid}};
-               my $vmtype;
-               if (defined($vminfo)) {
-                   $vmtype = $vminfo->{type};
-               }
-               if (defined($vmtype) && $vmtype eq 'lxc') {
-                   $item->{content} = 'rootdir';
-               } else {
-                   $item->{content} = 'images';
-               }
-               next if $type ne $item->{content};
-           } else {
-               $item->{content} = $type;
-           }
-
-           push @$res, $item;
-       }
-    }
-
-    return $res;
-}
-
-sub status {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    my $path = $scfg->{path};
-
-    die "storage definition has no path\n" if !$path;
-
-    my $timeout = 2;
-    my $res = PVE::Tools::df($path, $timeout);
-
-    return undef if !$res || !$res->{total};
-
-    return ($res->{total}, $res->{avail}, $res->{used}, 1);
-}
-
-# Returns a hash with the snapshot names as keys and the following data:
-# id        - Unique id to distinguish different snapshots even if the have the same name.
-# timestamp - Creation time of the snapshot (seconds since epoch).
-# Returns an empty hash if the volume does not exist.
-sub volume_snapshot_info {
-    my ($class, $scfg, $storeid, $volname) = @_;
-
-    die "volume_snapshot_info is not implemented for $class";
-}
-
-sub activate_storage {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    my $path = $scfg->{path};
-
-    die "storage definition has no path\n" if !$path;
-
-    # this path test may hang indefinitely on unresponsive mounts
-    my $timeout = 2;
-    if (! PVE::Tools::run_fork_with_timeout($timeout, sub {-d $path})) {
-       die "unable to activate storage '$storeid' - " .
-       "directory '$path' does not exist or is unreachable\n";
-    }
-
-
-    return if defined($scfg->{mkdir}) && !$scfg->{mkdir};
-
-    if (defined($scfg->{content})) {
-       foreach my $vtype (keys %$vtype_subdirs) {
-           # OpenVZMigrate uses backup (dump) dir
-           if (defined($scfg->{content}->{$vtype}) ||
-               ($vtype eq 'backup' && defined($scfg->{content}->{'rootdir'}))) {
-               my $subdir = $class->get_subdir($scfg, $vtype);
-               mkpath $subdir if $subdir ne $path;
-           }
-       }
-    }
-}
-
-sub deactivate_storage {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    # do nothing by default
-}
-
-sub map_volume {
-    my ($class, $storeid, $scfg, $volname, $snapname) = @_;
-
-    my ($path) = $class->path($scfg, $volname, $storeid, $snapname);
-    return $path;
-}
-
-sub unmap_volume {
-    my ($class, $storeid, $scfg, $volname, $snapname) = @_;
-
-    return 1;
-}
-
-sub activate_volume {
-    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
-
-    my $path = $class->filesystem_path($scfg, $volname, $snapname);
-
-    # check is volume exists
-    if ($scfg->{path}) {
-       die "volume '$storeid:$volname' does not exist\n" if ! -e $path;
-    } else {
-       die "volume '$storeid:$volname' does not exist\n" if ! -b $path;
-    }
-}
-
-sub deactivate_volume {
-    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
-
-    # do nothing by default
-}
-
-sub check_connection {
-    my ($class, $storeid, $scfg) = @_;
-    # do nothing by default
-    return 1;
-}
-
-sub prune_backups {
-    my ($class, $scfg, $storeid, $keep, $vmid, $type, $dryrun, $logfunc) = @_;
-
-    $logfunc //= sub { print "$_[1]\n" };
-
-    my $backups = $class->list_volumes($storeid, $scfg, $vmid, ['backup']);
-
-    my $backup_groups = {};
-    my $prune_list = [];
-
-    foreach my $backup (@{$backups}) {
-       my $volid = $backup->{volid};
-       my $archive_info = eval { PVE::Storage::archive_info($volid) } // {};
-       my $backup_type = $archive_info->{type} // 'unknown';
-       my $backup_vmid = $archive_info->{vmid} // $backup->{vmid};
-
-       next if defined($type) && $type ne $backup_type;
-
-       my $prune_entry = {
-           ctime => $backup->{ctime},
-           type => $backup_type,
-           volid => $volid,
-       };
-
-       $prune_entry->{vmid} = $backup_vmid if defined($backup_vmid);
-
-       if ($archive_info->{is_std_name}) {
-           die "internal error - got no VMID\n" if !defined($backup_vmid);
-           die "internal error - got wrong VMID '$backup_vmid' != '$vmid'\n"
-               if defined($vmid) && $backup_vmid ne $vmid;
-
-           $prune_entry->{ctime} = $archive_info->{ctime};
-           my $group = "$backup_type/$backup_vmid";
-           push @{$backup_groups->{$group}}, $prune_entry;
-       } else {
-           # ignore backups that don't use the standard naming scheme
-           $prune_entry->{mark} = 'renamed';
-       }
-
-       $prune_entry->{mark} = 'protected' if $backup->{protected};
-
-       push @{$prune_list}, $prune_entry;
-    }
-
-    foreach my $backup_group (values %{$backup_groups}) {
-       PVE::Storage::prune_mark_backup_group($backup_group, $keep);
-    }
-
-    my $failed;
-    if (!$dryrun) {
-       foreach my $prune_entry (@{$prune_list}) {
-           next if $prune_entry->{mark} ne 'remove';
-
-           my $volid = $prune_entry->{volid};
-           $logfunc->('info', "removing backup '$volid'");
-           eval {
-               my (undef, $volname) = parse_volume_id($volid);
-               my $archive_path = $class->filesystem_path($scfg, $volname);
-               PVE::Storage::archive_remove($archive_path);
-           };
-           if (my $err = $@) {
-               $logfunc->('err', "error when removing backup '$volid' - $err\n");
-               $failed = 1;
-           }
-       }
-    }
-    die "error pruning backups - check log\n" if $failed;
-
-    return $prune_list;
-}
-
-# Import/Export interface:
-#   Any path based storage is assumed to support 'raw' and 'tar' streams, so
-#   the default implementations will return this if $scfg->{path} is set,
-#   mimicking the old PVE::Storage::storage_migrate() function.
-#
-# Plugins may fall back to PVE::Storage::Plugin::volume_{export,import}...
-#   functions in case the format doesn't match their specialized
-#   implementations to reuse the raw/tar code.
-#
-# Format specification:
-#   The following formats are all prefixed with image information in the form
-#   of a 64 bit little endian unsigned integer (pack('Q<')) in order to be able
-#   to preallocate the image on storages which require it.
-#
-#   raw+size: (image files only)
-#     A raw binary data stream such as produced via `dd if=TheImageFile`.
-#   qcow2+size, vmdk: (image files only)
-#     A raw qcow2/vmdk/... file such as produced via `dd if=some.qcow2` for
-#     files which are already in qcow2 format, or via `qemu-img convert`.
-#     Note that these formats are only valid with $with_snapshots being true.
-#   tar+size: (subvolumes only)
-#     A GNU tar stream containing just the inner contents of the subvolume.
-#     This does not distinguish between the contents of a privileged or
-#     unprivileged container. In other words, this is from the root user
-#     namespace's point of view with no uid-mapping in effect.
-#     As produced via `tar -C vm-100-disk-1.subvol -cpf TheOutputFile.dat .`
-
-# Plugins may reuse these helpers. Changes to the header format should be
-# reflected by changes to the function prototypes.
-sub write_common_header($$) {
-    my ($fh, $image_size_in_bytes) = @_;
-    syswrite($fh, pack("Q<", $image_size_in_bytes), 8);
-}
-
-sub read_common_header($) {
-    my ($fh) = @_;
-    sysread($fh, my $size, 8);
-    $size = unpack('Q<', $size);
-    die "import: no size found in export header, aborting.\n" if !defined($size);
-    # Size is in bytes!
-    return $size;
-}
-
-# Export a volume into a file handle as a stream of desired format.
-sub volume_export {
-    my ($class, $scfg, $storeid, $fh, $volname, $format, $snapshot, $base_snapshot, $with_snapshots) = @_;
-    if ($scfg->{path} && !defined($snapshot) && !defined($base_snapshot)) {
-       my $file = $class->path($scfg, $volname, $storeid)
-           or goto unsupported;
-       my ($size, $file_format) = file_size_info($file);
-
-       if ($format eq 'raw+size') {
-           goto unsupported if $with_snapshots || $file_format eq 'subvol';
-           write_common_header($fh, $size);
-           if ($file_format eq 'raw') {
-               run_command(['dd', "if=$file", "bs=4k"], output => '>&'.fileno($fh));
-           } else {
-               run_command(['qemu-img', 'convert', '-f', $file_format, '-O', 'raw', $file, '/dev/stdout'],
-                           output => '>&'.fileno($fh));
-           }
-           return;
-       } elsif ($format =~ /^(qcow2|vmdk)\+size$/) {
-           my $data_format = $1;
-           goto unsupported if !$with_snapshots || $file_format ne $data_format;
-           write_common_header($fh, $size);
-           run_command(['dd', "if=$file", "bs=4k"], output => '>&'.fileno($fh));
-           return;
-       } elsif ($format eq 'tar+size') {
-           goto unsupported if $file_format ne 'subvol';
-           write_common_header($fh, $size);
-           run_command(['tar', @COMMON_TAR_FLAGS, '-cf', '-', '-C', $file, '.'],
-                       output => '>&'.fileno($fh));
-           return;
-       }
-    }
- unsupported:
-    die "volume export format $format not available for $class";
-}
-
-sub volume_export_formats {
-    my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
-    if ($scfg->{path} && !defined($snapshot) && !defined($base_snapshot)) {
-       my $file = $class->path($scfg, $volname, $storeid)
-           or return;
-       my ($size, $format) = file_size_info($file);
-
-       if ($with_snapshots) {
-           return ($format.'+size') if ($format eq 'qcow2' || $format eq 'vmdk');
-           return ();
-       }
-       return ('tar+size') if $format eq 'subvol';
-       return ('raw+size');
-    }
-    return ();
-}
-
-# Import data from a stream, creating a new or replacing or adding to an existing volume.
-sub volume_import {
-    my ($class, $scfg, $storeid, $fh, $volname, $format, $snapshot, $base_snapshot, $with_snapshots, $allow_rename) = @_;
-
-    die "volume import format '$format' not available for $class\n"
-       if $format !~ /^(raw|tar|qcow2|vmdk)\+size$/;
-    my $data_format = $1;
-
-    die "format $format cannot be imported without snapshots\n"
-       if !$with_snapshots && ($data_format eq 'qcow2' || $data_format eq 'vmdk');
-    die "format $format cannot be imported with snapshots\n"
-       if $with_snapshots && ($data_format eq 'raw' || $data_format eq 'tar');
-
-    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $file_format) =
-       $class->parse_volname($volname);
-
-    # XXX: Should we bother with conversion routines at this level? This won't
-    # happen without manual CLI usage, so for now we just error out...
-    die "cannot import format $format into a file of format $file_format\n"
-       if $data_format ne $file_format && !($data_format eq 'tar' && $file_format eq 'subvol');
-
-    # Check for an existing file first since interrupting alloc_image doesn't
-    # free it.
-    my $file = $class->path($scfg, $volname, $storeid);
-    if (-e $file) {
-       die "file '$file' already exists\n" if !$allow_rename;
-       warn "file '$file' already exists - importing with a different name\n";
-       $name = undef;
-    }
-
-    my ($size) = read_common_header($fh);
-    $size = int($size/1024);
-
-    eval {
-       my $allocname = $class->alloc_image($storeid, $scfg, $vmid, $file_format, $name, $size);
-       my $oldname = $volname;
-       $volname = $allocname;
-       if (defined($name) && $allocname ne $oldname) {
-           die "internal error: unexpected allocated name: '$allocname' != '$oldname'\n";
-       }
-       my $file = $class->path($scfg, $volname, $storeid)
-           or die "internal error: failed to get path to newly allocated volume $volname\n";
-       if ($data_format eq 'raw' || $data_format eq 'qcow2' || $data_format eq 'vmdk') {
-           run_command(['dd', "of=$file", 'conv=sparse', 'bs=64k'],
-                       input => '<&'.fileno($fh));
-       } elsif ($data_format eq 'tar') {
-           run_command(['tar', @COMMON_TAR_FLAGS, '-C', $file, '-xf', '-'],
-                       input => '<&'.fileno($fh));
-       } else {
-           die "volume import format '$format' not available for $class";
-       }
-    };
-    if (my $err = $@) {
-       eval { $class->free_image($storeid, $scfg, $volname, 0, $file_format) };
-       warn $@ if $@;
-       die $err;
-    }
-
-    return "$storeid:$volname";
-}
-
-sub volume_import_formats {
-    my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
-    if ($scfg->{path} && !defined($base_snapshot)) {
-       my $format = ($class->parse_volname($volname))[6];
-       if ($with_snapshots) {
-           return ($format.'+size') if ($format eq 'qcow2' || $format eq 'vmdk');
-           return ();
-       }
-       return ('tar+size') if $format eq 'subvol';
-       return ('raw+size');
-    }
-    return ();
-}
-
-sub rename_volume {
-    my ($class, $scfg, $storeid, $source_volname, $target_vmid, $target_volname) = @_;
-    die "not implemented in storage plugin '$class'\n" if $class->can('api') && $class->api() < 10;
-    die "no path found\n" if !$scfg->{path};
-
-    my (
-       undef,
-       $source_image,
-       $source_vmid,
-       $base_name,
-       $base_vmid,
-       undef,
-       $format
-    ) = $class->parse_volname($source_volname);
-
-    $target_volname = $class->find_free_diskname($storeid, $scfg, $target_vmid, $format, 1)
-       if !$target_volname;
-
-    my $basedir = $class->get_subdir($scfg, 'images');
-
-    mkpath "${basedir}/${target_vmid}";
-
-    my $old_path = "${basedir}/${source_vmid}/${source_image}";
-    my $new_path = "${basedir}/${target_vmid}/${target_volname}";
-
-    die "target volume '${target_volname}' already exists\n" if -e $new_path;
-
-    my $base = $base_name ? "${base_vmid}/${base_name}/" : '';
-
-    rename($old_path, $new_path) ||
-       die "rename '$old_path' to '$new_path' failed - $!\n";
-
-    return "${storeid}:${base}${target_vmid}/${target_volname}";
-}
-
-1;
diff --git a/PVE/Storage/RBDPlugin.pm b/PVE/Storage/RBDPlugin.pm
deleted file mode 100644 (file)
index 73703fb..0000000
+++ /dev/null
@@ -1,891 +0,0 @@
-package PVE::Storage::RBDPlugin;
-
-use strict;
-use warnings;
-
-use Cwd qw(abs_path);
-use IO::File;
-use JSON;
-use Net::IP;
-
-use PVE::CephConfig;
-use PVE::Cluster qw(cfs_read_file);;
-use PVE::JSONSchema qw(get_standard_option);
-use PVE::ProcFSTools;
-use PVE::RADOS;
-use PVE::RPCEnvironment;
-use PVE::Storage::Plugin;
-use PVE::Tools qw(run_command trim file_read_firstline);
-
-use base qw(PVE::Storage::Plugin);
-
-my $get_parent_image_name = sub {
-    my ($parent) = @_;
-    return undef if !$parent;
-    return $parent->{image} . "@" . $parent->{snapshot};
-};
-
-my $librados_connect = sub {
-    my ($scfg, $storeid, $options) = @_;
-
-    $options->{timeout} = 60
-       if !defined($options->{timeout}) && PVE::RPCEnvironment->is_worker();
-
-    my $librados_config = PVE::CephConfig::ceph_connect_option($scfg, $storeid, $options->%*);
-
-    my $rados = PVE::RADOS->new(%$librados_config);
-
-    return $rados;
-};
-
-my sub get_rbd_path {
-    my ($scfg, $volume) = @_;
-    my $path = $scfg->{pool} ? $scfg->{pool} : 'rbd';
-    $path .= "/$scfg->{namespace}" if defined($scfg->{namespace});
-    $path .= "/$volume" if defined($volume);
-    return $path;
-};
-
-my sub get_rbd_dev_path {
-    my ($scfg, $storeid, $volume) = @_;
-
-    my $cluster_id = '';
-    if ($scfg->{fsid}) {
-       # NOTE: the config doesn't support this currently (but it could!), hack for qemu-server tests
-       $cluster_id = $scfg->{fsid};
-    } elsif ($scfg->{monhost}) {
-       my $rados = $librados_connect->($scfg, $storeid);
-       $cluster_id = $rados->mon_command({ prefix => 'fsid', format => 'json' })->{fsid};
-    } else {
-       $cluster_id = cfs_read_file('ceph.conf')->{global}->{fsid};
-    }
-
-    my $uuid_pattern = "([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})";
-    if ($cluster_id =~ qr/^${uuid_pattern}$/is) {
-       $cluster_id = $1; # use untained value
-    } else {
-       die "cluster fsid has invalid format\n";
-    }
-
-    my $rbd_path = get_rbd_path($scfg, $volume);
-    my $pve_path = "/dev/rbd-pve/${cluster_id}/${rbd_path}";
-    my $path = "/dev/rbd/${rbd_path}";
-
-    if (!-e $pve_path && -e $path) {
-       # possibly mapped before rbd-pve rule existed
-       my $real_dev = abs_path($path);
-       my ($rbd_id) = ($real_dev =~ m|/dev/rbd([0-9]+)$|);
-       my $dev_cluster_id = file_read_firstline("/sys/devices/rbd/${rbd_id}/cluster_fsid");
-       return $path if $cluster_id eq $dev_cluster_id;
-    }
-    return $pve_path;
-}
-
-my $build_cmd = sub {
-    my ($binary, $scfg, $storeid, $op, @options) = @_;
-
-    my $cmd_option = PVE::CephConfig::ceph_connect_option($scfg, $storeid);
-    my $pool =  $scfg->{pool} ? $scfg->{pool} : 'rbd';
-
-    my $cmd = [$binary, '-p', $pool];
-
-    if (defined(my $namespace = $scfg->{namespace})) {
-       # some subcommands will fail if the --namespace parameter is present
-       my $no_namespace_parameter = {
-           unmap => 1,
-       };
-       push @$cmd, '--namespace', "$namespace" if !$no_namespace_parameter->{$op};
-    }
-    push @$cmd, '-c', $cmd_option->{ceph_conf} if ($cmd_option->{ceph_conf});
-    push @$cmd, '-m', $cmd_option->{mon_host} if ($cmd_option->{mon_host});
-    push @$cmd, '--auth_supported', $cmd_option->{auth_supported} if ($cmd_option->{auth_supported});
-    push @$cmd, '-n', "client.$cmd_option->{userid}" if ($cmd_option->{userid});
-    push @$cmd, '--keyring', $cmd_option->{keyring} if ($cmd_option->{keyring});
-
-    push @$cmd, $op;
-
-    push @$cmd, @options if scalar(@options);
-
-    return $cmd;
-};
-
-my $rbd_cmd = sub {
-    my ($scfg, $storeid, $op, @options) = @_;
-
-    return $build_cmd->('/usr/bin/rbd', $scfg, $storeid, $op, @options);
-};
-
-my $rados_cmd = sub {
-    my ($scfg, $storeid, $op, @options) = @_;
-
-    return $build_cmd->('/usr/bin/rados', $scfg, $storeid, $op, @options);
-};
-
-# needed for volumes created using ceph jewel (or higher)
-my $krbd_feature_update = sub {
-    my ($scfg, $storeid, $name) = @_;
-
-    my (@disable, @enable);
-    my ($kmajor, $kminor) = PVE::ProcFSTools::kernel_version();
-
-    if ($kmajor > 5 || $kmajor == 5 && $kminor >= 3) {
-       # 'deep-flatten' can only be disabled, not enabled after image creation
-       push @enable, 'fast-diff', 'object-map';
-    } else {
-       push @disable, 'fast-diff', 'object-map', 'deep-flatten';
-    }
-
-    if ($kmajor >= 5) {
-       push @enable, 'exclusive-lock';
-    } else {
-       push @disable, 'exclusive-lock';
-    }
-
-    my $active_features_list = (rbd_volume_info($scfg, $storeid, $name))[4];
-    my $active_features = { map { $_ => 1 } @$active_features_list };
-
-    my $to_disable = join(',', grep {  $active_features->{$_} } @disable);
-    my $to_enable  = join(',', grep { !$active_features->{$_} } @enable );
-
-    if ($to_disable) {
-       print "disable RBD image features this kernel RBD drivers is not compatible with: $to_disable\n";
-       my $cmd = $rbd_cmd->($scfg, $storeid, 'feature', 'disable', $name, $to_disable);
-       run_rbd_command(
-           $cmd,
-           errmsg => "could not disable krbd-incompatible image features '$to_disable' for rbd image: $name",
-       );
-    }
-    if ($to_enable) {
-       print "enable RBD image features this kernel RBD drivers supports: $to_enable\n";
-       eval {
-           my $cmd = $rbd_cmd->($scfg, $storeid, 'feature', 'enable', $name, $to_enable);
-           run_rbd_command(
-               $cmd,
-               errmsg => "could not enable krbd-compatible image features '$to_enable' for rbd image: $name",
-           );
-       };
-       warn "$@" if $@;
-    }
-};
-
-sub run_rbd_command {
-    my ($cmd, %args) = @_;
-
-    my $lasterr;
-    my $errmsg = $args{errmsg} . ": " || "";
-    if (!exists($args{errfunc})) {
-       # ' error: 2014-02-06 11:51:59.839135 7f09f94d0760 -1 librbd: snap_unprotect: can't unprotect;
-       # at least 1 child(ren) in pool cephstor1
-       $args{errfunc} = sub {
-           my $line = shift;
-           if ($line =~ m/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+ [0-9a-f]+ [\-\d]+ librbd: (.*)$/) {
-               $lasterr = "$1\n";
-           } else {
-               $lasterr = $line;
-           }
-           print STDERR $lasterr;
-           *STDERR->flush();
-       };
-    }
-
-    eval { run_command($cmd, %args); };
-    if (my $err = $@) {
-       die $errmsg . $lasterr if length($lasterr);
-       die $err;
-    }
-
-    return undef;
-}
-
-sub rbd_ls {
-    my ($scfg, $storeid) = @_;
-
-    my $pool =  $scfg->{pool} ? $scfg->{pool} : 'rbd';
-    $pool .= "/$scfg->{namespace}" if defined($scfg->{namespace});
-
-    my $raw = '';
-    my $parser = sub { $raw .= shift };
-
-    my $cmd = $rbd_cmd->($scfg, $storeid, 'ls', '-l', '--format', 'json');
-    eval {
-       run_rbd_command($cmd, errmsg => "rbd error", errfunc => sub {}, outfunc => $parser);
-    };
-    my $err = $@;
-
-    die $err if $err && $err !~ m/doesn't contain rbd images/ ;
-
-    my $result;
-    if ($raw eq '') {
-       $result = [];
-    } elsif ($raw =~ m/^(\[.*\])$/s) { # untaint
-       $result = JSON::decode_json($1);
-    } else {
-       die "got unexpected data from rbd ls: '$raw'\n";
-    }
-
-    my $list = {};
-
-    foreach my $el (@$result) {
-       next if defined($el->{snapshot});
-
-       my $image = $el->{image};
-
-       my ($owner) = $image =~ m/^(?:vm|base)-(\d+)-/;
-       next if !defined($owner);
-
-       $list->{$pool}->{$image} = {
-           name => $image,
-           size => $el->{size},
-           parent => $get_parent_image_name->($el->{parent}),
-           vmid => $owner
-       };
-    }
-
-    return $list;
-}
-
-sub rbd_ls_snap {
-    my ($scfg, $storeid, $name) = @_;
-
-    my $cmd = $rbd_cmd->($scfg, $storeid, 'snap', 'ls', $name, '--format', 'json');
-
-    my $raw = '';
-    run_rbd_command($cmd, errmsg => "rbd error", errfunc => sub {}, outfunc => sub { $raw .= shift; });
-
-    my $list;
-    if ($raw =~ m/^(\[.*\])$/s) { # untaint
-       $list = eval { JSON::decode_json($1) };
-       die "invalid JSON output from 'rbd snap ls $name': $@\n" if $@;
-    } else {
-       die "got unexpected data from 'rbd snap ls $name': '$raw'\n";
-    }
-
-    $list = [] if !defined($list);
-
-    my $res = {};
-    foreach my $el (@$list) {
-       my $snap = $el->{name};
-       my $protected = defined($el->{protected}) && $el->{protected} eq "true" ? 1 : undef;
-       $res->{$snap} = {
-           name => $snap,
-           id => $el->{id} // undef,
-           size => $el->{size} // 0,
-           protected => $protected,
-       };
-    }
-    return $res;
-}
-
-sub rbd_volume_info {
-    my ($scfg, $storeid, $volname, $snap) = @_;
-
-    my $cmd = undef;
-
-    my @options = ('info', $volname, '--format', 'json');
-    if ($snap) {
-       push @options, '--snap', $snap;
-    }
-
-    $cmd = $rbd_cmd->($scfg, $storeid, @options);
-
-    my $raw = '';
-    my $parser = sub { $raw .= shift };
-
-    run_rbd_command($cmd, errmsg => "rbd error", errfunc => sub {}, outfunc => $parser);
-
-    my $volume;
-    if ($raw eq '') {
-       $volume = {};
-    } elsif ($raw =~ m/^(\{.*\})$/s) { # untaint
-       $volume = JSON::decode_json($1);
-    } else {
-       die "got unexpected data from rbd info: '$raw'\n";
-    }
-
-    $volume->{parent} = $get_parent_image_name->($volume->{parent});
-    $volume->{protected} = defined($volume->{protected}) && $volume->{protected} eq "true" ? 1 : undef;
-
-    return $volume->@{qw(size parent format protected features)};
-}
-
-sub rbd_volume_du {
-    my ($scfg, $storeid, $volname) = @_;
-
-    my @options = ('du', $volname, '--format', 'json');
-    my $cmd = $rbd_cmd->($scfg, $storeid, @options);
-
-    my $raw = '';
-    my $parser = sub { $raw .= shift };
-
-    run_rbd_command($cmd, errmsg => "rbd error", errfunc => sub {}, outfunc => $parser);
-
-    my $volume;
-    if ($raw eq '') {
-       $volume = {};
-    } elsif ($raw =~ m/^(\{.*\})$/s) { # untaint
-       $volume = JSON::decode_json($1);
-    } else {
-       die "got unexpected data from rbd du: '$raw'\n";
-    }
-
-    if (!defined($volume->{images})) {
-       die "got no images from rbd du\n";
-    }
-
-    # `rbd du` returns array of images for name matching `volname`,
-    # including snapshots.
-    my $images = $volume->{images};
-    foreach my $image (@$images) {
-       next if defined($image->{snapshot});
-       next if !defined($image->{used_size}) || !defined($image->{name});
-
-       # Return `used_size` of first volume with matching name which
-       # is not a snapshot.
-       return $image->{used_size} if $image->{name} eq $volname;
-    }
-
-    die "got no matching image from rbd du\n";
-}
-
-# Configuration
-
-sub type {
-    return 'rbd';
-}
-
-sub plugindata {
-    return {
-       content => [ {images => 1, rootdir => 1}, { images => 1 }],
-    };
-}
-
-sub properties {
-    return {
-       monhost => {
-           description => "IP addresses of monitors (for external clusters).",
-           type => 'string', format => 'pve-storage-portal-dns-list',
-       },
-       pool => {
-           description => "Pool.",
-           type => 'string',
-       },
-       'data-pool' => {
-           description => "Data Pool (for erasure coding only)",
-           type => 'string',
-       },
-       namespace => {
-           description => "Namespace.",
-           type => 'string',
-       },
-       username => {
-           description => "RBD Id.",
-           type => 'string',
-       },
-       authsupported => {
-           description => "Authsupported.",
-           type => 'string',
-       },
-       krbd => {
-           description => "Always access rbd through krbd kernel module.",
-           type => 'boolean',
-       },
-       keyring => {
-           description => "Client keyring contents (for external clusters).",
-           type => 'string',
-       },
-    };
-}
-
-sub options {
-    return {
-       nodes => { optional => 1 },
-       disable => { optional => 1 },
-       monhost => { optional => 1},
-       pool => { optional => 1 },
-       'data-pool' => { optional => 1 },
-       namespace => { optional => 1 },
-       username => { optional => 1 },
-       content => { optional => 1 },
-       krbd => { optional => 1 },
-       keyring => { optional => 1 },
-       bwlimit => { optional => 1 },
-    };
-}
-
-# Storage implementation
-
-sub on_add_hook {
-    my ($class, $storeid, $scfg, %param) = @_;
-
-    PVE::CephConfig::ceph_create_keyfile($scfg->{type}, $storeid, $param{keyring});
-
-    return;
-}
-
-sub on_update_hook {
-    my ($class, $storeid, $scfg, %param) = @_;
-
-    if (exists($param{keyring})) {
-       if (defined($param{keyring})) {
-           PVE::CephConfig::ceph_create_keyfile($scfg->{type}, $storeid, $param{keyring});
-       } else {
-           PVE::CephConfig::ceph_remove_keyfile($scfg->{type}, $storeid);
-       }
-    }
-
-    return;
-}
-
-sub on_delete_hook {
-    my ($class, $storeid, $scfg) = @_;
-    PVE::CephConfig::ceph_remove_keyfile($scfg->{type}, $storeid);
-    return;
-}
-
-sub parse_volname {
-    my ($class, $volname) = @_;
-
-    if ($volname =~ m/^((base-(\d+)-\S+)\/)?((base)?(vm)?-(\d+)-\S+)$/) {
-       return ('images', $4, $7, $2, $3, $5, 'raw');
-    }
-
-    die "unable to parse rbd volume name '$volname'\n";
-}
-
-sub path {
-    my ($class, $scfg, $volname, $storeid, $snapname) = @_;
-
-    my $cmd_option = PVE::CephConfig::ceph_connect_option($scfg, $storeid);
-    my ($vtype, $name, $vmid) = $class->parse_volname($volname);
-    $name .= '@'.$snapname if $snapname;
-
-    if ($scfg->{krbd}) {
-       my $rbd_dev_path = get_rbd_dev_path($scfg, $storeid, $name);
-       return ($rbd_dev_path, $vmid, $vtype);
-    }
-
-    my $rbd_path = get_rbd_path($scfg, $name);
-    my $path = "rbd:${rbd_path}";
-
-    $path .= ":conf=$cmd_option->{ceph_conf}" if $cmd_option->{ceph_conf};
-    if (defined($scfg->{monhost})) {
-       my $monhost = PVE::CephConfig::hostlist($scfg->{monhost}, ';');
-       $monhost =~ s/:/\\:/g;
-       $path .= ":mon_host=$monhost";
-       $path .= ":auth_supported=$cmd_option->{auth_supported}";
-    }
-
-    $path .= ":id=$cmd_option->{userid}:keyring=$cmd_option->{keyring}" if ($cmd_option->{keyring});
-
-    return ($path, $vmid, $vtype);
-}
-
-sub find_free_diskname {
-    my ($class, $storeid, $scfg, $vmid, $fmt, $add_fmt_suffix) = @_;
-
-    my $cmd = $rbd_cmd->($scfg, $storeid, 'ls');
-
-    my $disk_list = [];
-
-    my $parser = sub {
-       my $line = shift;
-       if ($line =~ m/^(.*)$/) { # untaint
-           push @$disk_list, $1;
-       }
-    };
-
-    eval {
-       run_rbd_command($cmd, errmsg => "rbd error", errfunc => sub {}, outfunc => $parser);
-    };
-    my $err = $@;
-
-    die $err if $err && $err !~ m/doesn't contain rbd images/;
-
-    return PVE::Storage::Plugin::get_next_vm_diskname($disk_list, $storeid, $vmid, undef, $scfg);
-}
-
-sub create_base {
-    my ($class, $storeid, $scfg, $volname) = @_;
-
-    my $snap = '__base__';
-
-    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
-        $class->parse_volname($volname);
-
-    die "create_base not possible with base image\n" if $isBase;
-
-    my ($size, $parent, $format, undef) = rbd_volume_info($scfg, $storeid, $name);
-    die "rbd volume info on '$name' failed\n" if !($size);
-
-    die "rbd image must be at format V2" if $format ne "2";
-
-    die "volname '$volname' contains wrong information about parent $parent $basename\n"
-        if $basename && (!$parent || $parent ne $basename."@".$snap);
-
-    my $newname = $name;
-    $newname =~ s/^vm-/base-/;
-
-    my $newvolname = $basename ? "$basename/$newname" : "$newname";
-
-    my $cmd = $rbd_cmd->(
-       $scfg,
-       $storeid,
-       'rename',
-       get_rbd_path($scfg, $name),
-       get_rbd_path($scfg, $newname),
-    );
-    run_rbd_command($cmd, errmsg => "rbd rename '$name' error");
-
-    eval { $class->unmap_volume($storeid, $scfg, $volname); };
-    warn $@ if $@;
-
-    my $running  = undef; #fixme : is create_base always offline ?
-
-    $class->volume_snapshot($scfg, $storeid, $newname, $snap, $running);
-
-    my (undef, undef, undef, $protected) = rbd_volume_info($scfg, $storeid, $newname, $snap);
-
-    if (!$protected){
-       my $cmd = $rbd_cmd->($scfg, $storeid, 'snap', 'protect', $newname, '--snap', $snap);
-       run_rbd_command($cmd, errmsg => "rbd protect $newname snap '$snap' error");
-    }
-
-    return $newvolname;
-
-}
-
-sub clone_image {
-    my ($class, $scfg, $storeid, $volname, $vmid, $snapname) = @_;
-
-    my $snap = '__base__';
-    $snap = $snapname if length $snapname;
-
-    my ($vtype, $basename, $basevmid, undef, undef, $isBase) =
-        $class->parse_volname($volname);
-
-    die "$volname is not a base image and snapname is not provided\n" 
-       if !$isBase && !length($snapname);
-
-    my $name = $class->find_free_diskname($storeid, $scfg, $vmid);
-
-    warn "clone $volname: $basename snapname $snap to $name\n";
-
-    if (length($snapname)) {
-       my (undef, undef, undef, $protected) = rbd_volume_info($scfg, $storeid, $volname, $snapname);
-
-       if (!$protected) {
-           my $cmd = $rbd_cmd->($scfg, $storeid, 'snap', 'protect', $volname, '--snap', $snapname);
-           run_rbd_command($cmd, errmsg => "rbd protect $volname snap $snapname error");
-       }
-    }
-
-    my $newvol = "$basename/$name";
-    $newvol = $name if length($snapname);
-
-    my @options = (
-       get_rbd_path($scfg, $basename),
-       '--snap', $snap,
-    );
-    push @options, ('--data-pool', $scfg->{'data-pool'}) if $scfg->{'data-pool'};
-
-    my $cmd = $rbd_cmd->($scfg, $storeid, 'clone', @options, get_rbd_path($scfg, $name));
-    run_rbd_command($cmd, errmsg => "rbd clone '$basename' error");
-
-    return $newvol;
-}
-
-sub alloc_image {
-    my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
-
-
-    die "illegal name '$name' - should be 'vm-$vmid-*'\n"
-       if  $name && $name !~ m/^vm-$vmid-/;
-
-    $name = $class->find_free_diskname($storeid, $scfg, $vmid) if !$name;
-
-    my @options = (
-       '--image-format' , 2,
-       '--size', int(($size + 1023) / 1024),
-    );
-    push @options, ('--data-pool', $scfg->{'data-pool'}) if $scfg->{'data-pool'};
-
-    my $cmd = $rbd_cmd->($scfg, $storeid, 'create', @options, $name);
-    run_rbd_command($cmd, errmsg => "rbd create '$name' error");
-
-    return $name;
-}
-
-sub free_image {
-    my ($class, $storeid, $scfg, $volname, $isBase) = @_;
-
-    my ($vtype, $name, $vmid, undef, undef, undef) =
-       $class->parse_volname($volname);
-
-
-    my $snaps = rbd_ls_snap($scfg, $storeid, $name);
-    foreach my $snap (keys %$snaps) {
-       if ($snaps->{$snap}->{protected}) {
-           my $cmd = $rbd_cmd->($scfg, $storeid, 'snap', 'unprotect', $name, '--snap', $snap);
-           run_rbd_command($cmd, errmsg => "rbd unprotect $name snap '$snap' error");
-       }
-    }
-
-    $class->deactivate_volume($storeid, $scfg, $volname);
-
-    my $cmd = $rbd_cmd->($scfg, $storeid, 'snap', 'purge',  $name);
-    run_rbd_command($cmd, errmsg => "rbd snap purge '$name' error");
-
-    $cmd = $rbd_cmd->($scfg, $storeid, 'rm', $name);
-    run_rbd_command($cmd, errmsg => "rbd rm '$name' error");
-
-    return undef;
-}
-
-sub list_images {
-    my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
-
-    $cache->{rbd} = rbd_ls($scfg, $storeid) if !$cache->{rbd};
-
-    my $dat = $cache->{rbd}->{get_rbd_path($scfg)};
-    return [] if !$dat; # nothing found
-
-    my $res = [];
-    for my $image (sort keys %$dat) {
-       my $info = $dat->{$image};
-       my ($volname, $parent, $owner) = $info->@{'name', 'parent', 'vmid'};
-
-       if ($parent && $parent =~ m/^(base-\d+-\S+)\@__base__$/) {
-           $info->{volid} = "$storeid:$1/$volname";
-       } else {
-           $info->{volid} = "$storeid:$volname";
-       }
-
-       if ($vollist) {
-           my $found = grep { $_ eq $info->{volid} } @$vollist;
-           next if !$found;
-       } else {
-           next if defined ($vmid) && ($owner ne $vmid);
-       }
-
-       $info->{format} = 'raw';
-
-       push @$res, $info;
-    }
-
-    return $res;
-}
-
-sub status {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    my $rados = $librados_connect->($scfg, $storeid);
-    my $df = $rados->mon_command({ prefix => 'df', format => 'json' });
-
-    my $pool = $scfg->{'data-pool'} // $scfg->{pool} // 'rbd';
-
-    my ($d) = grep { $_->{name} eq $pool } @{$df->{pools}};
-
-    if (!defined($d)) {
-       warn "could not get usage stats for pool '$pool'\n";
-       return;
-    }
-
-    # max_avail -> max available space for data w/o replication in the pool
-    # bytes_used -> data w/o replication in the pool
-    my $free = $d->{stats}->{max_avail};
-    my $used = $d->{stats}->{stored} // $d->{stats}->{bytes_used};
-    my $total = $used + $free;
-    my $active = 1;
-
-    return ($total, $free, $used, $active);
-}
-
-sub activate_storage {
-    my ($class, $storeid, $scfg, $cache) = @_;
-    return 1;
-}
-
-sub deactivate_storage {
-    my ($class, $storeid, $scfg, $cache) = @_;
-    return 1;
-}
-
-sub map_volume {
-    my ($class, $storeid, $scfg, $volname, $snapname) = @_;
-
-    my ($vtype, $img_name, $vmid) = $class->parse_volname($volname);
-
-    my $name = $img_name;
-    $name .= '@'.$snapname if $snapname;
-
-    my $kerneldev = get_rbd_dev_path($scfg, $storeid, $name);
-
-    return $kerneldev if -b $kerneldev; # already mapped
-
-    # features can only be enabled/disabled for image, not for snapshot!
-    $krbd_feature_update->($scfg, $storeid, $img_name);
-
-    my $cmd = $rbd_cmd->($scfg, $storeid, 'map', $name);
-    run_rbd_command($cmd, errmsg => "can't map rbd volume $name");
-
-    return $kerneldev;
-}
-
-sub unmap_volume {
-    my ($class, $storeid, $scfg, $volname, $snapname) = @_;
-
-    my ($vtype, $name, $vmid) = $class->parse_volname($volname);
-    $name .= '@'.$snapname if $snapname;
-
-    my $kerneldev = get_rbd_dev_path($scfg, $storeid, $name);
-
-    if (-b $kerneldev) {
-       my $cmd = $rbd_cmd->($scfg, $storeid, 'unmap', $kerneldev);
-       run_rbd_command($cmd, errmsg => "can't unmap rbd device $kerneldev");
-    }
-
-    return 1;
-}
-
-sub activate_volume {
-    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
-
-    $class->map_volume($storeid, $scfg, $volname, $snapname) if $scfg->{krbd};
-
-    return 1;
-}
-
-sub deactivate_volume {
-    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
-
-    $class->unmap_volume($storeid, $scfg, $volname, $snapname);
-
-    return 1;
-}
-
-sub volume_size_info {
-    my ($class, $scfg, $storeid, $volname, $timeout) = @_;
-
-    my ($vtype, $name, $vmid) = $class->parse_volname($volname);
-    my ($size, $parent) = rbd_volume_info($scfg, $storeid, $name);
-    my $used = wantarray ? rbd_volume_du($scfg, $storeid, $name) : 0;
-    return wantarray ? ($size, 'raw', $used, $parent) : $size;
-}
-
-sub volume_resize {
-    my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
-
-    return 1 if $running && !$scfg->{krbd}; # FIXME???
-
-    my ($vtype, $name, $vmid) = $class->parse_volname($volname);
-
-    my $cmd = $rbd_cmd->($scfg, $storeid, 'resize', '--allow-shrink', '--size', ($size/1024/1024), $name);
-    run_rbd_command($cmd, errmsg => "rbd resize '$volname' error");
-    return undef;
-}
-
-sub volume_snapshot {
-    my ($class, $scfg, $storeid, $volname, $snap) = @_;
-
-    my ($vtype, $name, $vmid) = $class->parse_volname($volname);
-
-    my $cmd = $rbd_cmd->($scfg, $storeid, 'snap', 'create', '--snap', $snap, $name);
-    run_rbd_command($cmd, errmsg => "rbd snapshot '$volname' error");
-    return undef;
-}
-
-sub volume_snapshot_rollback {
-    my ($class, $scfg, $storeid, $volname, $snap) = @_;
-
-    my ($vtype, $name, $vmid) = $class->parse_volname($volname);
-
-    my $cmd = $rbd_cmd->($scfg, $storeid, 'snap', 'rollback', '--snap', $snap, $name);
-    run_rbd_command($cmd, errmsg => "rbd snapshot $volname to '$snap' error");
-}
-
-sub volume_snapshot_delete {
-    my ($class, $scfg, $storeid, $volname, $snap, $running) = @_;
-
-    $class->deactivate_volume($storeid, $scfg, $volname, $snap, {});
-
-    my ($vtype, $name, $vmid) = $class->parse_volname($volname);
-
-    my (undef, undef, undef, $protected) = rbd_volume_info($scfg, $storeid, $name, $snap);
-    if ($protected){
-       my $cmd = $rbd_cmd->($scfg, $storeid, 'snap', 'unprotect', $name, '--snap', $snap);
-       run_rbd_command($cmd, errmsg => "rbd unprotect $name snap '$snap' error");
-    }
-
-    my $cmd = $rbd_cmd->($scfg, $storeid, 'snap', 'rm', '--snap', $snap, $name);
-
-    run_rbd_command($cmd, errmsg => "rbd snapshot '$volname' error");
-
-    return undef;
-}
-
-sub volume_snapshot_needs_fsfreeze {
-    return 1;
-}
-
-sub volume_has_feature {
-    my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
-
-   my $features = {
-       snapshot => { current => 1, snap => 1},
-       clone => { base => 1, snap => 1},
-       template => { current => 1},
-       copy => { base => 1, current => 1, snap => 1},
-       sparseinit => { base => 1, current => 1},
-       rename => {current => 1},
-    };
-
-    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) = $class->parse_volname($volname);
-
-    my $key = undef;
-    if ($snapname){
-       $key = 'snap';
-    } else {
-       $key = $isBase ? 'base' : 'current';
-    }
-    return 1 if $features->{$feature}->{$key};
-
-    return undef;
-}
-
-sub rename_volume {
-    my ($class, $scfg, $storeid, $source_volname, $target_vmid, $target_volname) = @_;
-
-    my (
-       undef,
-       $source_image,
-       $source_vmid,
-       $base_name,
-       $base_vmid,
-       undef,
-       $format
-    ) = $class->parse_volname($source_volname);
-    $target_volname = $class->find_free_diskname($storeid, $scfg, $target_vmid, $format)
-       if !$target_volname;
-
-    eval {
-       my $cmd = $rbd_cmd->($scfg, $storeid, 'info', $target_volname);
-       run_rbd_command($cmd, errmsg => "exist check",  quiet => 1);
-    };
-    die "target volume '${target_volname}' already exists\n" if !$@;
-
-    my $cmd = $rbd_cmd->($scfg, $storeid, 'rename', $source_image, $target_volname);
-
-    run_rbd_command(
-       $cmd,
-       errmsg => "could not rename image '${source_image}' to '${target_volname}'",
-    );
-
-    eval { $class->unmap_volume($storeid, $scfg, $source_volname); };
-    warn $@ if $@;
-
-    $base_name = $base_name ? "${base_name}/" : '';
-
-    return "${storeid}:${base_name}${target_volname}";
-}
-
-1;
diff --git a/PVE/Storage/ZFSPlugin.pm b/PVE/Storage/ZFSPlugin.pm
deleted file mode 100644 (file)
index d4dc2a4..0000000
+++ /dev/null
@@ -1,422 +0,0 @@
-package PVE::Storage::ZFSPlugin;
-
-use strict;
-use warnings;
-use IO::File;
-use POSIX;
-use PVE::Tools qw(run_command);
-use PVE::Storage::ZFSPoolPlugin;
-use PVE::RPCEnvironment;
-
-use base qw(PVE::Storage::ZFSPoolPlugin);
-use PVE::Storage::LunCmd::Comstar;
-use PVE::Storage::LunCmd::Istgt;
-use PVE::Storage::LunCmd::Iet;
-use PVE::Storage::LunCmd::LIO;
-
-
-my @ssh_opts = ('-o', 'BatchMode=yes');
-my @ssh_cmd = ('/usr/bin/ssh', @ssh_opts);
-my $id_rsa_path = '/etc/pve/priv/zfs';
-
-my $lun_cmds = {
-    create_lu   => 1,
-    delete_lu   => 1,
-    import_lu   => 1,
-    modify_lu   => 1,
-    add_view    => 1,
-    list_view   => 1,
-    list_lu     => 1,
-};
-
-my $zfs_unknown_scsi_provider = sub {
-    my ($provider) = @_;
-
-    die "$provider: unknown iscsi provider. Available [comstar, istgt, iet, LIO]";
-};
-
-my $zfs_get_base = sub {
-    my ($scfg) = @_;
-
-    if ($scfg->{iscsiprovider} eq 'comstar') {
-        return PVE::Storage::LunCmd::Comstar::get_base;
-    } elsif ($scfg->{iscsiprovider} eq 'istgt') {
-        return PVE::Storage::LunCmd::Istgt::get_base;
-    } elsif ($scfg->{iscsiprovider} eq 'iet') {
-        return PVE::Storage::LunCmd::Iet::get_base;
-    } elsif ($scfg->{iscsiprovider} eq 'LIO') {
-        return PVE::Storage::LunCmd::LIO::get_base;
-    } else {
-        $zfs_unknown_scsi_provider->($scfg->{iscsiprovider});
-    }
-};
-
-sub zfs_request {
-    my ($class, $scfg, $timeout, $method, @params) = @_;
-
-    $timeout = PVE::RPCEnvironment->is_worker() ? 60*60 : 10
-       if !$timeout;
-
-    my $msg = '';
-
-    if ($lun_cmds->{$method}) {
-        if ($scfg->{iscsiprovider} eq 'comstar') {
-            $msg = PVE::Storage::LunCmd::Comstar::run_lun_command($scfg, $timeout, $method, @params);
-        } elsif ($scfg->{iscsiprovider} eq 'istgt') {
-            $msg = PVE::Storage::LunCmd::Istgt::run_lun_command($scfg, $timeout, $method, @params);
-        } elsif ($scfg->{iscsiprovider} eq 'iet') {
-            $msg = PVE::Storage::LunCmd::Iet::run_lun_command($scfg, $timeout, $method, @params);
-        } elsif ($scfg->{iscsiprovider} eq 'LIO') {
-            $msg = PVE::Storage::LunCmd::LIO::run_lun_command($scfg, $timeout, $method, @params);
-        } else {
-            $zfs_unknown_scsi_provider->($scfg->{iscsiprovider});
-        }
-    } else {
-
-       my $target = 'root@' . $scfg->{portal};
-
-       my $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target];
-
-        if ($method eq 'zpool_list') {
-           push @$cmd, 'zpool', 'list';
-       } else {
-           push @$cmd, 'zfs', $method;
-        }
-
-       push @$cmd, @params;
-
-       my $output = sub {
-           my $line = shift;
-           $msg .= "$line\n";
-        };
-
-        run_command($cmd, outfunc => $output, timeout => $timeout);
-    }
-
-    return $msg;
-}
-
-sub zfs_get_lu_name {
-    my ($class, $scfg, $zvol) = @_;
-
-    my $base = $zfs_get_base->($scfg);
-
-    $zvol = ($class->parse_volname($zvol))[1];
-
-    my $object = ($zvol =~ /^.+\/.+/) ? "$base/$zvol" : "$base/$scfg->{pool}/$zvol";
-
-    my $lu_name = $class->zfs_request($scfg, undef, 'list_lu', $object);
-
-    return $lu_name if $lu_name;
-
-    die "Could not find lu_name for zvol $zvol";
-}
-
-sub zfs_add_lun_mapping_entry {
-    my ($class, $scfg, $zvol, $guid) = @_;
-
-    if (!defined($guid)) {
-       $guid = $class->zfs_get_lu_name($scfg, $zvol);
-    }
-
-    $class->zfs_request($scfg, undef, 'add_view', $guid);
-}
-
-sub zfs_delete_lu {
-    my ($class, $scfg, $zvol) = @_;
-
-    my $guid = $class->zfs_get_lu_name($scfg, $zvol);
-
-    $class->zfs_request($scfg, undef, 'delete_lu', $guid);
-}
-
-sub zfs_create_lu {
-    my ($class, $scfg, $zvol) = @_;
-
-    my $base = $zfs_get_base->($scfg);
-    my $guid = $class->zfs_request($scfg, undef, 'create_lu', "$base/$scfg->{pool}/$zvol");
-
-    return $guid;
-}
-
-sub zfs_import_lu {
-    my ($class, $scfg, $zvol) = @_;
-
-    my $base = $zfs_get_base->($scfg);
-    $class->zfs_request($scfg, undef, 'import_lu', "$base/$scfg->{pool}/$zvol");
-}
-
-sub zfs_resize_lu {
-    my ($class, $scfg, $zvol, $size) = @_;
-
-    my $guid = $class->zfs_get_lu_name($scfg, $zvol);
-
-    $class->zfs_request($scfg, undef, 'modify_lu', "${size}K", $guid);
-}
-
-sub zfs_get_lun_number {
-    my ($class, $scfg, $guid) = @_;
-
-    die "could not find lun_number for guid $guid" if !$guid;
-
-    if ($class->zfs_request($scfg, undef, 'list_view', $guid) =~ /^(\d+)$/) {
-       return $1;
-    }
-
-    die "lun_number for guid $guid is not a number";
-}
-
-# Configuration
-
-sub type {
-    return 'zfs';
-}
-
-sub plugindata {
-    return {
-       content => [ {images => 1}, { images => 1 }],
-    };
-}
-
-sub properties {
-    return {
-       iscsiprovider => {
-           description => "iscsi provider",
-           type => 'string',
-       },
-       # this will disable write caching on comstar and istgt.
-       # it is not implemented for iet. iet blockio always operates with
-       # writethrough caching when not in readonly mode
-       nowritecache => {
-           description => "disable write caching on the target",
-           type => 'boolean',
-       },
-       comstar_tg => {
-           description => "target group for comstar views",
-           type => 'string',
-       },
-       comstar_hg => {
-           description => "host group for comstar views",
-           type => 'string',
-       },
-       lio_tpg => {
-           description => "target portal group for Linux LIO targets",
-           type => 'string',
-       },
-    };
-}
-
-sub options {
-    return {
-       nodes => { optional => 1 },
-       disable => { optional => 1 },
-       portal => { fixed => 1 },
-       target => { fixed => 1 },
-       pool => { fixed => 1 },
-       blocksize => { fixed => 1 },
-       iscsiprovider => { fixed => 1 },
-       nowritecache => { optional => 1 },
-       sparse => { optional => 1 },
-       comstar_hg => { optional => 1 },
-       comstar_tg => { optional => 1 },
-       lio_tpg => { optional => 1 },
-       content => { optional => 1 },
-       bwlimit => { optional => 1 },
-    };
-}
-
-# Storage implementation
-
-sub path {
-    my ($class, $scfg, $volname, $storeid, $snapname) = @_;
-
-    die "direct access to snapshots not implemented"
-       if defined($snapname);
-
-    my ($vtype, $name, $vmid) = $class->parse_volname($volname);
-
-    my $target = $scfg->{target};
-    my $portal = $scfg->{portal};
-
-    my $guid = $class->zfs_get_lu_name($scfg, $name);
-    my $lun = $class->zfs_get_lun_number($scfg, $guid);
-
-    my $path = "iscsi://$portal/$target/$lun";
-
-    return ($path, $vmid, $vtype);
-}
-
-sub create_base {
-    my ($class, $storeid, $scfg, $volname) = @_;
-
-    my $snap = '__base__';
-
-    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
-        $class->parse_volname($volname);
-
-    die "create_base not possible with base image\n" if $isBase;
-
-    my $newname = $name;
-    $newname =~ s/^vm-/base-/;
-
-    my $newvolname = $basename ? "$basename/$newname" : "$newname";
-
-    $class->zfs_delete_lu($scfg, $name);
-    $class->zfs_request($scfg, undef, 'rename', "$scfg->{pool}/$name", "$scfg->{pool}/$newname");
-
-    my $guid = $class->zfs_create_lu($scfg, $newname);
-    $class->zfs_add_lun_mapping_entry($scfg, $newname, $guid);
-
-    my $running  = undef; #fixme : is create_base always offline ?
-
-    $class->volume_snapshot($scfg, $storeid, $newname, $snap, $running);
-
-    return $newvolname;
-}
-
-sub clone_image {
-    my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
-
-    my $name = $class->SUPER::clone_image($scfg, $storeid, $volname, $vmid, $snap);
-
-    # get ZFS dataset name from PVE volname
-    my (undef, $clonedname) = $class->parse_volname($name);
-
-    my $guid = $class->zfs_create_lu($scfg, $clonedname);
-    $class->zfs_add_lun_mapping_entry($scfg, $clonedname, $guid);
-
-    return $name;
-}
-
-sub alloc_image {
-    my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
-    
-    die "unsupported format '$fmt'" if $fmt ne 'raw';
-
-    die "illegal name '$name' - should be 'vm-$vmid-*'\n"
-    if $name && $name !~ m/^vm-$vmid-/;
-
-    my $volname = $name;
-
-    $volname = $class->find_free_diskname($storeid, $scfg, $vmid, $fmt) if !$volname;
-    
-    $class->zfs_create_zvol($scfg, $volname, $size);
-    my $guid = $class->zfs_create_lu($scfg, $volname);
-    $class->zfs_add_lun_mapping_entry($scfg, $volname, $guid);
-
-    return $volname;
-}
-
-sub free_image {
-    my ($class, $storeid, $scfg, $volname, $isBase) = @_;
-
-    my ($vtype, $name, $vmid) = $class->parse_volname($volname);
-
-    $class->zfs_delete_lu($scfg, $name);
-
-    eval { $class->zfs_delete_zvol($scfg, $name); };
-    if (my $err = $@) {
-        my $guid = $class->zfs_create_lu($scfg, $name);
-        $class->zfs_add_lun_mapping_entry($scfg, $name, $guid);
-        die $err;
-    }
-
-    return undef;
-}
-
-sub volume_resize {
-    my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
-
-    $volname = ($class->parse_volname($volname))[1];
-
-    my $new_size = $class->SUPER::volume_resize($scfg, $storeid, $volname, $size, $running);
-
-    $class->zfs_resize_lu($scfg, $volname, $new_size);
-
-    return $new_size;
-}
-
-sub volume_snapshot_delete {
-    my ($class, $scfg, $storeid, $volname, $snap, $running) = @_;
-
-    $volname = ($class->parse_volname($volname))[1];
-
-    $class->zfs_request($scfg, undef, 'destroy', "$scfg->{pool}/$volname\@$snap");
-}
-
-sub volume_snapshot_rollback {
-    my ($class, $scfg, $storeid, $volname, $snap) = @_;
-
-    $volname = ($class->parse_volname($volname))[1];
-
-    $class->zfs_delete_lu($scfg, $volname);
-
-    $class->zfs_request($scfg, undef, 'rollback', "$scfg->{pool}/$volname\@$snap");
-
-    $class->zfs_import_lu($scfg, $volname);
-
-    $class->zfs_add_lun_mapping_entry($scfg, $volname);
-}
-
-sub storage_can_replicate {
-    my ($class, $scfg, $storeid, $format) = @_;
-
-    return 0;
-}
-
-sub volume_has_feature {
-    my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
-
-    my $features = {
-       snapshot => { current => 1, snap => 1},
-       clone => { base => 1},
-       template => { current => 1},
-       copy => { base => 1, current => 1},
-    };
-
-    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
-       $class->parse_volname($volname);
-
-    my $key = undef;
-
-    if ($snapname) {
-       $key = 'snap';
-    } else {
-       $key = $isBase ? 'base' : 'current';
-    }
-
-    return 1 if $features->{$feature}->{$key};
-
-    return undef;
-}
-
-sub activate_storage {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    return 1;
-}
-
-sub deactivate_storage {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    return 1;
-}
-
-sub activate_volume {
-    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
-
-    die "unable to activate snapshot from remote zfs storage" if $snapname;
-
-    return 1;
-}
-
-sub deactivate_volume {
-    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
-
-    die "unable to deactivate snapshot from remote zfs storage" if $snapname;
-
-    return 1;
-}
-
-1;
diff --git a/PVE/Storage/ZFSPoolPlugin.pm b/PVE/Storage/ZFSPoolPlugin.pm
deleted file mode 100644 (file)
index 951d027..0000000
+++ /dev/null
@@ -1,852 +0,0 @@
-package PVE::Storage::ZFSPoolPlugin;
-
-use strict;
-use warnings;
-
-use IO::File;
-use Net::IP;
-use POSIX;
-
-use PVE::ProcFSTools;
-use PVE::RPCEnvironment;
-use PVE::Storage::Plugin;
-use PVE::Tools qw(run_command);
-
-use base qw(PVE::Storage::Plugin);
-
-sub type {
-    return 'zfspool';
-}
-
-sub plugindata {
-    return {
-       content => [ {images => 1, rootdir => 1}, {images => 1 , rootdir => 1}],
-       format => [ { raw => 1, subvol => 1 } , 'raw' ],
-    };
-}
-
-sub properties {
-    return {
-       blocksize => {
-           description => "block size",
-           type => 'string',
-       },
-       sparse => {
-           description => "use sparse volumes",
-           type => 'boolean',
-       },
-       mountpoint => {
-           description => "mount point",
-           type => 'string', format => 'pve-storage-path',
-       },
-    };
-}
-
-sub options {
-    return {
-       pool => { fixed => 1 },
-       blocksize => { optional => 1 },
-       sparse => { optional => 1 },
-       nodes => { optional => 1 },
-       disable => { optional => 1 },
-       content => { optional => 1 },
-       bwlimit => { optional => 1 },
-       mountpoint => { optional => 1 },
-    };
-}
-
-# static zfs helper methods
-
-sub zfs_parse_zvol_list {
-    my ($text, $pool) = @_;
-
-    my $list = ();
-
-    return $list if !$text;
-
-    my @lines = split /\n/, $text;
-    foreach my $line (@lines) {
-       my ($dataset, $size, $origin, $type, $refquota) = split(/\s+/, $line);
-       next if !($type eq 'volume' || $type eq 'filesystem');
-
-       my $zvol = {};
-       my @parts = split /\//, $dataset;
-       next if scalar(@parts) < 2; # we need pool/name
-       my $name = pop @parts;
-       my $parsed_pool = join('/', @parts);
-       next if $parsed_pool ne $pool;
-
-       next unless $name =~ m!^(vm|base|subvol|basevol)-(\d+)-(\S+)$!;
-       $zvol->{owner} = $2;
-
-       $zvol->{name} = $name;
-       if ($type eq 'filesystem') {
-           if ($refquota eq 'none') {
-               $zvol->{size} = 0;
-           } else {
-               $zvol->{size} = $refquota + 0;
-           }
-           $zvol->{format} = 'subvol';
-       } else {
-           $zvol->{size} = $size + 0;
-           $zvol->{format} = 'raw';
-       }
-       if ($origin !~ /^-$/) {
-           $zvol->{origin} = $origin;
-       }
-       push @$list, $zvol;
-    }
-
-    return $list;
-}
-
-sub parse_volname {
-    my ($class, $volname) = @_;
-
-    if ($volname =~ m/^(((base|basevol)-(\d+)-\S+)\/)?((base|basevol|vm|subvol)-(\d+)-\S+)$/) {
-       my $format = ($6 eq 'subvol' || $6 eq 'basevol') ? 'subvol' : 'raw';
-       my $isBase = ($6 eq 'base' || $6 eq 'basevol');
-       return ('images', $5, $7, $2, $4, $isBase, $format);
-    }
-
-    die "unable to parse zfs volume name '$volname'\n";
-}
-
-# virtual zfs methods (subclass can overwrite them)
-
-sub on_add_hook {
-    my ($class, $storeid, $scfg, %param) = @_;
-
-    my $cfg_mountpoint = $scfg->{mountpoint};
-
-    # ignore failure, pool might currently not be imported
-    my $mountpoint;
-    eval {
-       my $res = $class->zfs_get_properties($scfg, 'mountpoint', $scfg->{pool}, 1);
-       $mountpoint = PVE::Storage::Plugin::verify_path($res, 1) if defined($res);
-    };
-
-    if (defined($cfg_mountpoint)) {
-       if (defined($mountpoint) && !($cfg_mountpoint =~ m|^\Q$mountpoint\E/?$|)) {
-           warn "warning for $storeid - mountpoint: $cfg_mountpoint " .
-                "does not match current mount point: $mountpoint\n";
-       }
-    } else {
-       $scfg->{mountpoint} = $mountpoint;
-    }
-
-    return;
-}
-
-sub path {
-    my ($class, $scfg, $volname, $storeid, $snapname) = @_;
-
-    my ($vtype, $name, $vmid) = $class->parse_volname($volname);
-
-    my $path = '';
-    my $mountpoint = $scfg->{mountpoint} // "/$scfg->{pool}";
-
-    if ($vtype eq "images") {
-       if ($name =~ m/^subvol-/ || $name =~ m/^basevol-/) {
-           $path = "$mountpoint/$name";
-       } else {
-           $path = "/dev/zvol/$scfg->{pool}/$name";
-       }
-       $path .= "\@$snapname" if defined($snapname);
-    } else {
-       die "$vtype is not allowed in ZFSPool!";
-    }
-
-    return ($path, $vmid, $vtype);
-}
-
-sub zfs_request {
-    my ($class, $scfg, $timeout, $method, @params) = @_;
-
-    my $cmd = [];
-
-    if ($method eq 'zpool_list') {
-       push @$cmd, 'zpool', 'list';
-    } elsif ($method eq 'zpool_import') {
-       push @$cmd, 'zpool', 'import';
-       $timeout = 15 if !$timeout || $timeout < 15;
-    } else {
-       push @$cmd, 'zfs', $method;
-    }
-    push @$cmd, @params;
-
-    my $msg = '';
-    my $output = sub { $msg .= "$_[0]\n" };
-
-    if (PVE::RPCEnvironment->is_worker()) {
-       $timeout = 60*60 if !$timeout;
-       $timeout = 60*5 if $timeout < 60*5;
-    } else {
-       $timeout = 10 if !$timeout;
-    }
-
-    run_command($cmd, errmsg => "zfs error", outfunc => $output, timeout => $timeout);
-
-    return $msg;
-}
-
-sub zfs_wait_for_zvol_link {
-    my ($class, $scfg, $volname, $timeout) = @_;
-
-    my $default_timeout = PVE::RPCEnvironment->is_worker() ? 60*5 : 10;
-    $timeout = $default_timeout if !defined($timeout);
-
-    my ($devname, undef, undef) = $class->path($scfg, $volname);
-
-    for (my $i = 1; $i <= $timeout; $i++) {
-       last if -b $devname;
-       die "timeout: no zvol device link for '$volname' found after $timeout sec found.\n"
-           if $i == $timeout;
-
-       sleep(1);
-    }
-}
-
-sub alloc_image {
-    my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
-
-    my $volname = $name;
-
-    if ($fmt eq 'raw') {
-
-       die "illegal name '$volname' - should be 'vm-$vmid-*'\n"
-           if $volname && $volname !~ m/^vm-$vmid-/;
-       $volname = $class->find_free_diskname($storeid, $scfg, $vmid, $fmt)
-           if !$volname;
-
-       $class->zfs_create_zvol($scfg, $volname, $size);
-       $class->zfs_wait_for_zvol_link($scfg, $volname);
-
-    } elsif ( $fmt eq 'subvol') {
-
-       die "illegal name '$volname' - should be 'subvol-$vmid-*'\n"
-           if $volname && $volname !~ m/^subvol-$vmid-/;
-       $volname = $class->find_free_diskname($storeid, $scfg, $vmid, $fmt)
-           if !$volname;
-
-       die "illegal name '$volname' - should be 'subvol-$vmid-*'\n"
-           if $volname !~ m/^subvol-$vmid-/;
-
-       $class->zfs_create_subvol($scfg, $volname, $size);
-
-    } else {
-       die "unsupported format '$fmt'";
-    }
-
-    return $volname;
-}
-
-sub free_image {
-    my ($class, $storeid, $scfg, $volname, $isBase) = @_;
-
-    my (undef, $name, undef) = $class->parse_volname($volname);
-
-    $class->zfs_delete_zvol($scfg, $name);
-
-    return undef;
-}
-
-sub list_images {
-    my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
-
-    my $zfs_list = $class->zfs_list_zvol($scfg);
-
-    my $res = [];
-
-    for my $info (values $zfs_list->%*) {
-       my $volname = $info->{name};
-       my $parent = $info->{parent};
-       my $owner = $info->{vmid};
-
-       if ($parent && $parent =~ m/^(\S+)\@__base__$/) {
-           my ($basename) = ($1);
-           $info->{volid} = "$storeid:$basename/$volname";
-       } else {
-           $info->{volid} = "$storeid:$volname";
-       }
-
-       if ($vollist) {
-           my $found = grep { $_ eq $info->{volid} } @$vollist;
-           next if !$found;
-       } else {
-           next if defined ($vmid) && ($owner ne $vmid);
-       }
-
-       push @$res, $info;
-    }
-    return $res;
-}
-
-sub zfs_get_properties {
-    my ($class, $scfg, $properties, $dataset, $timeout) = @_;
-
-    my $result = $class->zfs_request($scfg, $timeout, 'get', '-o', 'value',
-                                    '-Hp', $properties, $dataset);
-    my @values = split /\n/, $result;
-    return wantarray ? @values : $values[0];
-}
-
-sub zfs_get_pool_stats {
-    my ($class, $scfg) = @_;
-
-    my $available = 0;
-    my $used = 0;
-
-    my @lines = $class->zfs_get_properties($scfg, 'available,used', $scfg->{pool});
-
-    if($lines[0] =~ /^(\d+)$/) {
-       $available = $1;
-    }
-
-    if($lines[1] =~ /^(\d+)$/) {
-       $used = $1;
-    }
-
-    return ($available, $used);
-}
-
-sub zfs_create_zvol {
-    my ($class, $scfg, $zvol, $size) = @_;
-
-    # always align size to 1M as workaround until
-    # https://github.com/zfsonlinux/zfs/issues/8541 is solved
-    my $padding = (1024 - $size % 1024) % 1024;
-    $size = $size + $padding;
-
-    my $cmd = ['create'];
-
-    push @$cmd, '-s' if $scfg->{sparse};
-
-    push @$cmd, '-b', $scfg->{blocksize} if $scfg->{blocksize};
-
-    push @$cmd, '-V', "${size}k", "$scfg->{pool}/$zvol";
-
-    $class->zfs_request($scfg, undef, @$cmd);
-}
-
-sub zfs_create_subvol {
-    my ($class, $scfg, $volname, $size) = @_;
-
-    my $dataset = "$scfg->{pool}/$volname";
-    my $quota = $size ? "${size}k" : "none";
-
-    my $cmd = ['create', '-o', 'acltype=posixacl', '-o', 'xattr=sa',
-              '-o', "refquota=${quota}", $dataset];
-
-    $class->zfs_request($scfg, undef, @$cmd);
-}
-
-sub zfs_delete_zvol {
-    my ($class, $scfg, $zvol) = @_;
-
-    my $err;
-
-    for (my $i = 0; $i < 6; $i++) {
-
-       eval { $class->zfs_request($scfg, undef, 'destroy', '-r', "$scfg->{pool}/$zvol"); };
-       if ($err = $@) {
-           if ($err =~ m/^zfs error:(.*): dataset is busy.*/) {
-               sleep(1);
-           } elsif ($err =~ m/^zfs error:.*: dataset does not exist.*$/) {
-               $err = undef;
-               last;
-           } else {
-               die $err;
-           }
-       } else {
-           last;
-       }
-    }
-
-    die $err if $err;
-}
-
-sub zfs_list_zvol {
-    my ($class, $scfg) = @_;
-
-    my $text = $class->zfs_request(
-       $scfg,
-       10,
-       'list',
-       '-o',
-       'name,volsize,origin,type,refquota',
-       '-t',
-       'volume,filesystem',
-       '-d1',
-       '-Hp',
-       $scfg->{pool},
-    );
-    # It's still required to have zfs_parse_zvol_list filter by pool, because -d1 lists
-    # $scfg->{pool} too and while unlikely, it could be named to be mistaken for a volume.
-    my $zvols = zfs_parse_zvol_list($text, $scfg->{pool});
-    return {} if !$zvols;
-
-    my $list = {};
-    foreach my $zvol (@$zvols) {
-       my $name = $zvol->{name};
-       my $parent = $zvol->{origin};
-       if($zvol->{origin} && $zvol->{origin} =~ m/^$scfg->{pool}\/(\S+)$/){
-           $parent = $1;
-       }
-
-       $list->{$name} = {
-           name => $name,
-           size => $zvol->{size},
-           parent => $parent,
-           format => $zvol->{format},
-            vmid => $zvol->{owner},
-        };
-    }
-
-    return $list;
-}
-
-sub zfs_get_sorted_snapshot_list {
-    my ($class, $scfg, $volname, $sort_params) = @_;
-
-    my @params = ('-H', '-r', '-t', 'snapshot', '-o', 'name', $sort_params->@*);
-
-    my $vname = ($class->parse_volname($volname))[1];
-    push @params, "$scfg->{pool}\/$vname";
-
-    my $text = $class->zfs_request($scfg, undef, 'list', @params);
-    my @snapshots = split(/\n/, $text);
-
-    my $snap_names = [];
-    for my $snapshot (@snapshots) {
-       (my $snap_name = $snapshot) =~ s/^.*@//;
-       push $snap_names->@*, $snap_name;
-    }
-    return $snap_names;
-}
-
-sub status {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    my $total = 0;
-    my $free = 0;
-    my $used = 0;
-    my $active = 0;
-
-    eval {
-       ($free, $used) = $class->zfs_get_pool_stats($scfg);
-       $active = 1;
-       $total = $free + $used;
-    };
-    warn $@ if $@;
-
-    return ($total, $free, $used, $active);
-}
-
-sub volume_size_info {
-    my ($class, $scfg, $storeid, $volname, $timeout) = @_;
-
-    my (undef, $vname, undef, $parent, undef, undef, $format) =
-        $class->parse_volname($volname);
-
-    my $attr = $format eq 'subvol' ? 'refquota' : 'volsize';
-    my ($size, $used) = $class->zfs_get_properties($scfg, "$attr,usedbydataset", "$scfg->{pool}/$vname");
-
-    $used = ($used =~ /^(\d+)$/) ? $1 : 0;
-
-    if ($size =~ /^(\d+)$/) {
-       return wantarray ? ($1, $format, $used, $parent) : $1;
-    }
-
-    die "Could not get zfs volume size\n";
-}
-
-sub volume_snapshot {
-    my ($class, $scfg, $storeid, $volname, $snap) = @_;
-
-    my $vname = ($class->parse_volname($volname))[1];
-
-    $class->zfs_request($scfg, undef, 'snapshot', "$scfg->{pool}/$vname\@$snap");
-}
-
-sub volume_snapshot_delete {
-    my ($class, $scfg, $storeid, $volname, $snap, $running) = @_;
-
-    my $vname = ($class->parse_volname($volname))[1];
-
-    $class->deactivate_volume($storeid, $scfg, $vname, $snap, {});
-    $class->zfs_request($scfg, undef, 'destroy', "$scfg->{pool}/$vname\@$snap");
-}
-
-sub volume_snapshot_rollback {
-    my ($class, $scfg, $storeid, $volname, $snap) = @_;
-
-    my (undef, $vname, undef, undef, undef, undef, $format) = $class->parse_volname($volname);
-
-    my $msg = $class->zfs_request($scfg, undef, 'rollback', "$scfg->{pool}/$vname\@$snap");
-
-    # we have to unmount rollbacked subvols, to invalidate wrong kernel
-    # caches, they get mounted in activate volume again
-    # see zfs bug #10931 https://github.com/openzfs/zfs/issues/10931
-    if ($format eq 'subvol') {
-       eval { $class->zfs_request($scfg, undef, 'unmount', "$scfg->{pool}/$vname"); };
-       if (my $err = $@) {
-           die $err if $err !~ m/not currently mounted$/;
-       }
-    }
-
-    return $msg;
-}
-
-sub volume_rollback_is_possible {
-    my ($class, $scfg, $storeid, $volname, $snap, $blockers) = @_;
-
-    # can't use '-S creation', because zfs list won't reverse the order when the
-    # creation time is the same second, breaking at least our tests.
-    my $snapshots = $class->zfs_get_sorted_snapshot_list($scfg, $volname, ['-s', 'creation']);
-
-    my $found;
-    $blockers //= []; # not guaranteed to be set by caller
-    for my $snapshot ($snapshots->@*) {
-       if ($snapshot eq $snap) {
-           $found = 1;
-       } elsif ($found) {
-           push $blockers->@*, $snapshot;
-       }
-    }
-
-    my $volid = "${storeid}:${volname}";
-
-    die "can't rollback, snapshot '$snap' does not exist on '$volid'\n"
-       if !$found;
-
-    die "can't rollback, '$snap' is not most recent snapshot on '$volid'\n"
-       if scalar($blockers->@*) > 0;
-
-    return 1;
-}
-
-sub volume_snapshot_info {
-    my ($class, $scfg, $storeid, $volname) = @_;
-
-    my @params = ('-Hp', '-r', '-t', 'snapshot', '-o', 'name,guid,creation');
-
-    my $vname = ($class->parse_volname($volname))[1];
-    push @params, "$scfg->{pool}\/$vname";
-
-    my $text = $class->zfs_request($scfg, undef, 'list', @params);
-    my @lines = split(/\n/, $text);
-
-    my $info = {};
-    for my $line (@lines) {
-       my ($snapshot, $guid, $creation) = split(/\s+/, $line);
-       (my $snap_name = $snapshot) =~ s/^.*@//;
-
-       $info->{$snap_name} = {
-           id => $guid,
-           timestamp => $creation,
-       };
-    }
-    return $info;
-}
-
-my sub dataset_mounted_heuristic {
-    my ($dataset) = @_;
-
-    my $mounts = PVE::ProcFSTools::parse_proc_mounts();
-    for my $mp (@$mounts) {
-       my ($what, $dir, $fs) = $mp->@*;
-       next if $fs ne 'zfs';
-       # check for root-dataset or any child-dataset (root-dataset could have 'canmount=off')
-       # If any child is mounted heuristically assume that `zfs mount -a` was successful
-       next if $what !~ m!^$dataset(?:/|$)!;
-       return 1;
-    }
-    return 0;
-}
-
-sub activate_storage {
-    my ($class, $storeid, $scfg, $cache) = @_;
-
-    # Note: $scfg->{pool} can include dataset <pool>/<dataset>
-    my $dataset = $scfg->{pool};
-    my $pool = ($dataset =~ s!/.*$!!r);
-
-    return 1 if dataset_mounted_heuristic($dataset); # early return
-
-    my $pool_imported = sub {
-       my @param = ('-o', 'name', '-H', $pool);
-       my $res = eval { $class->zfs_request($scfg, undef, 'zpool_list', @param) };
-       warn "$@\n" if $@;
-
-       return defined($res) && $res =~ m/$pool/;
-    };
-
-    if (!$pool_imported->()) {
-       # import can only be done if not yet imported!
-       my @param = ('-d', '/dev/disk/by-id/', '-o', 'cachefile=none', $pool);
-       eval { $class->zfs_request($scfg, undef, 'zpool_import', @param) };
-       if (my $err = $@) {
-           # just could've raced with another import, so recheck if it is imported
-           die "could not activate storage '$storeid', $err\n" if !$pool_imported->();
-       }
-    }
-    eval { $class->zfs_request($scfg, undef, 'mount', '-a') };
-    die "could not activate storage '$storeid', $@\n" if $@;
-    return 1;
-}
-
-sub deactivate_storage {
-    my ($class, $storeid, $scfg, $cache) = @_;
-    return 1;
-}
-
-sub activate_volume {
-    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
-
-    return 1 if defined($snapname);
-
-    my (undef, $dataset, undef, undef, undef, undef, $format) = $class->parse_volname($volname);
-
-    if ($format eq 'raw') {
-       $class->zfs_wait_for_zvol_link($scfg, $volname);
-    } elsif ($format eq 'subvol') {
-       my $mounted = $class->zfs_get_properties($scfg, 'mounted', "$scfg->{pool}/$dataset");
-       if ($mounted !~ m/^yes$/) {
-           $class->zfs_request($scfg, undef, 'mount', "$scfg->{pool}/$dataset");
-       }
-    }
-
-    return 1;
-}
-
-sub deactivate_volume {
-    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
-    return 1;
-}
-
-sub clone_image {
-    my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
-
-    $snap ||= '__base__';
-
-    my ($vtype, $basename, $basevmid, undef, undef, $isBase, $format) =
-        $class->parse_volname($volname);
-
-    die "clone_image only works on base images\n" if !$isBase;
-
-    my $name = $class->find_free_diskname($storeid, $scfg, $vmid, $format);
-
-    if ($format eq 'subvol') {
-       my $size = $class->zfs_request($scfg, undef, 'list', '-Hp', '-o', 'refquota', "$scfg->{pool}/$basename");
-       chomp($size);
-       $class->zfs_request($scfg, undef, 'clone', "$scfg->{pool}/$basename\@$snap", "$scfg->{pool}/$name", '-o', "refquota=$size");
-    } else {
-       $class->zfs_request($scfg, undef, 'clone', "$scfg->{pool}/$basename\@$snap", "$scfg->{pool}/$name");
-    }
-
-    return "$basename/$name";
-}
-
-sub create_base {
-    my ($class, $storeid, $scfg, $volname) = @_;
-
-    my $snap = '__base__';
-
-    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) =
-        $class->parse_volname($volname);
-
-    die "create_base not possible with base image\n" if $isBase;
-
-    my $newname = $name;
-    if ( $format eq 'subvol' ) {
-       $newname =~ s/^subvol-/basevol-/;
-    } else {
-       $newname =~ s/^vm-/base-/;
-    }
-    my $newvolname = $basename ? "$basename/$newname" : "$newname";
-
-    $class->zfs_request($scfg, undef, 'rename', "$scfg->{pool}/$name", "$scfg->{pool}/$newname");
-
-    my $running  = undef; #fixme : is create_base always offline ?
-
-    $class->volume_snapshot($scfg, $storeid, $newname, $snap, $running);
-
-    return $newvolname;
-}
-
-sub volume_resize {
-    my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
-
-    my $new_size = int($size/1024);
-
-    my (undef, $vname, undef, undef, undef, undef, $format) =
-        $class->parse_volname($volname);
-
-    my $attr = $format eq 'subvol' ? 'refquota' : 'volsize';
-
-    # align size to 1M so we always have a valid multiple of the volume block size
-    if ($format eq 'raw') {
-       my $padding = (1024 - $new_size % 1024) % 1024;
-       $new_size = $new_size + $padding;
-    }
-
-    $class->zfs_request($scfg, undef, 'set', "$attr=${new_size}k", "$scfg->{pool}/$vname");
-
-    return $new_size;
-}
-
-sub storage_can_replicate {
-    my ($class, $scfg, $storeid, $format) = @_;
-
-    return 1 if $format eq 'raw' || $format eq 'subvol';
-
-    return 0;
-}
-
-sub volume_has_feature {
-    my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
-
-    my $features = {
-       snapshot => { current => 1, snap => 1},
-       clone => { base => 1},
-       template => { current => 1},
-       copy => { base => 1, current => 1},
-       sparseinit => { base => 1, current => 1},
-       replicate => { base => 1, current => 1},
-       rename => {current => 1},
-    };
-
-    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
-       $class->parse_volname($volname);
-
-    my $key = undef;
-
-    if ($snapname) {
-       $key = 'snap';
-    } else {
-       $key = $isBase ? 'base' : 'current';
-    }
-
-    return 1 if $features->{$feature}->{$key};
-
-    return undef;
-}
-
-sub volume_export {
-    my ($class, $scfg, $storeid, $fh, $volname, $format, $snapshot, $base_snapshot, $with_snapshots) = @_;
-
-    die "unsupported export stream format for $class: $format\n"
-       if $format ne 'zfs';
-
-    die "$class storage can only export snapshots\n"
-       if !defined($snapshot);
-
-    my $dataset = ($class->parse_volname($volname))[1];
-
-    my $fd = fileno($fh);
-    die "internal error: invalid file handle for volume_export\n"
-       if !defined($fd);
-    $fd = ">&$fd";
-
-    # For zfs we always create a replication stream (-R) which means the remote
-    # side will always delete non-existing source snapshots. This should work
-    # for all our use cases.
-    my $cmd = ['zfs', 'send', '-Rpv'];
-    if (defined($base_snapshot)) {
-       my $arg = $with_snapshots ? '-I' : '-i';
-       push @$cmd, $arg, $base_snapshot;
-    }
-    push @$cmd, '--', "$scfg->{pool}/$dataset\@$snapshot";
-
-    run_command($cmd, output => $fd);
-
-    return;
-}
-
-sub volume_export_formats {
-    my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
-
-    my @formats = ('zfs');
-    # TODOs:
-    # push @formats, 'fies' if $volname !~ /^(?:basevol|subvol)-/;
-    # push @formats, 'raw' if !$base_snapshot && !$with_snapshots;
-    return @formats;
-}
-
-sub volume_import {
-    my ($class, $scfg, $storeid, $fh, $volname, $format, $snapshot, $base_snapshot, $with_snapshots, $allow_rename) = @_;
-
-    die "unsupported import stream format for $class: $format\n"
-       if $format ne 'zfs';
-
-    my $fd = fileno($fh);
-    die "internal error: invalid file handle for volume_import\n"
-       if !defined($fd);
-
-    my (undef, $dataset, $vmid, undef, undef, undef, $volume_format) =
-       $class->parse_volname($volname);
-
-    my $zfspath = "$scfg->{pool}/$dataset";
-    my $suffix = defined($base_snapshot) ? "\@$base_snapshot" : '';
-    my $exists = 0 == run_command(['zfs', 'get', '-H', 'name', $zfspath.$suffix],
-                                 noerr => 1, quiet => 1);
-    if (defined($base_snapshot)) {
-       die "base snapshot '$zfspath\@$base_snapshot' doesn't exist\n" if !$exists;
-    } elsif ($exists) {
-       die "volume '$zfspath' already exists\n" if !$allow_rename;
-       warn "volume '$zfspath' already exists - importing with a different name\n";
-       $dataset = $class->find_free_diskname($storeid, $scfg, $vmid, $volume_format);
-       $zfspath = "$scfg->{pool}/$dataset";
-    }
-
-    eval { run_command(['zfs', 'recv', '-F', '--', $zfspath], input => "<&$fd") };
-    if (my $err = $@) {
-       if (defined($base_snapshot)) {
-           eval { run_command(['zfs', 'rollback', '-r', '--', "$zfspath\@$base_snapshot"]) };
-       } else {
-           eval { run_command(['zfs', 'destroy', '-r', '--', $zfspath]) };
-       }
-       die $err;
-    }
-
-    return "$storeid:$dataset";
-}
-
-sub volume_import_formats {
-    my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
-
-    return $class->volume_export_formats($scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots);
-}
-
-sub rename_volume {
-    my ($class, $scfg, $storeid, $source_volname, $target_vmid, $target_volname) = @_;
-
-    my (
-       undef,
-       $source_image,
-       $source_vmid,
-       $base_name,
-       $base_vmid,
-       undef,
-       $format
-    ) = $class->parse_volname($source_volname);
-    $target_volname = $class->find_free_diskname($storeid, $scfg, $target_vmid, $format)
-       if !$target_volname;
-
-    my $pool = $scfg->{pool};
-    my $source_zfspath = "${pool}/${source_image}";
-    my $target_zfspath = "${pool}/${target_volname}";
-
-    my $exists = 0 == run_command(['zfs', 'get', '-H', 'name', $target_zfspath],
-                                 noerr => 1, quiet => 1);
-    die "target volume '${target_volname}' already exists\n" if $exists;
-
-    $class->zfs_request($scfg, 5, 'rename', ${source_zfspath}, ${target_zfspath});
-
-    $base_name = $base_name ? "${base_name}/" : '';
-
-    return "${storeid}:${base_name}${target_volname}";
-}
-
-1;
diff --git a/pvesm b/pvesm
deleted file mode 100755 (executable)
index ece9be8..0000000
--- a/pvesm
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/usr/bin/perl
-
-use strict;
-use warnings;
-
-use PVE::CLI::pvesm;
-
-PVE::CLI::pvesm->run_cli_handler();
diff --git a/src/Makefile b/src/Makefile
new file mode 100644 (file)
index 0000000..1eba51e
--- /dev/null
@@ -0,0 +1,23 @@
+DESTDIR=
+PREFIX=/usr
+
+export PERLDIR=$(PREFIX)/share/perl5
+
+all:
+
+.PHONY: install
+install: PVE bin udev-rbd
+       $(MAKE) -C bin install
+       $(MAKE) -C PVE install
+       $(MAKE) -C udev-rbd install
+
+.PHONY: test
+test:
+       perl -I. -T -e "use PVE::CLI::pvesm; PVE::CLI::pvesm->verify_api();"
+       $(MAKE) -C test
+
+.PHONY: clean
+clean:
+       $(MAKE) -C bin clean
+       $(MAKE) -C PVE clean
+       $(MAKE) -C udev-rbd clean
diff --git a/src/PVE/API2/Disks.pm b/src/PVE/API2/Disks.pm
new file mode 100644 (file)
index 0000000..bde6132
--- /dev/null
@@ -0,0 +1,318 @@
+package PVE::API2::Disks;
+
+use strict;
+use warnings;
+
+use File::Basename;
+use HTTP::Status qw(:constants);
+
+use PVE::Diskmanage;
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::SafeSyslog;
+use PVE::Tools qw(run_command);
+
+use PVE::API2::Disks::Directory;
+use PVE::API2::Disks::LVM;
+use PVE::API2::Disks::LVMThin;
+use PVE::API2::Disks::ZFS;
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method ({
+   subclass => "PVE::API2::Disks::LVM",
+   path => 'lvm',
+});
+
+__PACKAGE__->register_method ({
+   subclass => "PVE::API2::Disks::LVMThin",
+   path => 'lvmthin',
+});
+
+__PACKAGE__->register_method ({
+   subclass => "PVE::API2::Disks::Directory",
+   path => 'directory',
+});
+
+__PACKAGE__->register_method ({
+   subclass => "PVE::API2::Disks::ZFS",
+   path => 'zfs',
+});
+
+__PACKAGE__->register_method ({
+    name => 'index',
+    path => '',
+    method => 'GET',
+    proxyto => 'node',
+    permissions => { user => 'all' },
+    description => "Node index.",
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+       },
+    },
+    returns => {
+       type => 'array',
+       items => {
+           type => "object",
+           properties => {},
+       },
+       links => [ { rel => 'child', href => "{name}" } ],
+    },
+    code => sub {
+       my ($param) = @_;
+
+       my $result = [
+           { name => 'list' },
+           { name => 'initgpt' },
+           { name => 'smart' },
+           { name => 'lvm' },
+           { name => 'lvmthin' },
+           { name => 'directory' },
+           { name => 'wipedisk' },
+           { name => 'zfs' },
+       ];
+
+       return $result;
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'list',
+    path => 'list',
+    method => 'GET',
+    description => "List local disks.",
+    protected => 1,
+    proxyto => 'node',
+    permissions => {
+       check => ['or',
+           ['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1],
+           ['perm', '/nodes/{node}', ['Sys.Audit', 'Datastore.Audit'], any => 1],
+       ],
+    },
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           'include-partitions' => {
+               description => "Also include partitions.",
+               type => 'boolean',
+               optional => 1,
+               default => 0,
+           },
+           skipsmart => {
+               description => "Skip smart checks.",
+               type => 'boolean',
+               optional => 1,
+               default => 0,
+           },
+           type => {
+               description => "Only list specific types of disks.",
+               type => 'string',
+               enum => ['unused', 'journal_disks'],
+               optional => 1,
+           },
+       },
+    },
+    returns => {
+       type => 'array',
+       items => {
+           type => 'object',
+           properties => {
+               devpath => {
+                   type => 'string',
+                   description => 'The device path',
+               },
+               used => { type => 'string', optional => 1 },
+               gpt => { type => 'boolean' },
+               mounted => { type => 'boolean' },
+               size => { type => 'integer'},
+               osdid => { type => 'integer'},
+               vendor =>  { type => 'string', optional => 1 },
+               model =>  { type => 'string', optional => 1 },
+               serial =>  { type => 'string', optional => 1 },
+               wwn => { type => 'string', optional => 1},
+               health => { type => 'string', optional => 1},
+               parent => {
+                   type => 'string',
+                   description => 'For partitions only. The device path of ' .
+                       'the disk the partition resides on.',
+                   optional => 1
+               },
+           },
+       },
+    },
+    code => sub {
+       my ($param) = @_;
+
+       my $skipsmart = $param->{skipsmart} // 0;
+       my $include_partitions = $param->{'include-partitions'} // 0;
+
+       my $disks = PVE::Diskmanage::get_disks(
+           undef,
+           $skipsmart,
+           $include_partitions
+       );
+
+       my $type = $param->{type} // '';
+       my $result = [];
+
+       foreach my $disk (sort keys %$disks) {
+           my $entry = $disks->{$disk};
+           if ($type eq 'journal_disks') {
+               next if $entry->{osdid} >= 0;
+               if (my $usage = $entry->{used}) {
+                   next if !($usage eq 'partitions' && $entry->{gpt}
+                       || $usage eq 'LVM');
+               }
+           } elsif ($type eq 'unused') {
+               next if $entry->{used};
+           } elsif ($type ne '') {
+               die "internal error"; # should not happen
+           }
+           push @$result, $entry;
+       }
+       return $result;
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'smart',
+    path => 'smart',
+    method => 'GET',
+    description => "Get SMART Health of a disk.",
+    protected => 1,
+    proxyto => "node",
+    permissions => {
+       check => ['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1],
+    },
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           disk => {
+               type => 'string',
+               pattern => '^/dev/[a-zA-Z0-9\/]+$',
+               description => "Block device name",
+           },
+           healthonly => {
+               type => 'boolean',
+               description => "If true returns only the health status",
+               optional => 1,
+           },
+       },
+    },
+    returns => {
+       type => 'object',
+       properties => {
+           health => { type => 'string' },
+           type => { type => 'string', optional => 1 },
+           attributes => { type => 'array', optional => 1},
+           text => { type => 'string', optional => 1 },
+       },
+    },
+    code => sub {
+       my ($param) = @_;
+
+       my $disk = PVE::Diskmanage::verify_blockdev_path($param->{disk});
+
+       my $result = PVE::Diskmanage::get_smart_data($disk, $param->{healthonly});
+
+       $result->{health} = 'UNKNOWN' if !defined $result->{health};
+       $result = { health => $result->{health} } if $param->{healthonly};
+
+       return $result;
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'initgpt',
+    path => 'initgpt',
+    method => 'POST',
+    description => "Initialize Disk with GPT",
+    protected => 1,
+    proxyto => "node",
+    permissions => {
+       check => ['perm', '/', ['Sys.Modify']],
+    },
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           disk => {
+               type => 'string',
+               description => "Block device name",
+               pattern => '^/dev/[a-zA-Z0-9\/]+$',
+           },
+           uuid => {
+               type => 'string',
+               description => 'UUID for the GPT table',
+               pattern => '[a-fA-F0-9\-]+',
+               maxLength => 36,
+               optional => 1,
+           },
+       },
+    },
+    returns => { type => 'string' },
+    code => sub {
+       my ($param) = @_;
+
+       my $disk = PVE::Diskmanage::verify_blockdev_path($param->{disk});
+
+       my $rpcenv = PVE::RPCEnvironment::get();
+
+       my $authuser = $rpcenv->get_user();
+
+       die "$disk is a partition\n" if PVE::Diskmanage::is_partition($disk);
+       die "disk $disk already in use\n" if PVE::Diskmanage::disk_is_used($disk);
+       my $worker = sub {
+           PVE::Diskmanage::init_disk($disk, $param->{uuid});
+       };
+
+       my $diskid = $disk;
+       $diskid =~ s|^.*/||; # remove all up to the last slash
+       return $rpcenv->fork_worker('diskinit', $diskid, $authuser, $worker);
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'wipe_disk',
+    path => 'wipedisk',
+    method => 'PUT',
+    description => "Wipe a disk or partition.",
+    proxyto => 'node',
+    protected => 1,
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           disk => {
+               type => 'string',
+               description => "Block device name",
+               pattern => '^/dev/[a-zA-Z0-9\/]+$',
+           },
+       },
+    },
+    returns => { type => 'string' },
+    code => sub {
+       my ($param) = @_;
+
+       my $disk = PVE::Diskmanage::verify_blockdev_path($param->{disk});
+
+       my $mounted = PVE::Diskmanage::is_mounted($disk);
+       die "disk/partition '${mounted}' is mounted\n" if $mounted;
+
+       my $held = PVE::Diskmanage::has_holder($disk);
+       die "disk/partition '${held}' has a holder\n" if $held;
+
+       my $rpcenv = PVE::RPCEnvironment::get();
+       my $authuser = $rpcenv->get_user();
+
+       my $worker = sub {
+           PVE::Diskmanage::wipe_blockdev($disk);
+           PVE::Diskmanage::udevadm_trigger($disk);
+       };
+
+       my $basename = basename($disk); # avoid '/' in the ID
+
+       return $rpcenv->fork_worker('wipedisk', $basename, $authuser, $worker);
+    }});
+
+1;
diff --git a/src/PVE/API2/Disks/Directory.pm b/src/PVE/API2/Disks/Directory.pm
new file mode 100644 (file)
index 0000000..4fdb068
--- /dev/null
@@ -0,0 +1,409 @@
+package PVE::API2::Disks::Directory;
+
+use strict;
+use warnings;
+
+use POSIX;
+
+use PVE::Diskmanage;
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::RESTHandler;
+use PVE::RPCEnvironment;
+use PVE::Systemd;
+use PVE::Tools qw(run_command trim file_set_contents file_get_contents dir_glob_foreach lock_file);
+
+use PVE::API2::Storage::Config;
+
+use base qw(PVE::RESTHandler);
+
+my $SGDISK = '/sbin/sgdisk';
+my $MKFS = '/sbin/mkfs';
+my $BLKID = '/sbin/blkid';
+
+my $read_ini = sub {
+    my ($filename) = @_;
+
+    my $content = file_get_contents($filename);
+    my @lines = split /\n/, $content;
+
+    my $result = {};
+    my $section;
+
+    foreach my $line (@lines) {
+       $line = trim($line);
+       if ($line =~ m/^\[([^\]]+)\]/) {
+           $section = $1;
+           if (!defined($result->{$section})) {
+               $result->{$section} = {};
+           }
+       } elsif ($line =~ m/^(.*?)=(.*)$/) {
+           my ($key, $val) = ($1, $2);
+           if (!$section) {
+               warn "key value pair found without section, skipping\n";
+               next;
+           }
+
+           if ($result->{$section}->{$key}) {
+               # make duplicate properties to arrays to keep the order
+               my $prop = $result->{$section}->{$key};
+               if (ref($prop) eq 'ARRAY') {
+                   push @$prop, $val;
+               } else {
+                   $result->{$section}->{$key} = [$prop, $val];
+               }
+           } else {
+               $result->{$section}->{$key} = $val;
+           }
+       }
+       # ignore everything else
+    }
+
+    return $result;
+};
+
+my $write_ini = sub {
+    my ($ini, $filename) = @_;
+
+    my $content = "";
+
+    foreach my $sname (sort keys %$ini) {
+       my $section = $ini->{$sname};
+
+       $content .= "[$sname]\n";
+
+       foreach my $pname (sort keys %$section) {
+           my $prop = $section->{$pname};
+
+           if (!ref($prop)) {
+               $content .= "$pname=$prop\n";
+           } elsif (ref($prop) eq 'ARRAY') {
+               foreach my $val (@$prop) {
+                   $content .= "$pname=$val\n";
+               }
+           } else {
+               die "invalid property '$pname'\n";
+           }
+       }
+       $content .= "\n";
+    }
+
+    file_set_contents($filename, $content);
+};
+
+__PACKAGE__->register_method ({
+    name => 'index',
+    path => '',
+    method => 'GET',
+    proxyto => 'node',
+    protected => 1,
+    permissions => {
+       check => ['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1],
+    },
+    description => "PVE Managed Directory storages.",
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+       },
+    },
+    returns => {
+       type => 'array',
+       items => {
+           type => 'object',
+           properties => {
+               unitfile => {
+                   type => 'string',
+                   description => 'The path of the mount unit.',
+               },
+               path => {
+                   type => 'string',
+                   description => 'The mount path.',
+               },
+               device => {
+                   type => 'string',
+                   description => 'The mounted device.',
+               },
+               type => {
+                   type => 'string',
+                   description => 'The filesystem type.',
+               },
+               options => {
+                   type => 'string',
+                   description => 'The mount options.',
+               },
+           },
+       },
+    },
+    code => sub {
+       my ($param) = @_;
+
+       my $result = [];
+
+       dir_glob_foreach('/etc/systemd/system', '^mnt-pve-(.+)\.mount$', sub {
+           my ($filename, $storid) = @_;
+           $storid = PVE::Systemd::unescape_unit($storid);
+
+           my $unitfile = "/etc/systemd/system/$filename";
+           my $unit = $read_ini->($unitfile);
+
+           push @$result, {
+               unitfile => $unitfile,
+               path => "/mnt/pve/$storid",
+               device => $unit->{'Mount'}->{'What'},
+               type => $unit->{'Mount'}->{'Type'},
+               options => $unit->{'Mount'}->{'Options'},
+           };
+       });
+
+       return $result;
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'create',
+    path => '',
+    method => 'POST',
+    proxyto => 'node',
+    protected => 1,
+    permissions => {
+       check => ['perm', '/', ['Sys.Modify', 'Datastore.Allocate']],
+    },
+    description => "Create a Filesystem on an unused disk. Will be mounted under '/mnt/pve/NAME'.",
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           name => get_standard_option('pve-storage-id'),
+           device => {
+               type => 'string',
+               description => 'The block device you want to create the filesystem on.',
+           },
+           add_storage => {
+               description => "Configure storage using the directory.",
+               type => 'boolean',
+               optional => 1,
+               default => 0,
+           },
+           filesystem => {
+               description => "The desired filesystem.",
+               type => 'string',
+               enum => ['ext4', 'xfs'],
+               optional => 1,
+               default => 'ext4',
+           },
+       },
+    },
+    returns => { type => 'string' },
+    code => sub {
+       my ($param) = @_;
+
+       my $rpcenv = PVE::RPCEnvironment::get();
+       my $user = $rpcenv->get_user();
+
+       my $name = $param->{name};
+       my $dev = $param->{device};
+       my $node = $param->{node};
+       my $type = $param->{filesystem} // 'ext4';
+       my $path = "/mnt/pve/$name";
+       my $mountunitname = PVE::Systemd::escape_unit($path, 1) . ".mount";
+       my $mountunitpath = "/etc/systemd/system/$mountunitname";
+
+       $dev = PVE::Diskmanage::verify_blockdev_path($dev);
+       PVE::Diskmanage::assert_disk_unused($dev);
+
+       my $storage_params = {
+           type => 'dir',
+           storage => $name,
+           content => 'rootdir,images,iso,backup,vztmpl,snippets',
+           is_mountpoint => 1,
+           path => $path,
+           nodes => $node,
+       };
+       my $verify_params = [qw(path)];
+
+       if ($param->{add_storage}) {
+           PVE::API2::Storage::Config->create_or_update(
+               $name,
+               $node,
+               $storage_params,
+               $verify_params,
+               1,
+           );
+       }
+
+       my $mounted = PVE::Diskmanage::mounted_paths();
+       die "the path for '${name}' is already mounted: ${path} ($mounted->{$path})\n"
+           if $mounted->{$path};
+       die "a systemd mount unit already exists: ${mountunitpath}\n" if -e $mountunitpath;
+
+       my $worker = sub {
+           PVE::Diskmanage::locked_disk_action(sub {
+               PVE::Diskmanage::assert_disk_unused($dev);
+
+               my $part = $dev;
+
+               if (PVE::Diskmanage::is_partition($dev)) {
+                   eval { PVE::Diskmanage::change_parttype($dev, '8300'); };
+                   warn $@ if $@;
+               } else {
+                   # create partition
+                   my $cmd = [$SGDISK, '-n1', '-t1:8300', $dev];
+                   print "# ", join(' ', @$cmd), "\n";
+                   run_command($cmd);
+
+                   my ($devname) = $dev =~ m|^/dev/(.*)$|;
+                   $part = "/dev/";
+                   dir_glob_foreach("/sys/block/$devname", qr/\Q$devname\E.+/, sub {
+                       my ($partition) = @_;
+                       $part .= $partition;
+                   });
+               }
+
+               # create filesystem
+               my $cmd = [$MKFS, '-t', $type, $part];
+               print "# ", join(' ', @$cmd), "\n";
+               run_command($cmd);
+
+               # create systemd mount unit and enable & start it
+               my $ini = {
+                   'Unit' => {
+                       'Description' => "Mount storage '$name' under /mnt/pve",
+                   },
+                   'Install' => {
+                       'WantedBy' => 'multi-user.target',
+                   },
+               };
+
+               my $uuid_path;
+               my $uuid;
+
+               $cmd = [$BLKID, $part, '-o', 'export'];
+               print "# ", join(' ', @$cmd), "\n";
+               run_command($cmd, outfunc => sub {
+                       my ($line) = @_;
+
+                       if ($line =~ m/^UUID=(.*)$/) {
+                           $uuid = $1;
+                           $uuid_path = "/dev/disk/by-uuid/$uuid";
+                       }
+                   });
+
+               die "could not get UUID of device '$part'\n" if !$uuid;
+
+               $ini->{'Mount'} = {
+                   'What' => $uuid_path,
+                   'Where' => $path,
+                   'Type' => $type,
+                   'Options' => 'defaults',
+               };
+
+               $write_ini->($ini, $mountunitpath);
+
+               PVE::Diskmanage::udevadm_trigger($part);
+
+               run_command(['systemctl', 'daemon-reload']);
+               run_command(['systemctl', 'enable', $mountunitname]);
+               run_command(['systemctl', 'start', $mountunitname]);
+
+               if ($param->{add_storage}) {
+                   PVE::API2::Storage::Config->create_or_update(
+                       $name,
+                       $node,
+                       $storage_params,
+                       $verify_params,
+                   );
+               }
+           });
+       };
+
+       return $rpcenv->fork_worker('dircreate', $name, $user, $worker);
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'delete',
+    path => '{name}',
+    method => 'DELETE',
+    proxyto => 'node',
+    protected => 1,
+    permissions => {
+       check => ['perm', '/', ['Sys.Modify', 'Datastore.Allocate']],
+    },
+    description => "Unmounts the storage and removes the mount unit.",
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           name => get_standard_option('pve-storage-id'),
+           'cleanup-config' => {
+               description => "Marks associated storage(s) as not available on this node anymore ".
+                   "or removes them from the configuration (if configured for this node only).",
+               type => 'boolean',
+               optional => 1,
+               default => 0,
+           },
+           'cleanup-disks' => {
+               description => "Also wipe disk so it can be repurposed afterwards.",
+               type => 'boolean',
+               optional => 1,
+               default => 0,
+           },
+       },
+    },
+    returns => { type => 'string' },
+    code => sub {
+       my ($param) = @_;
+
+       my $rpcenv = PVE::RPCEnvironment::get();
+       my $user = $rpcenv->get_user();
+
+       my $name = $param->{name};
+       my $node = $param->{node};
+
+       my $worker = sub {
+           my $path = "/mnt/pve/$name";
+           my $mountunitname = PVE::Systemd::escape_unit($path, 1) . ".mount";
+           my $mountunitpath = "/etc/systemd/system/$mountunitname";
+
+           PVE::Diskmanage::locked_disk_action(sub {
+               my $to_wipe;
+               if ($param->{'cleanup-disks'}) {
+                   my $unit = $read_ini->($mountunitpath);
+
+                   my $dev = PVE::Diskmanage::verify_blockdev_path($unit->{'Mount'}->{'What'});
+                   $to_wipe = $dev;
+
+                   # clean up whole device if this is the only partition
+                   $dev =~ s|^/dev/||;
+                   my $info = PVE::Diskmanage::get_disks($dev, 1, 1);
+                   die "unable to obtain information for disk '$dev'\n" if !$info->{$dev};
+                   $to_wipe = $info->{$dev}->{parent}
+                       if $info->{$dev}->{parent} && scalar(keys $info->%*) == 2;
+               }
+
+               run_command(['systemctl', 'stop', $mountunitname]);
+               run_command(['systemctl', 'disable', $mountunitname]);
+
+               unlink $mountunitpath or $! == ENOENT or die "cannot remove $mountunitpath - $!\n";
+
+               my $config_err;
+               if ($param->{'cleanup-config'}) {
+                   my $match = sub {
+                       my ($scfg) = @_;
+                       return $scfg->{type} eq 'dir' && $scfg->{path} eq $path;
+                   };
+                   eval { PVE::API2::Storage::Config->cleanup_storages_for_node($match, $node); };
+                   warn $config_err = $@ if $@;
+               }
+
+               if ($to_wipe) {
+                   PVE::Diskmanage::wipe_blockdev($to_wipe);
+                   PVE::Diskmanage::udevadm_trigger($to_wipe);
+               }
+
+               die "config cleanup failed - $config_err" if $config_err;
+           });
+       };
+
+       return $rpcenv->fork_worker('dirremove', $name, $user, $worker);
+    }});
+
+1;
diff --git a/src/PVE/API2/Disks/LVM.pm b/src/PVE/API2/Disks/LVM.pm
new file mode 100644 (file)
index 0000000..fe87545
--- /dev/null
@@ -0,0 +1,281 @@
+package PVE::API2::Disks::LVM;
+
+use strict;
+use warnings;
+
+use PVE::Storage::LVMPlugin;
+use PVE::Diskmanage;
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::API2::Storage::Config;
+use PVE::Tools qw(lock_file run_command);
+
+use PVE::RPCEnvironment;
+use PVE::RESTHandler;
+
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method ({
+    name => 'index',
+    path => '',
+    method => 'GET',
+    proxyto => 'node',
+    protected => 1,
+    permissions => {
+       check => ['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1],
+    },
+    description => "List LVM Volume Groups",
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+       },
+    },
+    returns => {
+       type => 'object',
+       properties => {
+           leaf => {
+               type => 'boolean',
+           },
+           children => {
+               type => 'array',
+               items => {
+                   type => "object",
+                   properties => {
+                       leaf => {
+                           type => 'boolean',
+                       },
+                       name => {
+                           type => 'string',
+                           description => 'The name of the volume group',
+                       },
+                       size => {
+                           type => 'integer',
+                           description => 'The size of the volume group in bytes',
+                       },
+                       free => {
+                           type => 'integer',
+                           description => 'The free bytes in the volume group',
+                       },
+                       children => {
+                           optional => 1,
+                           type => 'array',
+                           description => 'The underlying physical volumes',
+                           items =>  {
+                               type => 'object',
+                               properties => {
+                                   leaf => {
+                                       type => 'boolean',
+                                   },
+                                   name => {
+                                       type => 'string',
+                                       description => 'The name of the physical volume',
+                                   },
+                                   size => {
+                                       type => 'integer',
+                                       description => 'The size of the physical volume in bytes',
+                                   },
+                                   free => {
+                                       type => 'integer',
+                                       description => 'The free bytes in the physical volume',
+                                   },
+                               },
+                           },
+                       },
+                   },
+               },
+           },
+       },
+    },
+    code => sub {
+       my ($param) = @_;
+
+       my $result = [];
+
+       my $vgs = PVE::Storage::LVMPlugin::lvm_vgs(1);
+
+       foreach my $vg_name (sort keys %$vgs) {
+           my $vg = $vgs->{$vg_name};
+           $vg->{name} = $vg_name;
+           $vg->{leaf} = 0;
+           foreach my $pv (@{$vg->{pvs}}) {
+               $pv->{leaf} = 1;
+           }
+           $vg->{children} = delete $vg->{pvs};
+           push @$result, $vg;
+       }
+
+       return {
+           leaf => 0,
+           children => $result,
+       };
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'create',
+    path => '',
+    method => 'POST',
+    proxyto => 'node',
+    protected => 1,
+    permissions => {
+       check => ['perm', '/', ['Sys.Modify', 'Datastore.Allocate']],
+    },
+    description => "Create an LVM Volume Group",
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           name => get_standard_option('pve-storage-id'),
+           device => {
+               type => 'string',
+               description => 'The block device you want to create the volume group on',
+           },
+           add_storage => {
+               description => "Configure storage using the Volume Group",
+               type => 'boolean',
+               optional => 1,
+               default => 0,
+           },
+       },
+    },
+    returns => { type => 'string' },
+    code => sub {
+       my ($param) = @_;
+
+       my $rpcenv = PVE::RPCEnvironment::get();
+       my $user = $rpcenv->get_user();
+
+       my $name = $param->{name};
+       my $dev = $param->{device};
+       my $node = $param->{node};
+
+       $dev = PVE::Diskmanage::verify_blockdev_path($dev);
+       PVE::Diskmanage::assert_disk_unused($dev);
+
+       my $storage_params = {
+           type => 'lvm',
+           vgname => $name,
+           storage => $name,
+           content => 'rootdir,images',
+           shared => 0,
+           nodes => $node,
+       };
+       my $verify_params = [qw(vgname)];
+
+       if ($param->{add_storage}) {
+           PVE::API2::Storage::Config->create_or_update(
+               $name,
+               $node,
+               $storage_params,
+               $verify_params,
+               1,
+           );
+       }
+
+       my $worker = sub {
+           PVE::Diskmanage::locked_disk_action(sub {
+               PVE::Diskmanage::assert_disk_unused($dev);
+               die "volume group with name '${name}' already exists on node '${node}'\n"
+                   if PVE::Storage::LVMPlugin::lvm_vgs()->{$name};
+
+               if (PVE::Diskmanage::is_partition($dev)) {
+                   eval { PVE::Diskmanage::change_parttype($dev, '8E00'); };
+                   warn $@ if $@;
+               }
+
+               PVE::Storage::LVMPlugin::lvm_create_volume_group($dev, $name);
+
+               PVE::Diskmanage::udevadm_trigger($dev);
+
+               if ($param->{add_storage}) {
+                   PVE::API2::Storage::Config->create_or_update(
+                       $name,
+                       $node,
+                       $storage_params,
+                       $verify_params,
+                   );
+               }
+           });
+       };
+
+       return $rpcenv->fork_worker('lvmcreate', $name, $user, $worker);
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'delete',
+    path => '{name}',
+    method => 'DELETE',
+    proxyto => 'node',
+    protected => 1,
+    permissions => {
+       check => ['perm', '/', ['Sys.Modify', 'Datastore.Allocate']],
+    },
+    description => "Remove an LVM Volume Group.",
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           name => get_standard_option('pve-storage-id'),
+           'cleanup-config' => {
+               description => "Marks associated storage(s) as not available on this node anymore ".
+                   "or removes them from the configuration (if configured for this node only).",
+               type => 'boolean',
+               optional => 1,
+               default => 0,
+           },
+           'cleanup-disks' => {
+               description => "Also wipe disks so they can be repurposed afterwards.",
+               type => 'boolean',
+               optional => 1,
+               default => 0,
+           },
+       },
+    },
+    returns => { type => 'string' },
+    code => sub {
+       my ($param) = @_;
+
+       my $rpcenv = PVE::RPCEnvironment::get();
+       my $user = $rpcenv->get_user();
+
+       my $name = $param->{name};
+       my $node = $param->{node};
+
+       my $worker = sub {
+           PVE::Diskmanage::locked_disk_action(sub {
+               my $vgs = PVE::Storage::LVMPlugin::lvm_vgs(1);
+               die "no such volume group '$name'\n" if !$vgs->{$name};
+
+               PVE::Storage::LVMPlugin::lvm_destroy_volume_group($name);
+
+               my $config_err;
+               if ($param->{'cleanup-config'}) {
+                   my $match = sub {
+                       my ($scfg) = @_;
+                       return $scfg->{type} eq 'lvm' && $scfg->{vgname} eq $name;
+                   };
+                   eval { PVE::API2::Storage::Config->cleanup_storages_for_node($match, $node); };
+                   warn $config_err = $@ if $@;
+               }
+
+               if ($param->{'cleanup-disks'}) {
+                   my $wiped = [];
+                   eval {
+                       for my $pv ($vgs->{$name}->{pvs}->@*) {
+                           my $dev = PVE::Diskmanage::verify_blockdev_path($pv->{name});
+                           PVE::Diskmanage::wipe_blockdev($dev);
+                           push $wiped->@*, $dev;
+                       }
+                   };
+                   my $err = $@;
+                   PVE::Diskmanage::udevadm_trigger($wiped->@*);
+                   die "cleanup failed - $err" if $err;
+               }
+
+               die "config cleanup failed - $config_err" if $config_err;
+           });
+       };
+
+       return $rpcenv->fork_worker('lvmremove', $name, $user, $worker);
+    }});
+
+1;
diff --git a/src/PVE/API2/Disks/LVMThin.pm b/src/PVE/API2/Disks/LVMThin.pm
new file mode 100644 (file)
index 0000000..038310a
--- /dev/null
@@ -0,0 +1,271 @@
+package PVE::API2::Disks::LVMThin;
+
+use strict;
+use warnings;
+
+use PVE::Storage::LvmThinPlugin;
+use PVE::Diskmanage;
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::API2::Storage::Config;
+use PVE::Storage;
+use PVE::Tools qw(run_command lock_file);
+
+use PVE::RPCEnvironment;
+use PVE::RESTHandler;
+
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method ({
+    name => 'index',
+    path => '',
+    method => 'GET',
+    proxyto => 'node',
+    protected => 1,
+    permissions => {
+       check => ['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1],
+    },
+    description => "List LVM thinpools",
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+       },
+    },
+    returns => {
+       type => 'array',
+       items => {
+           type => 'object',
+           properties => {
+               lv => {
+                   type => 'string',
+                   description => 'The name of the thinpool.',
+               },
+               vg => {
+                   type => 'string',
+                   description => 'The associated volume group.',
+               },
+               lv_size => {
+                   type => 'integer',
+                   description => 'The size of the thinpool in bytes.',
+               },
+               used => {
+                   type => 'integer',
+                   description => 'The used bytes of the thinpool.',
+               },
+               metadata_size => {
+                   type => 'integer',
+                   description => 'The size of the metadata lv in bytes.',
+               },
+               metadata_used => {
+                   type => 'integer',
+                   description => 'The used bytes of the metadata lv.',
+               },
+           },
+       },
+    },
+    code => sub {
+       my ($param) = @_;
+       return PVE::Storage::LvmThinPlugin::list_thinpools(undef);
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'create',
+    path => '',
+    method => 'POST',
+    proxyto => 'node',
+    protected => 1,
+    permissions => {
+       check => ['perm', '/', ['Sys.Modify', 'Datastore.Allocate']],
+    },
+    description => "Create an LVM thinpool",
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           name => get_standard_option('pve-storage-id'),
+           device => {
+               type => 'string',
+               description => 'The block device you want to create the thinpool on.',
+           },
+           add_storage => {
+               description => "Configure storage using the thinpool.",
+               type => 'boolean',
+               optional => 1,
+               default => 0,
+           },
+       },
+    },
+    returns => { type => 'string' },
+    code => sub {
+       my ($param) = @_;
+
+       my $rpcenv = PVE::RPCEnvironment::get();
+       my $user = $rpcenv->get_user();
+
+       my $name = $param->{name};
+       my $dev = $param->{device};
+       my $node = $param->{node};
+
+       $dev = PVE::Diskmanage::verify_blockdev_path($dev);
+       PVE::Diskmanage::assert_disk_unused($dev);
+
+       my $storage_params = {
+           type => 'lvmthin',
+           vgname => $name,
+           thinpool => $name,
+           storage => $name,
+           content => 'rootdir,images',
+           nodes => $node,
+       };
+       my $verify_params = [qw(vgname thinpool)];
+
+       if ($param->{add_storage}) {
+           PVE::API2::Storage::Config->create_or_update(
+               $name,
+               $node,
+               $storage_params,
+               $verify_params,
+               1,
+           );
+       }
+
+       my $worker = sub {
+           PVE::Diskmanage::locked_disk_action(sub {
+               PVE::Diskmanage::assert_disk_unused($dev);
+
+               die "volume group with name '${name}' already exists on node '${node}'\n"
+                   if PVE::Storage::LVMPlugin::lvm_vgs()->{$name};
+
+               if (PVE::Diskmanage::is_partition($dev)) {
+                   eval { PVE::Diskmanage::change_parttype($dev, '8E00'); };
+                   warn $@ if $@;
+               }
+
+               PVE::Storage::LVMPlugin::lvm_create_volume_group($dev, $name);
+               my $pv = PVE::Storage::LVMPlugin::lvm_pv_info($dev);
+               # keep some free space just in case
+               my $datasize = $pv->{size} - 128*1024;
+               # default to 1% for metadata
+               my $metadatasize = $datasize/100;
+               # but at least 1G, as recommended in lvmthin man
+               $metadatasize = 1024*1024 if $metadatasize < 1024*1024;
+               # but at most 16G, which is the current lvm max
+               $metadatasize = 16*1024*1024 if $metadatasize > 16*1024*1024;
+               # shrink data by needed amount for metadata
+               $datasize -= 2*$metadatasize;
+
+               run_command([
+                   '/sbin/lvcreate',
+                   '--type', 'thin-pool',
+                   "-L${datasize}K",
+                   '--poolmetadatasize', "${metadatasize}K",
+                   '-n', $name,
+                   $name
+               ]);
+
+               PVE::Diskmanage::udevadm_trigger($dev);
+
+               if ($param->{add_storage}) {
+                   PVE::API2::Storage::Config->create_or_update(
+                       $name,
+                       $node,
+                       $storage_params,
+                       $verify_params,
+                   );
+               }
+           });
+       };
+
+       return $rpcenv->fork_worker('lvmthincreate', $name, $user, $worker);
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'delete',
+    path => '{name}',
+    method => 'DELETE',
+    proxyto => 'node',
+    protected => 1,
+    permissions => {
+       check => ['perm', '/', ['Sys.Modify', 'Datastore.Allocate']],
+    },
+    description => "Remove an LVM thin pool.",
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           name => get_standard_option('pve-storage-id'),
+           'volume-group' => get_standard_option('pve-storage-id'),
+           'cleanup-config' => {
+               description => "Marks associated storage(s) as not available on this node anymore ".
+                   "or removes them from the configuration (if configured for this node only).",
+               type => 'boolean',
+               optional => 1,
+               default => 0,
+           },
+           'cleanup-disks' => {
+               description => "Also wipe disks so they can be repurposed afterwards.",
+               type => 'boolean',
+               optional => 1,
+               default => 0,
+           },
+       },
+    },
+    returns => { type => 'string' },
+    code => sub {
+       my ($param) = @_;
+
+       my $rpcenv = PVE::RPCEnvironment::get();
+       my $user = $rpcenv->get_user();
+
+       my $vg = $param->{'volume-group'};
+       my $lv = $param->{name};
+       my $node = $param->{node};
+
+       my $worker = sub {
+           PVE::Diskmanage::locked_disk_action(sub {
+               my $thinpools = PVE::Storage::LvmThinPlugin::list_thinpools();
+
+               die "no such thin pool ${vg}/${lv}\n"
+                   if !grep { $_->{lv} eq $lv && $_->{vg} eq $vg } $thinpools->@*;
+
+               run_command(['lvremove', '-y', "${vg}/${lv}"]);
+
+               my $config_err;
+               if ($param->{'cleanup-config'}) {
+                   my $match = sub {
+                       my ($scfg) = @_;
+                       return $scfg->{type} eq 'lvmthin'
+                           && $scfg->{vgname} eq $vg
+                           && $scfg->{thinpool} eq $lv;
+                   };
+                   eval { PVE::API2::Storage::Config->cleanup_storages_for_node($match, $node); };
+                   warn $config_err = $@ if $@;
+               }
+
+               if ($param->{'cleanup-disks'}) {
+                   my $vgs = PVE::Storage::LVMPlugin::lvm_vgs(1);
+
+                   die "no such volume group '$vg'\n" if !$vgs->{$vg};
+                   die "volume group '$vg' still in use\n" if $vgs->{$vg}->{lvcount} > 0;
+
+                   my $wiped = [];
+                   eval {
+                       for my $pv ($vgs->{$vg}->{pvs}->@*) {
+                           my $dev = PVE::Diskmanage::verify_blockdev_path($pv->{name});
+                           PVE::Diskmanage::wipe_blockdev($dev);
+                           push $wiped->@*, $dev;
+                       }
+                   };
+                   my $err = $@;
+                   PVE::Diskmanage::udevadm_trigger($wiped->@*);
+                   die "cleanup failed - $err" if $err;
+               }
+
+               die "config cleanup failed - $config_err" if $config_err;
+           });
+       };
+
+       return $rpcenv->fork_worker('lvmthinremove', "${vg}-${lv}", $user, $worker);
+    }});
+
+1;
diff --git a/src/PVE/API2/Disks/Makefile b/src/PVE/API2/Disks/Makefile
new file mode 100644 (file)
index 0000000..9152aed
--- /dev/null
@@ -0,0 +1,9 @@
+
+SOURCES= LVM.pm\
+        LVMThin.pm\
+        ZFS.pm\
+        Directory.pm
+
+.PHONY: install
+install:
+       for i in ${SOURCES}; do install -D -m 0644 $$i ${DESTDIR}${PERLDIR}/PVE/API2/Disks/$$i; done
diff --git a/src/PVE/API2/Disks/ZFS.pm b/src/PVE/API2/Disks/ZFS.pm
new file mode 100644 (file)
index 0000000..a9dc3a7
--- /dev/null
@@ -0,0 +1,612 @@
+package PVE::API2::Disks::ZFS;
+
+use strict;
+use warnings;
+
+use PVE::Diskmanage;
+use PVE::JSONSchema qw(get_standard_option parse_property_string);
+use PVE::Systemd;
+use PVE::API2::Storage::Config;
+use PVE::Storage;
+use PVE::Tools qw(run_command lock_file trim);
+
+use PVE::RPCEnvironment;
+use PVE::RESTHandler;
+
+use base qw(PVE::RESTHandler);
+
+my $ZPOOL = '/sbin/zpool';
+my $ZFS = '/sbin/zfs';
+
+sub get_pool_data {
+    die "zfsutils-linux not installed\n" if ! -f $ZPOOL;
+
+    my $propnames = [qw(name size alloc free frag dedup health)];
+    my $numbers = {
+       size => 1,
+       alloc => 1,
+       free => 1,
+       frag => 1,
+       dedup => 1,
+    };
+
+    my $pools = [];
+    run_command([$ZPOOL, 'list', '-HpPLo', join(',', @$propnames)], outfunc => sub {
+       my ($line) = @_;
+
+       my @props = split('\s+', trim($line));
+       my $pool = {};
+       for (my $i = 0; $i < scalar(@$propnames); $i++) {
+           if ($numbers->{$propnames->[$i]}) {
+               $pool->{$propnames->[$i]} = $props[$i] + 0;
+           } else {
+               $pool->{$propnames->[$i]} = $props[$i];
+           }
+       }
+
+       push @$pools, $pool;
+    });
+
+    return $pools;
+}
+
+__PACKAGE__->register_method ({
+    name => 'index',
+    path => '',
+    method => 'GET',
+    proxyto => 'node',
+    protected => 1,
+    permissions => {
+       check => ['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1],
+    },
+    description => "List Zpools.",
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+       },
+    },
+    returns => {
+       type => 'array',
+       items => {
+           type => 'object',
+           properties => {
+               name => {
+                   type => 'string',
+                   description => "",
+               },
+               size => {
+                   type => 'integer',
+                   description => "",
+               },
+               alloc => {
+                   type => 'integer',
+                   description => "",
+               },
+               free => {
+                   type => 'integer',
+                   description => "",
+               },
+               frag => {
+                   type => 'integer',
+                   description => "",
+               },
+               dedup => {
+                   type => 'number',
+                   description => "",
+               },
+               health => {
+                   type => 'string',
+                   description => "",
+               },
+           },
+       },
+       links => [ { rel => 'child', href => "{name}" } ],
+    },
+    code => sub {
+       my ($param) = @_;
+
+       return get_pool_data();
+    }});
+
+sub preparetree {
+    my ($el) = @_;
+    delete $el->{lvl};
+    if ($el->{children} && scalar(@{$el->{children}})) {
+       $el->{leaf} = 0;
+       foreach my $child (@{$el->{children}}) {
+           preparetree($child);
+       }
+    } else {
+       $el->{leaf} = 1;
+    }
+}
+
+
+__PACKAGE__->register_method ({
+    name => 'detail',
+    path => '{name}',
+    method => 'GET',
+    proxyto => 'node',
+    protected => 1,
+    permissions => {
+       check => ['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1],
+    },
+    description => "Get details about a zpool.",
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           name => get_standard_option('pve-storage-id'),
+       },
+    },
+    returns => {
+       type => 'object',
+       properties => {
+           name => {
+               type => 'string',
+               description => 'The name of the zpool.',
+           },
+           state => {
+               type => 'string',
+               description => 'The state of the zpool.',
+           },
+           status => {
+               optional => 1,
+               type => 'string',
+               description => 'Information about the state of the zpool.',
+           },
+           action => {
+               optional => 1,
+               type => 'string',
+               description => 'Information about the recommended action to fix the state.',
+           },
+           scan => {
+               optional => 1,
+               type => 'string',
+               description => 'Information about the last/current scrub.',
+           },
+           errors => {
+               type => 'string',
+               description => 'Information about the errors on the zpool.',
+           },
+           children => {
+               type => 'array',
+               description => "The pool configuration information, including the vdevs for each section (e.g. spares, cache), may be nested.",
+               items => {
+                   type => 'object',
+                   properties => {
+                       name => {
+                           type => 'string',
+                           description => 'The name of the vdev or section.',
+                       },
+                       state => {
+                           optional => 1,
+                           type => 'string',
+                           description => 'The state of the vdev.',
+                       },
+                       read => {
+                           optional => 1,
+                           type => 'number',
+                       },
+                       write => {
+                           optional => 1,
+                           type => 'number',
+                       },
+                       cksum => {
+                           optional => 1,
+                           type => 'number',
+                       },
+                       msg => {
+                           type => 'string',
+                           description => 'An optional message about the vdev.'
+                       }
+                   },
+               },
+           },
+       },
+    },
+    code => sub {
+       my ($param) = @_;
+
+       if (!-f $ZPOOL) {
+           die "zfsutils-linux not installed\n";
+       }
+
+       my $cmd = [$ZPOOL, 'status', '-P', $param->{name}];
+
+       my $pool = {
+           lvl => 0,
+       };
+
+       my $curfield;
+       my $config = 0;
+
+       my $stack = [$pool];
+       my $curlvl = 0;
+
+       run_command($cmd, outfunc => sub {
+           my ($line) = @_;
+
+           if ($line =~ m/^\s*(\S+): (\S+.*)$/) {
+               $curfield = $1;
+               $pool->{$curfield} = $2;
+
+               $config = 0 if $curfield eq 'errors';
+           } elsif (!$config && $line =~ m/^\s+(\S+.*)$/) {
+               $pool->{$curfield} .= " " . $1;
+           } elsif (!$config && $line =~ m/^\s*config:/) {
+               $config = 1;
+           } elsif ($config && $line =~ m/^(\s+)(\S+)\s*(\S+)?(?:\s+(\S+)\s+(\S+)\s+(\S+))?\s*(.*)$/) {
+               my ($space, $name, $state, $read, $write, $cksum, $msg) = ($1, $2, $3, $4, $5, $6, $7);
+               if ($name ne "NAME") {
+                   my $lvl = int(length($space) / 2) + 1; # two spaces per level
+                   my $vdev = {
+                       name => $name,
+                       msg => $msg,
+                       lvl => $lvl,
+                   };
+
+                   $vdev->{state} = $state if defined($state);
+                   $vdev->{read} = $read + 0 if defined($read);
+                   $vdev->{write} = $write + 0 if defined($write);
+                   $vdev->{cksum} = $cksum + 0 if defined($cksum);
+
+                   my $cur = pop @$stack;
+
+                   if ($lvl > $curlvl) {
+                       $cur->{children} = [ $vdev ];
+                   } elsif ($lvl == $curlvl) {
+                       $cur = pop @$stack;
+                       push @{$cur->{children}}, $vdev;
+                   } else {
+                       while ($lvl <= $cur->{lvl} && $cur->{lvl} != 0) {
+                           $cur = pop @$stack;
+                       }
+                       push @{$cur->{children}}, $vdev;
+                   }
+
+                   push @$stack, $cur;
+                   push @$stack, $vdev;
+                   $curlvl = $lvl;
+               }
+           }
+       });
+
+       # change treenodes for extjs tree
+       $pool->{name} = delete $pool->{pool};
+       preparetree($pool);
+
+       return $pool;
+    }});
+
+my $draid_config_format = {
+    spares => {
+       type => 'integer',
+       minimum => 0,
+       description => 'Number of dRAID spares.',
+    },
+    data => {
+       type => 'integer',
+       minimum => 1,
+       description => 'The number of data devices per redundancy group. (dRAID)',
+    },
+};
+
+__PACKAGE__->register_method ({
+    name => 'create',
+    path => '',
+    method => 'POST',
+    proxyto => 'node',
+    protected => 1,
+    permissions => {
+       check => ['perm', '/', ['Sys.Modify', 'Datastore.Allocate']],
+    },
+    description => "Create a ZFS pool.",
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           name => get_standard_option('pve-storage-id'),
+           raidlevel => {
+               type => 'string',
+               description => 'The RAID level to use.',
+               enum => [
+                   'single', 'mirror',
+                   'raid10', 'raidz', 'raidz2', 'raidz3',
+                   'draid', 'draid2', 'draid3',
+               ],
+           },
+           devices => {
+               type => 'string', format => 'string-list',
+               description => 'The block devices you want to create the zpool on.',
+           },
+           'draid-config' => {
+               type => 'string',
+               format => $draid_config_format,
+               optional => 1,
+           },
+           ashift => {
+               type => 'integer',
+               minimum => 9,
+               maximum => 16,
+               optional => 1,
+               default => 12,
+               description => 'Pool sector size exponent.',
+           },
+           compression => {
+               type => 'string',
+               description => 'The compression algorithm to use.',
+               enum => ['on', 'off', 'gzip', 'lz4', 'lzjb', 'zle', 'zstd'],
+               optional => 1,
+               default => 'on',
+           },
+           add_storage => {
+               description => "Configure storage using the zpool.",
+               type => 'boolean',
+               optional => 1,
+               default => 0,
+           },
+       },
+    },
+    returns => { type => 'string' },
+    code => sub {
+       my ($param) = @_;
+
+       my $rpcenv = PVE::RPCEnvironment::get();
+       my $user = $rpcenv->get_user();
+
+       my $name = $param->{name};
+       my $node = $param->{node};
+       my $devs = [PVE::Tools::split_list($param->{devices})];
+       my $raidlevel = $param->{raidlevel};
+       my $compression = $param->{compression} // 'on';
+
+       my $draid_config;
+       if (exists $param->{'draid-config'}) {
+           die "draid-config set without using dRAID level\n" if $raidlevel !~ m/^draid/;
+           $draid_config = parse_property_string($draid_config_format, $param->{'draid-config'});
+       }
+
+       for my $dev (@$devs) {
+           $dev = PVE::Diskmanage::verify_blockdev_path($dev);
+           PVE::Diskmanage::assert_disk_unused($dev);
+
+       }
+       my $storage_params = {
+           type => 'zfspool',
+           pool => $name,
+           storage => $name,
+           content => 'rootdir,images',
+           nodes => $node,
+       };
+       my $verify_params = [qw(pool)];
+
+       if ($param->{add_storage}) {
+           PVE::API2::Storage::Config->create_or_update(
+               $name,
+               $node,
+               $storage_params,
+               $verify_params,
+               1,
+           );
+       }
+
+       my $pools = get_pool_data();
+       die "pool '${name}' already exists on node '${node}'\n"
+           if grep { $_->{name} eq $name } @{$pools};
+
+       my $numdisks = scalar(@$devs);
+       my $mindisks = {
+           single => 1,
+           mirror => 2,
+           raid10 => 4,
+           raidz => 3,
+           raidz2 => 4,
+           raidz3 => 5,
+           draid => 3,
+           draid2 => 4,
+           draid3 => 5,
+       };
+
+       # sanity checks
+       die "raid10 needs an even number of disks\n"
+           if $raidlevel eq 'raid10' && $numdisks % 2 != 0;
+
+       die "please give only one disk for single disk mode\n"
+           if $raidlevel eq 'single' && $numdisks > 1;
+
+       die "$raidlevel needs at least $mindisks->{$raidlevel} disks\n"
+           if $numdisks < $mindisks->{$raidlevel};
+
+       # draid checks
+       if ($raidlevel =~ m/^draid/) {
+           # bare minimum would be two drives: one for parity & one for data, but forbid that
+           # because it makes no sense in practice, at least one spare disk should be used
+           my $draid_min = $mindisks->{$raidlevel} - 2;
+           if ($draid_config) {
+               $draid_min += $draid_config->{data} || 0;
+               $draid_min += $draid_config->{spares} || 0;
+           }
+           die "At least $draid_min disks needed for current dRAID config\n"
+               if $numdisks < $draid_min;
+       }
+
+       my $code = sub {
+           for my $dev (@$devs) {
+               PVE::Diskmanage::assert_disk_unused($dev);
+
+               my $is_partition = PVE::Diskmanage::is_partition($dev);
+
+               if ($is_partition) {
+                   eval {
+                       PVE::Diskmanage::change_parttype($dev, '6a898cc3-1dd2-11b2-99a6-080020736631');
+                   };
+                   warn $@ if $@;
+               }
+
+               my $sysfsdev = $is_partition ? PVE::Diskmanage::get_blockdev($dev) : $dev;
+
+               $sysfsdev =~ s!^/dev/!/sys/block/!;
+               if ($is_partition) {
+                   my $part = $dev =~ s!^/dev/!!r;
+                   $sysfsdev .= "/${part}";
+               }
+
+               my $udevinfo = PVE::Diskmanage::get_udev_info($sysfsdev);
+               $dev = $udevinfo->{by_id_link} if defined($udevinfo->{by_id_link});
+           }
+
+           # create zpool with desired raidlevel
+           my $ashift = $param->{ashift} // 12;
+
+           my $cmd = [$ZPOOL, 'create', '-o', "ashift=$ashift", $name];
+
+           if ($raidlevel eq 'raid10') {
+               for (my $i = 0; $i < @$devs; $i+=2) {
+                   push @$cmd, 'mirror', $devs->[$i], $devs->[$i+1];
+               }
+           } elsif ($raidlevel eq 'single') {
+               push @$cmd, $devs->[0];
+           } elsif ($raidlevel =~ m/^draid/) {
+               my $draid_cmd = $raidlevel;
+               $draid_cmd .= ":$draid_config->{data}d" if $draid_config->{data};
+               $draid_cmd .= ":$draid_config->{spares}s" if $draid_config->{spares};
+               push @$cmd, $draid_cmd, @$devs;
+           } else {
+               push @$cmd, $raidlevel, @$devs;
+           }
+
+           print "# ", join(' ', @$cmd), "\n";
+           run_command($cmd);
+
+           $cmd = [$ZFS, 'set', "compression=$compression", $name];
+           print "# ", join(' ', @$cmd), "\n";
+           run_command($cmd);
+
+           if (-e '/lib/systemd/system/zfs-import@.service') {
+               my $importunit = 'zfs-import@'. PVE::Systemd::escape_unit($name, undef) . '.service';
+               $cmd = ['systemctl', 'enable', $importunit];
+               print "# ", join(' ', @$cmd), "\n";
+               run_command($cmd);
+           }
+
+           PVE::Diskmanage::udevadm_trigger($devs->@*);
+
+           if ($param->{add_storage}) {
+               PVE::API2::Storage::Config->create_or_update(
+                   $name,
+                   $node,
+                   $storage_params,
+                   $verify_params,
+               );
+           }
+       };
+
+       return $rpcenv->fork_worker('zfscreate', $name, $user, sub {
+           PVE::Diskmanage::locked_disk_action($code);
+       });
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'delete',
+    path => '{name}',
+    method => 'DELETE',
+    proxyto => 'node',
+    protected => 1,
+    permissions => {
+       check => ['perm', '/', ['Sys.Modify', 'Datastore.Allocate']],
+    },
+    description => "Destroy a ZFS pool.",
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           name => get_standard_option('pve-storage-id'),
+           'cleanup-config' => {
+               description => "Marks associated storage(s) as not available on this node anymore ".
+                   "or removes them from the configuration (if configured for this node only).",
+               type => 'boolean',
+               optional => 1,
+               default => 0,
+           },
+           'cleanup-disks' => {
+               description => "Also wipe disks so they can be repurposed afterwards.",
+               type => 'boolean',
+               optional => 1,
+               default => 0,
+           },
+       },
+    },
+    returns => { type => 'string' },
+    code => sub {
+       my ($param) = @_;
+
+       my $rpcenv = PVE::RPCEnvironment::get();
+       my $user = $rpcenv->get_user();
+
+       my $name = $param->{name};
+       my $node = $param->{node};
+
+       my $worker = sub {
+           PVE::Diskmanage::locked_disk_action(sub {
+               my $to_wipe = [];
+               if ($param->{'cleanup-disks'}) {
+                   # Using -o name does not only output the name in combination with -v.
+                   run_command(['zpool', 'list', '-vHPL', $name], outfunc => sub {
+                       my ($line) = @_;
+
+                       my ($name) = PVE::Tools::split_list($line);
+                       return if $name !~ m|^/dev/.+|;
+
+                       my $dev = PVE::Diskmanage::verify_blockdev_path($name);
+                       my $wipe = $dev;
+
+                       $dev =~ s|^/dev/||;
+                       my $info = PVE::Diskmanage::get_disks($dev, 1, 1);
+                       die "unable to obtain information for disk '$dev'\n" if !$info->{$dev};
+
+                       # Wipe whole disk if usual ZFS layout with partition 9 as ZFS reserved.
+                       my $parent = $info->{$dev}->{parent};
+                       if ($parent && scalar(keys $info->%*) == 3) {
+                           $parent =~ s|^/dev/||;
+                           my $info9 = $info->{"${parent}9"};
+
+                           $wipe = $info->{$dev}->{parent} # need leading /dev/
+                               if $info9 && $info9->{used} && $info9->{used} =~ m/^ZFS reserved/;
+                       }
+
+                       push $to_wipe->@*, $wipe;
+                   });
+               }
+
+               if (-e '/lib/systemd/system/zfs-import@.service') {
+                   my $importunit = 'zfs-import@' . PVE::Systemd::escape_unit($name) . '.service';
+                   run_command(['systemctl', 'disable', $importunit]);
+               }
+
+               run_command(['zpool', 'destroy', $name]);
+
+               my $config_err;
+               if ($param->{'cleanup-config'}) {
+                   my $match = sub {
+                       my ($scfg) = @_;
+                       return $scfg->{type} eq 'zfspool' && $scfg->{pool} eq $name;
+                   };
+                   eval { PVE::API2::Storage::Config->cleanup_storages_for_node($match, $node); };
+                   warn $config_err = $@ if $@;
+               }
+
+               eval { PVE::Diskmanage::wipe_blockdev($_) for $to_wipe->@*; };
+               my $err = $@;
+               PVE::Diskmanage::udevadm_trigger($to_wipe->@*);
+               die "cleanup failed - $err" if $err;
+
+               die "config cleanup failed - $config_err" if $config_err;
+           });
+       };
+
+       return $rpcenv->fork_worker('zfsremove', $name, $user, $worker);
+    }});
+
+1;
diff --git a/src/PVE/API2/Makefile b/src/PVE/API2/Makefile
new file mode 100644 (file)
index 0000000..fe316c5
--- /dev/null
@@ -0,0 +1,7 @@
+
+
+.PHONY: install
+install:
+       install -D -m 0644 Disks.pm ${DESTDIR}${PERLDIR}/PVE/API2/Disks.pm
+       make -C Storage install
+       make -C Disks install
diff --git a/src/PVE/API2/Storage/Config.pm b/src/PVE/API2/Storage/Config.pm
new file mode 100755 (executable)
index 0000000..821db21
--- /dev/null
@@ -0,0 +1,424 @@
+package PVE::API2::Storage::Config;
+
+use strict;
+use warnings;
+
+use PVE::SafeSyslog;
+use PVE::Tools qw(extract_param extract_sensitive_params);
+use PVE::Cluster qw(cfs_read_file cfs_write_file);
+use PVE::Storage;
+use PVE::Storage::Plugin;
+use PVE::Storage::LVMPlugin;
+use PVE::Storage::CIFSPlugin;
+use HTTP::Status qw(:constants);
+use Storable qw(dclone);
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::RPCEnvironment;
+
+use PVE::RESTHandler;
+
+use base qw(PVE::RESTHandler);
+
+my @ctypes = qw(images vztmpl iso backup);
+
+my $storage_type_enum = PVE::Storage::Plugin->lookup_types();
+
+my $api_storage_config = sub {
+    my ($cfg, $storeid) = @_;
+
+    my $scfg = dclone(PVE::Storage::storage_config($cfg, $storeid));
+    $scfg->{storage} = $storeid;
+    $scfg->{digest} = $cfg->{digest};
+    $scfg->{content} = PVE::Storage::Plugin->encode_value($scfg->{type}, 'content', $scfg->{content});
+
+    if ($scfg->{nodes}) {
+       $scfg->{nodes} = PVE::Storage::Plugin->encode_value($scfg->{type}, 'nodes', $scfg->{nodes});
+    }
+
+    return $scfg;
+};
+
+# For storages that $match->($scfg), update node restrictions to not include $node anymore and
+# in case no node remains, remove the storage altogether.
+sub cleanup_storages_for_node {
+    my ($self, $match, $node) = @_;
+
+    my $config = PVE::Storage::config();
+    my $cluster_nodes = PVE::Cluster::get_nodelist();
+
+    for my $storeid (keys $config->{ids}->%*) {
+       my $scfg = PVE::Storage::storage_config($config, $storeid);
+       next if !$match->($scfg);
+
+       my $nodes = $scfg->{nodes} || { map { $_ => 1 } $cluster_nodes->@* };
+       next if !$nodes->{$node}; # not configured on $node, so nothing to do
+       delete $nodes->{$node};
+
+       if (scalar(keys $nodes->%*) > 0) {
+           $self->update({
+               nodes => join(',', sort keys $nodes->%*),
+               storage => $storeid,
+           });
+       } else {
+           $self->delete({storage => $storeid});
+       }
+    }
+}
+
+# Decides if a storage needs to be created or updated. An update is needed, if
+# the storage has a node list configured, then the current node will be added.
+# The verify_params parameter is an array of parameter names that need to match
+# if there already is a storage config of the same name present.  This is
+# mainly intended for local storage types as certain parameters need to be the
+# same.  For exmaple 'pool' for ZFS, 'vg_name' for LVM, ...
+# Set the dryrun parameter, to only verify the parameters without updating or
+# creating the storage.
+sub create_or_update {
+    my ($self, $sid, $node, $storage_params, $verify_params, $dryrun) = @_;
+
+    my $cfg = PVE::Storage::config();
+    my $scfg = PVE::Storage::storage_config($cfg, $sid, 1);
+
+    if ($scfg) {
+       die "storage config for '${sid}' exists but no parameters to verify were provided\n"
+           if !$verify_params;
+
+       $node = PVE::INotify::nodename() if !$node || ($node eq 'localhost');
+       die "Storage ID '${sid}' already exists on node ${node}\n"
+           if !defined($scfg->{nodes}) || $scfg->{nodes}->{$node};
+
+       push @$verify_params, 'type';
+       for my $key (@$verify_params) {
+           if (!defined($scfg->{$key})) {
+               die "Option '${key}' is not configured for storage '$sid', "
+                   ."expected it to be '$storage_params->{$key}'";
+           }
+           if ($storage_params->{$key} ne $scfg->{$key}) {
+               die "Option '${key}' ($storage_params->{$key}) does not match "
+                   ."existing storage configuration '$scfg->{$key}'\n";
+           }
+       }
+    }
+
+    if (!$dryrun) {
+       if ($scfg) {
+           if ($scfg->{nodes}) {
+               $scfg->{nodes}->{$node} = 1;
+               $self->update({
+                   nodes => join(',', sort keys $scfg->{nodes}->%*),
+                   storage => $sid,
+               });
+               print "Added '${node}' to nodes for storage '${sid}'\n";
+           }
+       } else {
+           $self->create($storage_params);
+       }
+    }
+}
+
+__PACKAGE__->register_method ({
+    name => 'index',
+    path => '',
+    method => 'GET',
+    description => "Storage index.",
+    permissions => {
+       description => "Only list entries where you have 'Datastore.Audit' or 'Datastore.AllocateSpace' permissions on '/storage/<storage>'",
+       user => 'all',
+    },
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           type => {
+               description => "Only list storage of specific type",
+               type => 'string',
+               enum => $storage_type_enum,
+               optional => 1,
+           },
+       },
+    },
+    returns => {
+       type => 'array',
+       items => {
+           type => "object",
+           properties => { storage => { type => 'string'} },
+       },
+       links => [ { rel => 'child', href => "{storage}" } ],
+    },
+    code => sub {
+       my ($param) = @_;
+
+       my $rpcenv = PVE::RPCEnvironment::get();
+       my $authuser = $rpcenv->get_user();
+
+       my $cfg = PVE::Storage::config();
+
+       my @sids = PVE::Storage::storage_ids($cfg);
+
+       my $res = [];
+       foreach my $storeid (@sids) {
+           my $privs = [ 'Datastore.Audit', 'Datastore.AllocateSpace' ];
+           next if !$rpcenv->check_any($authuser, "/storage/$storeid", $privs, 1);
+
+           my $scfg = &$api_storage_config($cfg, $storeid);
+           next if $param->{type} && $param->{type} ne $scfg->{type};
+           push @$res, $scfg;
+       }
+
+       return $res;
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'read',
+    path => '{storage}',
+    method => 'GET',
+    description => "Read storage configuration.",
+    permissions => {
+       check => ['perm', '/storage/{storage}', ['Datastore.Allocate']],
+    },
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           storage => get_standard_option('pve-storage-id'),
+       },
+    },
+    returns => { type => 'object' },
+    code => sub {
+       my ($param) = @_;
+
+       my $cfg = PVE::Storage::config();
+
+       return &$api_storage_config($cfg, $param->{storage});
+    }});
+
+my $sensitive_params = [qw(password encryption-key master-pubkey keyring)];
+
+__PACKAGE__->register_method ({
+    name => 'create',
+    protected => 1,
+    path => '',
+    method => 'POST',
+    description => "Create a new storage.",
+    permissions => {
+       check => ['perm', '/storage', ['Datastore.Allocate']],
+    },
+    parameters => PVE::Storage::Plugin->createSchema(),
+    returns => {
+       type => 'object',
+       properties => {
+           storage => {
+               description => "The ID of the created storage.",
+               type => 'string',
+           },
+           type => {
+               description => "The type of the created storage.",
+               type => 'string',
+               enum => $storage_type_enum,
+           },
+           config => {
+               description => "Partial, possible server generated, configuration properties.",
+               type => 'object',
+               optional => 1,
+               additionalProperties => 1,
+               properties => {
+                   'encryption-key' => {
+                       description => "The, possible auto-generated, encryption-key.",
+                       optional => 1,
+                       type => 'string',
+                   },
+               },
+           },
+       },
+    },
+    code => sub {
+       my ($param) = @_;
+
+       my $type = extract_param($param, 'type');
+       my $storeid = extract_param($param, 'storage');
+
+       # revent an empty nodelist.
+       # fix me in section config create never need an empty entity.
+       delete $param->{nodes} if !$param->{nodes};
+
+       my $sensitive = extract_sensitive_params($param, $sensitive_params, []);
+
+       my $plugin = PVE::Storage::Plugin->lookup($type);
+       my $opts = $plugin->check_config($storeid, $param, 1, 1);
+
+       my $returned_config;
+       PVE::Storage::lock_storage_config(sub {
+           my $cfg = PVE::Storage::config();
+
+           if (my $scfg = PVE::Storage::storage_config($cfg, $storeid, 1)) {
+               die "storage ID '$storeid' already defined\n";
+           }
+
+           $cfg->{ids}->{$storeid} = $opts;
+
+           $returned_config = $plugin->on_add_hook($storeid, $opts, %$sensitive);
+
+           eval {
+               # try to activate if enabled on local node,
+               # we only do this to detect errors/problems sooner
+               if (PVE::Storage::storage_check_enabled($cfg, $storeid, undef, 1)) {
+                   PVE::Storage::activate_storage($cfg, $storeid);
+               }
+           };
+           if (my $err = $@) {
+               eval { $plugin->on_delete_hook($storeid, $opts) };
+               warn "$@\n" if $@;
+               die $err;
+           }
+
+           PVE::Storage::write_config($cfg);
+
+       }, "create storage failed");
+
+       my $res = {
+           storage => $storeid,
+           type => $type,
+       };
+       $res->{config} = $returned_config if $returned_config;
+       return $res;
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'update',
+    protected => 1,
+    path => '{storage}',
+    method => 'PUT',
+    description => "Update storage configuration.",
+    permissions => {
+       check => ['perm', '/storage', ['Datastore.Allocate']],
+    },
+    parameters => PVE::Storage::Plugin->updateSchema(),
+    returns => {
+       type => 'object',
+       properties => {
+           storage => {
+               description => "The ID of the created storage.",
+               type => 'string',
+           },
+           type => {
+               description => "The type of the created storage.",
+               type => 'string',
+               enum => $storage_type_enum,
+           },
+           config => {
+               description => "Partial, possible server generated, configuration properties.",
+               type => 'object',
+               optional => 1,
+               additionalProperties => 1,
+               properties => {
+                   'encryption-key' => {
+                       description => "The, possible auto-generated, encryption-key.",
+                       optional => 1,
+                       type => 'string',
+                   },
+               },
+           },
+       },
+    },
+    code => sub {
+       my ($param) = @_;
+
+       my $storeid = extract_param($param, 'storage');
+       my $digest = extract_param($param, 'digest');
+       my $delete = extract_param($param, 'delete');
+       my $type;
+
+       if ($delete) {
+           $delete = [ PVE::Tools::split_list($delete) ];
+       }
+
+       my $returned_config;
+        PVE::Storage::lock_storage_config(sub {
+           my $cfg = PVE::Storage::config();
+
+           PVE::SectionConfig::assert_if_modified($cfg, $digest);
+
+           my $scfg = PVE::Storage::storage_config($cfg, $storeid);
+           $type = $scfg->{type};
+
+           my $sensitive = extract_sensitive_params($param, $sensitive_params, $delete);
+
+           my $plugin = PVE::Storage::Plugin->lookup($type);
+           my $opts = $plugin->check_config($storeid, $param, 0, 1);
+
+           if ($delete) {
+               my $options = $plugin->private()->{options}->{$type};
+               foreach my $k (@$delete) {
+                   my $d = $options->{$k} || die "no such option '$k'\n";
+                   die "unable to delete required option '$k'\n" if !$d->{optional};
+                   die "unable to delete fixed option '$k'\n" if $d->{fixed};
+                   die "cannot set and delete property '$k' at the same time!\n"
+                       if defined($opts->{$k});
+
+                   delete $scfg->{$k};
+               }
+           }
+
+           $returned_config = $plugin->on_update_hook($storeid, $opts, %$sensitive);
+
+           for my $k (keys %$opts) {
+               $scfg->{$k} = $opts->{$k};
+           }
+
+           PVE::Storage::write_config($cfg);
+
+       }, "update storage failed");
+
+       my $res = {
+           storage => $storeid,
+           type => $type,
+       };
+       $res->{config} = $returned_config if $returned_config;
+       return $res;
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'delete',
+    protected => 1,
+    path => '{storage}', # /storage/config/{storage}
+    method => 'DELETE',
+    description => "Delete storage configuration.",
+    permissions => {
+       check => ['perm', '/storage', ['Datastore.Allocate']],
+    },
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           storage => get_standard_option('pve-storage-id', {
+               completion => \&PVE::Storage::complete_storage,
+           }),
+       },
+    },
+    returns => { type => 'null' },
+    code => sub {
+       my ($param) = @_;
+
+       my $storeid = extract_param($param, 'storage');
+
+        PVE::Storage::lock_storage_config(sub {
+           my $cfg = PVE::Storage::config();
+
+           my $scfg = PVE::Storage::storage_config($cfg, $storeid);
+
+           die "can't remove storage - storage is used as base of another storage\n"
+               if PVE::Storage::storage_is_used($cfg, $storeid);
+
+           my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+
+           $plugin->on_delete_hook($storeid, $scfg);
+
+           delete $cfg->{ids}->{$storeid};
+
+           PVE::Storage::write_config($cfg);
+
+       }, "delete storage failed");
+
+       PVE::AccessControl::remove_storage_access($storeid);
+
+       return undef;
+    }});
+
+1;
diff --git a/src/PVE/API2/Storage/Content.pm b/src/PVE/API2/Storage/Content.pm
new file mode 100644 (file)
index 0000000..fe0ad4a
--- /dev/null
@@ -0,0 +1,560 @@
+package PVE::API2::Storage::Content;
+
+use strict;
+use warnings;
+use Data::Dumper;
+
+use PVE::SafeSyslog;
+use PVE::Cluster;
+use PVE::Storage;
+use PVE::INotify;
+use PVE::Exception qw(raise_param_exc);
+use PVE::RPCEnvironment;
+use PVE::RESTHandler;
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::SSHInfo;
+
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method ({
+    name => 'index',
+    path => '',
+    method => 'GET',
+    description => "List storage content.",
+    permissions => {
+       check => ['perm', '/storage/{storage}', ['Datastore.Audit', 'Datastore.AllocateSpace'], any => 1],
+    },
+    protected => 1,
+    proxyto => 'node',
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           storage => get_standard_option('pve-storage-id', {
+               completion => \&PVE::Storage::complete_storage_enabled,
+            }),
+           content => {
+               description => "Only list content of this type.",
+               type => 'string', format => 'pve-storage-content',
+               optional => 1,
+               completion => \&PVE::Storage::complete_content_type,
+           },
+           vmid => get_standard_option('pve-vmid', {
+               description => "Only list images for this VM",
+               optional => 1,
+               completion => \&PVE::Cluster::complete_vmid,
+           }),
+       },
+    },
+    returns => {
+       type => 'array',
+       items => {
+           type => "object",
+           properties => {
+               volid => {
+                   description => "Volume identifier.",
+                   type => 'string',
+               },
+               vmid => {
+                   description => "Associated Owner VMID.",
+                   type => 'integer',
+                   optional => 1,
+               },
+               parent => {
+                   description => "Volume identifier of parent (for linked cloned).",
+                   type => 'string',
+                   optional => 1,
+               },
+               'format' => {
+                   description => "Format identifier ('raw', 'qcow2', 'subvol', 'iso', 'tgz' ...)",
+                   type => 'string',
+               },
+               size => {
+                   description => "Volume size in bytes.",
+                   type => 'integer',
+                   renderer => 'bytes',
+               },
+               used => {
+                   description => "Used space. Please note that most storage plugins " .
+                       "do not report anything useful here.",
+                   type => 'integer',
+                   renderer => 'bytes',
+                   optional => 1,
+               },
+               ctime => {
+                   description => "Creation time (seconds since the UNIX Epoch).",
+                   type => 'integer',
+                   minimum => 0,
+                   optional => 1,
+               },
+               notes => {
+                   description => "Optional notes. If they contain multiple lines, only the first one is returned here.",
+                   type => 'string',
+                   optional => 1,
+               },
+               encrypted => {
+                   description => "If whole backup is encrypted, value is the fingerprint or '1' "
+                       ." if encrypted. Only useful for the Proxmox Backup Server storage type.",
+                   type => 'string',
+                   optional => 1,
+               },
+               verification => {
+                   description => "Last backup verification result, only useful for PBS storages.",
+                   type => 'object',
+                   properties => {
+                       state => {
+                           description => "Last backup verification state.",
+                           type => 'string',
+                       },
+                       upid => {
+                           description => "Last backup verification UPID.",
+                           type => 'string',
+                       },
+                   },
+                   optional => 1,
+               },
+               protected => {
+                   description => "Protection status. Currently only supported for backups.",
+                   type => 'boolean',
+                   optional => 1,
+               },
+           },
+       },
+       links => [ { rel => 'child', href => "{volid}" } ],
+    },
+    code => sub {
+       my ($param) = @_;
+
+       my $rpcenv = PVE::RPCEnvironment::get();
+
+       my $authuser = $rpcenv->get_user();
+
+       my $storeid = $param->{storage};
+
+       my $cfg = PVE::Storage::config();
+
+       my $vollist = PVE::Storage::volume_list($cfg, $storeid, $param->{vmid}, $param->{content});
+
+       my $res = [];
+       foreach my $item (@$vollist) {
+           eval {  PVE::Storage::check_volume_access($rpcenv, $authuser, $cfg, undef, $item->{volid}); };
+           next if $@;
+           $item->{vmid} = int($item->{vmid}) if defined($item->{vmid});
+           $item->{size} = int($item->{size}) if defined($item->{size});
+           $item->{used} = int($item->{used}) if defined($item->{used});
+           push @$res, $item;
+       }
+
+       return $res;
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'create',
+    path => '',
+    method => 'POST',
+    description => "Allocate disk images.",
+    permissions => {
+       check => ['perm', '/storage/{storage}', ['Datastore.AllocateSpace']],
+    },
+    protected => 1,
+    proxyto => 'node',
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           storage => get_standard_option('pve-storage-id', {
+               completion => \&PVE::Storage::complete_storage_enabled,
+           }),
+           filename => {
+               description => "The name of the file to create.",
+               type => 'string',
+           },
+           vmid => get_standard_option('pve-vmid', {
+               description => "Specify owner VM",
+               completion => \&PVE::Cluster::complete_vmid,
+           }),
+           size => {
+               description => "Size in kilobyte (1024 bytes). Optional suffixes 'M' (megabyte, 1024K) and 'G' (gigabyte, 1024M)",
+               type => 'string',
+               pattern => '\d+[MG]?',
+           },
+           'format' => {
+               type => 'string',
+               enum => ['raw', 'qcow2', 'subvol'],
+               requires => 'size',
+               optional => 1,
+           },
+       },
+    },
+    returns => {
+       description => "Volume identifier",
+       type => 'string',
+    },
+    code => sub {
+       my ($param) = @_;
+
+       my $storeid = $param->{storage};
+       my $name = $param->{filename};
+       my $sizestr = $param->{size};
+
+       my $size;
+       if ($sizestr =~ m/^\d+$/) {
+           $size = $sizestr;
+       } elsif ($sizestr =~ m/^(\d+)M$/) {
+           $size = $1 * 1024;
+       } elsif ($sizestr =~ m/^(\d+)G$/) {
+           $size = $1 * 1024 * 1024;
+       } else {
+           raise_param_exc({ size => "unable to parse size '$sizestr'" });
+       }
+
+       # extract FORMAT from name
+       if ($name =~ m/\.(raw|qcow2|vmdk)$/) {
+           my $fmt = $1;
+
+           raise_param_exc({ format => "different storage formats ($param->{format} != $fmt)" })
+               if $param->{format} && $param->{format} ne $fmt;
+
+           $param->{format} = $fmt;
+       }
+
+       my $cfg = PVE::Storage::config();
+
+       my $volid = PVE::Storage::vdisk_alloc ($cfg, $storeid, $param->{vmid},
+                                              $param->{format},
+                                              $name, $size);
+
+       return $volid;
+    }});
+
+# we allow to pass volume names (without storage prefix) if the storage
+# is specified as separate parameter.
+my $real_volume_id = sub {
+    my ($storeid, $volume) = @_;
+
+    my $volid;
+
+    if ($volume =~ m/:/) {
+       eval {
+           my ($sid, $volname) = PVE::Storage::parse_volume_id ($volume);
+           die "storage ID mismatch ($sid != $storeid)\n"
+               if $storeid && $sid ne $storeid;
+           $volid = $volume;
+           $storeid = $sid;
+       };
+       raise_param_exc({ volume => $@ }) if $@;
+
+    } else {
+       raise_param_exc({ volume => "no storage specified - incomplete volume ID" })
+           if !$storeid;
+
+       $volid = "$storeid:$volume";
+    }
+
+    return wantarray ? ($volid, $storeid) : $volid;
+};
+
+__PACKAGE__->register_method ({
+    name => 'info',
+    path => '{volume}',
+    method => 'GET',
+    description => "Get volume attributes",
+    permissions => {
+       description => "You need read access for the volume.",
+       user => 'all',
+    },
+    protected => 1,
+    proxyto => 'node',
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           storage => get_standard_option('pve-storage-id', { optional => 1 }),
+           volume => {
+               description => "Volume identifier",
+               type => 'string',
+           },
+       },
+    },
+    returns => {
+       type => 'object',
+       properties => {
+           path => {
+               description => "The Path",
+               type => 'string',
+           },
+           size => {
+               description => "Volume size in bytes.",
+               type => 'integer',
+               renderer => 'bytes',
+           },
+           used => {
+               description => "Used space. Please note that most storage plugins " .
+               "do not report anything useful here.",
+               type => 'integer',
+               renderer => 'bytes',
+           },
+           format => {
+               description => "Format identifier ('raw', 'qcow2', 'subvol', 'iso', 'tgz' ...)",
+               type => 'string',
+           },
+           notes => {
+               description => "Optional notes.",
+               optional => 1,
+               type => 'string',
+           },
+           protected => {
+               description => "Protection status. Currently only supported for backups.",
+               type => 'boolean',
+               optional => 1,
+           },
+       },
+    },
+    code => sub {
+       my ($param) = @_;
+
+       my $rpcenv = PVE::RPCEnvironment::get();
+       my $authuser = $rpcenv->get_user();
+
+       my ($volid, $storeid) = &$real_volume_id($param->{storage}, $param->{volume});
+
+       my $cfg = PVE::Storage::config();
+
+       PVE::Storage::check_volume_access($rpcenv, $authuser, $cfg, undef, $volid);
+
+       my $path = PVE::Storage::path($cfg, $volid);
+       my ($size, $format, $used, $parent) =  PVE::Storage::volume_size_info($cfg, $volid);
+       die "volume_size_info on '$volid' failed\n" if !($format && $size);
+
+       my $entry = {
+           path => $path,
+           size => int($size), # cast to integer in case it was changed to a string previously
+           used => int($used),
+           format => $format,
+       };
+
+       for my $attribute (qw(notes protected)) {
+           # keep going if fetching an optional attribute fails
+           eval {
+               my $value = PVE::Storage::get_volume_attribute($cfg, $volid, $attribute);
+               $entry->{$attribute} = $value if defined($value);
+           };
+           warn $@ if $@;
+       }
+
+       return $entry;
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'updateattributes',
+    path => '{volume}',
+    method => 'PUT',
+    description => "Update volume attributes",
+    permissions => {
+       description => "You need read access for the volume.",
+       user => 'all',
+    },
+    protected => 1,
+    proxyto => 'node',
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           storage => get_standard_option('pve-storage-id', { optional => 1 }),
+           volume => {
+               description => "Volume identifier",
+               type => 'string',
+           },
+           notes => {
+               description => "The new notes.",
+               type => 'string',
+               optional => 1,
+           },
+           protected => {
+               description => "Protection status. Currently only supported for backups.",
+               type => 'boolean',
+               optional => 1,
+           },
+       },
+    },
+    returns => { type => 'null' },
+    code => sub {
+       my ($param) = @_;
+
+       my $rpcenv = PVE::RPCEnvironment::get();
+       my $authuser = $rpcenv->get_user();
+
+       my ($volid, $storeid) = &$real_volume_id($param->{storage}, $param->{volume});
+
+       my $cfg = PVE::Storage::config();
+
+       PVE::Storage::check_volume_access($rpcenv, $authuser, $cfg, undef, $volid);
+
+       for my $attr (qw(notes protected)) {
+           if (exists $param->{$attr}) {
+               PVE::Storage::update_volume_attribute($cfg, $volid, $attr, $param->{$attr});
+           }
+       }
+
+       return undef;
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'delete',
+    path => '{volume}',
+    method => 'DELETE',
+    description => "Delete volume",
+    permissions => {
+       description => "You need 'Datastore.Allocate' privilege on the storage (or 'Datastore.AllocateSpace' for backup volumes if you have VM.Backup privilege on the VM).",
+       user => 'all',
+    },
+    protected => 1,
+    proxyto => 'node',
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           storage => get_standard_option('pve-storage-id', {
+               optional => 1,
+               completion => \&PVE::Storage::complete_storage,
+           }),
+           volume => {
+               description => "Volume identifier",
+               type => 'string',
+               completion => \&PVE::Storage::complete_volume,
+           },
+           delay => {
+               type => 'integer',
+               description => "Time to wait for the task to finish. We return 'null' if the task finish within that time.",
+               minimum => 1,
+               maximum => 30,
+               optional => 1,
+           },
+       },
+    },
+    returns => { type => 'string', optional => 1, },
+    code => sub {
+       my ($param) = @_;
+
+       my $rpcenv = PVE::RPCEnvironment::get();
+       my $authuser = $rpcenv->get_user();
+
+       my $cfg = PVE::Storage::config();
+
+       my ($volid, $storeid) = &$real_volume_id($param->{storage}, $param->{volume});
+
+       my ($path, $ownervm, $vtype) = PVE::Storage::path($cfg, $volid);
+       if ($vtype eq 'backup' && $ownervm) {
+           $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
+           $rpcenv->check($authuser, "/vms/$ownervm", ['VM.Backup']);
+       } else {
+           $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.Allocate']);
+       }
+
+       my $worker = sub {
+           PVE::Storage::vdisk_free ($cfg, $volid);
+           print "Removed volume '$volid'\n";
+           if ($vtype eq 'backup'
+               && $path =~ /(.*\/vzdump-\w+-\d+-\d{4}_\d{2}_\d{2}-\d{2}_\d{2}_\d{2})[^\/]+$/) {
+               # Remove log file #318 and notes file #3972 if they still exist
+               PVE::Storage::archive_auxiliaries_remove($path);
+           }
+       };
+
+       my $id = (defined $ownervm ? "$ownervm@" : '') . $storeid;
+       my $upid = $rpcenv->fork_worker('imgdel', $id, $authuser, $worker);
+       my $background_delay = $param->{delay};
+       if ($background_delay) {
+           my $end_time = time() + $background_delay;
+           my $currently_deleting; # not necessarily true, e.g. sequential api call from cli
+           do {
+               my $task = PVE::Tools::upid_decode($upid);
+               $currently_deleting = PVE::ProcFSTools::check_process_running($task->{pid}, $task->{pstart});
+               sleep 1 if $currently_deleting;
+           } while (time() < $end_time && $currently_deleting);
+
+           if (!$currently_deleting) {
+               my $status = PVE::Tools::upid_read_status($upid);
+               chomp $status;
+               return undef if !PVE::Tools::upid_status_is_error($status);
+               die "$status\n";
+           }
+       }
+       return $upid;
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'copy',
+    path => '{volume}',
+    method => 'POST',
+    description => "Copy a volume. This is experimental code - do not use.",
+    protected => 1,
+    proxyto => 'node',
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           storage => get_standard_option('pve-storage-id', { optional => 1}),
+           volume => {
+               description => "Source volume identifier",
+               type => 'string',
+           },
+           target => {
+               description => "Target volume identifier",
+               type => 'string',
+           },
+           target_node => get_standard_option('pve-node',  {
+               description => "Target node. Default is local node.",
+               optional => 1,
+           }),
+       },
+    },
+    returns => {
+       type => 'string',
+    },
+    code => sub {
+       my ($param) = @_;
+
+       my $rpcenv = PVE::RPCEnvironment::get();
+
+       my $user = $rpcenv->get_user();
+
+       my $target_node = $param->{target_node} || PVE::INotify::nodename();
+       # pvesh examples
+       # cd /nodes/localhost/storage/local/content
+       # pve:/> create local:103/vm-103-disk-1.raw -target local:103/vm-103-disk-2.raw
+       # pve:/> create 103/vm-103-disk-1.raw -target 103/vm-103-disk-3.raw
+
+       my $src_volid = &$real_volume_id($param->{storage}, $param->{volume});
+       my $dst_volid = &$real_volume_id($param->{storage}, $param->{target});
+
+       print "DEBUG: COPY $src_volid TO $dst_volid\n";
+
+       my $cfg = PVE::Storage::config();
+
+       # do all parameter checks first
+
+       # then do all short running task (to raise errors before we go to background)
+
+       # then start the worker task
+       my $worker = sub  {
+           my $upid = shift;
+
+           print "DEBUG: starting worker $upid\n";
+
+           my ($target_sid, $target_volname) = PVE::Storage::parse_volume_id($dst_volid);
+           #my $target_ip = PVE::Cluster::remote_node_ip($target_node);
+
+           # you need to get this working (fails currently, because storage_migrate() uses
+           # ssh to connect to local host (which is not needed
+           my $sshinfo = PVE::SSHInfo::get_ssh_info($target_node);
+           PVE::Storage::storage_migrate($cfg, $src_volid, $sshinfo, $target_sid, {'target_volname' => $target_volname});
+
+           print "DEBUG: end worker $upid\n";
+
+       };
+
+       return $rpcenv->fork_worker('imgcopy', undef, $user, $worker);
+    }});
+
+1;
diff --git a/src/PVE/API2/Storage/FileRestore.pm b/src/PVE/API2/Storage/FileRestore.pm
new file mode 100644 (file)
index 0000000..764ebfb
--- /dev/null
@@ -0,0 +1,215 @@
+package PVE::API2::Storage::FileRestore;
+
+use strict;
+use warnings;
+
+use MIME::Base64;
+use PVE::Exception qw(raise_param_exc);
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::PBSClient;
+use PVE::Storage;
+use PVE::Tools qw(extract_param);
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+my $parse_volname_or_id = sub {
+    my ($storeid, $volume) = @_;
+
+    my $volid;
+    my ($sid, $volname) = PVE::Storage::parse_volume_id($volume, 1);
+
+    if (defined($sid)) {
+       raise_param_exc({ volume => "storage ID mismatch ($sid != $storeid)." })
+           if $sid ne $storeid;
+
+       $volid = $volume;
+    } elsif ($volume =~ m/^backup\//) {
+       $volid = "$storeid:$volume";
+    } else {
+       $volid = "$storeid:backup/$volume";
+    }
+
+    return $volid;
+};
+
+__PACKAGE__->register_method ({
+    name => 'list',
+    path => 'list',
+    method => 'GET',
+    proxyto => 'node',
+    permissions => {
+       description => "You need read access for the volume.",
+       user => 'all',
+    },
+    description => "List files and directories for single file restore under the given path.",
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           storage => get_standard_option('pve-storage-id', {
+               completion => \&PVE::Storage::complete_storage_enabled,
+           }),
+           volume => {
+               description => "Backup volume ID or name. Currently only PBS snapshots are supported.",
+               type => 'string',
+               completion => \&PVE::Storage::complete_volume,
+           },
+           filepath => {
+               description => 'base64-path to the directory or file being listed, or "/".',
+               type => 'string',
+           },
+       },
+    },
+    returns => {
+       type => 'array',
+       items => {
+           type => "object",
+           properties => {
+               filepath => {
+                   description => "base64 path of the current entry",
+                   type => 'string',
+               },
+               type => {
+                   description => "Entry type.",
+                   type => 'string',
+               },
+               text => {
+                   description => "Entry display text.",
+                   type => 'string',
+               },
+               leaf => {
+                   description => "If this entry is a leaf in the directory graph.",
+                   type => 'boolean',
+               },
+               size => {
+                   description => "Entry file size.",
+                   type => 'integer',
+                   optional => 1,
+               },
+               mtime => {
+                   description => "Entry last-modified time (unix timestamp).",
+                   type => 'integer',
+                   optional => 1,
+               },
+           },
+       },
+    },
+    protected => 1,
+    code => sub {
+       my ($param) = @_;
+
+       my $rpcenv = PVE::RPCEnvironment::get();
+       my $user = $rpcenv->get_user();
+
+       my $path = extract_param($param, 'filepath') || "/";
+       my $base64 = $path ne "/";
+
+       my $storeid = extract_param($param, 'storage');
+
+       my $volid = $parse_volname_or_id->($storeid, $param->{volume});
+       my $cfg = PVE::Storage::config();
+       my $scfg = PVE::Storage::storage_config($cfg, $storeid);
+
+       PVE::Storage::check_volume_access($rpcenv, $user, $cfg, undef, $volid, 'backup');
+
+       raise_param_exc({'storage' => "Only PBS storages supported for file-restore."})
+           if $scfg->{type} ne 'pbs';
+
+       my (undef, $snap) = PVE::Storage::parse_volname($cfg, $volid);
+
+       my $client = PVE::PBSClient->new($scfg, $storeid);
+       my $ret = $client->file_restore_list($snap, $path, $base64, { timeout => 25 });
+
+       if (ref($ret) eq "HASH") {
+           my $msg = $ret->{message};
+           if (my $code = $ret->{code}) {
+               die PVE::Exception->new("$msg\n", code => $code);
+           } else {
+               die "$msg\n";
+           }
+       } elsif (ref($ret) eq "ARRAY") {
+           # 'leaf' is a proper JSON boolean, map to perl-y bool
+           # TODO: make PBSClient decode all bools always as 1/0?
+           foreach my $item (@$ret) {
+               $item->{leaf} = $item->{leaf} ? 1 : 0;
+           }
+
+           return $ret;
+       }
+
+       die "invalid proxmox-file-restore output";
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'download',
+    path => 'download',
+    method => 'GET',
+    proxyto => 'node',
+    permissions => {
+       description => "You need read access for the volume.",
+       user => 'all',
+    },
+    description => "Extract a file or directory (as zip archive) from a PBS backup.",
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           storage => get_standard_option('pve-storage-id', {
+               completion => \&PVE::Storage::complete_storage_enabled,
+           }),
+           volume => {
+               description => "Backup volume ID or name. Currently only PBS snapshots are supported.",
+               type => 'string',
+               completion => \&PVE::Storage::complete_volume,
+           },
+           filepath => {
+               description => 'base64-path to the directory or file to download.',
+               type => 'string',
+           },
+       },
+    },
+    returns => {
+       type => 'any', # download
+    },
+    protected => 1,
+    code => sub {
+       my ($param) = @_;
+
+       my $rpcenv = PVE::RPCEnvironment::get();
+       my $user = $rpcenv->get_user();
+
+       my $path = extract_param($param, 'filepath');
+       my $storeid = extract_param($param, 'storage');
+       my $volid = $parse_volname_or_id->($storeid, $param->{volume});
+
+       my $cfg = PVE::Storage::config();
+       my $scfg = PVE::Storage::storage_config($cfg, $storeid);
+
+       PVE::Storage::check_volume_access($rpcenv, $user, $cfg, undef, $volid, 'backup');
+
+       raise_param_exc({'storage' => "Only PBS storages supported for file-restore."})
+           if $scfg->{type} ne 'pbs';
+
+       my (undef, $snap) = PVE::Storage::parse_volname($cfg, $volid);
+
+       my $client = PVE::PBSClient->new($scfg, $storeid);
+       my $fifo = $client->file_restore_extract_prepare();
+
+       $rpcenv->fork_worker('pbs-download', undef, $user, sub {
+           my $name = decode_base64($path);
+           print "Starting download of file: $name\n";
+           $client->file_restore_extract($fifo, $snap, $path, 1);
+       });
+
+       my $ret = {
+           download => {
+               path => $fifo,
+               stream => 1,
+               'content-type' => 'application/octet-stream',
+           },
+       };
+       return $ret;
+    }});
+
+1;
diff --git a/src/PVE/API2/Storage/Makefile b/src/PVE/API2/Storage/Makefile
new file mode 100644 (file)
index 0000000..1705080
--- /dev/null
@@ -0,0 +1,6 @@
+
+SOURCES= Content.pm Status.pm Config.pm PruneBackups.pm Scan.pm FileRestore.pm
+
+.PHONY: install
+install:
+       for i in ${SOURCES}; do install -D -m 0644 $$i ${DESTDIR}${PERLDIR}/PVE/API2/Storage/$$i; done
diff --git a/src/PVE/API2/Storage/PruneBackups.pm b/src/PVE/API2/Storage/PruneBackups.pm
new file mode 100644 (file)
index 0000000..e6ab276
--- /dev/null
@@ -0,0 +1,164 @@
+package PVE::API2::Storage::PruneBackups;
+
+use strict;
+use warnings;
+
+use PVE::Cluster;
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::RESTHandler;
+use PVE::RPCEnvironment;
+use PVE::Storage;
+use PVE::Tools qw(extract_param);
+
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method ({
+    name => 'dryrun',
+    path => '',
+    method => 'GET',
+    description => "Get prune information for backups. NOTE: this is only a preview and might not be " .
+                  "what a subsequent prune call does if backups are removed/added in the meantime.",
+    permissions => {
+       check => ['perm', '/storage/{storage}', ['Datastore.Audit', 'Datastore.AllocateSpace'], any => 1],
+    },
+    protected => 1,
+    proxyto => 'node',
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           storage => get_standard_option('pve-storage-id', {
+               completion => \&PVE::Storage::complete_storage_enabled,
+            }),
+           'prune-backups' => get_standard_option('prune-backups', {
+               description => "Use these retention options instead of those from the storage configuration.",
+               optional => 1,
+           }),
+           type => {
+               description => "Either 'qemu' or 'lxc'. Only consider backups for guests of this type.",
+               type => 'string',
+               optional => 1,
+               enum => ['qemu', 'lxc'],
+           },
+           vmid => get_standard_option('pve-vmid', {
+               description => "Only consider backups for this guest.",
+               optional => 1,
+               completion => \&PVE::Cluster::complete_vmid,
+           }),
+       },
+    },
+    returns => {
+       type => 'array',
+       items => {
+           type => 'object',
+           properties => {
+               volid => {
+                   description => "Backup volume ID.",
+                   type => 'string',
+               },
+               'ctime' => {
+                   description => "Creation time of the backup (seconds since the UNIX epoch).",
+                   type => 'integer',
+               },
+               'mark' => {
+                   description => "Whether the backup would be kept or removed. Backups that are" .
+                       " protected or don't use the standard naming scheme are not removed.",
+                   type => 'string',
+                   enum => ['keep', 'remove', 'protected', 'renamed'],
+               },
+               type => {
+                   description => "One of 'qemu', 'lxc', 'openvz' or 'unknown'.",
+                   type => 'string',
+               },
+               'vmid' => {
+                   description => "The VM the backup belongs to.",
+                   type => 'integer',
+                   optional => 1,
+               },
+           },
+       },
+    },
+    code => sub {
+       my ($param) = @_;
+
+       my $cfg = PVE::Storage::config();
+
+       my $vmid = extract_param($param, 'vmid');
+       my $type = extract_param($param, 'type');
+       my $storeid = extract_param($param, 'storage');
+
+       my $prune_backups = extract_param($param, 'prune-backups');
+       $prune_backups = PVE::JSONSchema::parse_property_string('prune-backups', $prune_backups)
+           if defined($prune_backups);
+
+       return PVE::Storage::prune_backups($cfg, $storeid, $prune_backups, $vmid, $type, 1);
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'delete',
+    path => '',
+    method => 'DELETE',
+    description => "Prune backups. Only those using the standard naming scheme are considered.",
+    permissions => {
+       description => "You need the 'Datastore.Allocate' privilege on the storage " .
+                      "(or if a VM ID is specified, 'Datastore.AllocateSpace' and 'VM.Backup' for the VM).",
+       user => 'all',
+    },
+    protected => 1,
+    proxyto => 'node',
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           storage => get_standard_option('pve-storage-id', {
+                completion => \&PVE::Storage::complete_storage,
+            }),
+           'prune-backups' => get_standard_option('prune-backups', {
+               description => "Use these retention options instead of those from the storage configuration.",
+           }),
+           type => {
+               description => "Either 'qemu' or 'lxc'. Only consider backups for guests of this type.",
+               type => 'string',
+               optional => 1,
+               enum => ['qemu', 'lxc'],
+           },
+           vmid => get_standard_option('pve-vmid', {
+               description => "Only prune backups for this VM.",
+               completion => \&PVE::Cluster::complete_vmid,
+               optional => 1,
+           }),
+       },
+    },
+    returns => { type => 'string' },
+    code => sub {
+       my ($param) = @_;
+
+       my $rpcenv = PVE::RPCEnvironment::get();
+       my $authuser = $rpcenv->get_user();
+
+       my $cfg = PVE::Storage::config();
+
+       my $vmid = extract_param($param, 'vmid');
+       my $type = extract_param($param, 'type');
+       my $storeid = extract_param($param, 'storage');
+
+       my $prune_backups = extract_param($param, 'prune-backups');
+       $prune_backups = PVE::JSONSchema::parse_property_string('prune-backups', $prune_backups)
+           if defined($prune_backups);
+
+       if (defined($vmid)) {
+           $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
+           $rpcenv->check($authuser, "/vms/$vmid", ['VM.Backup']);
+       } else {
+           $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.Allocate']);
+       }
+
+       my $id = (defined($vmid) ? "$vmid@" : '') . $storeid;
+       my $worker = sub {
+           PVE::Storage::prune_backups($cfg, $storeid, $prune_backups, $vmid, $type, 0);
+       };
+
+       return $rpcenv->fork_worker('prunebackups', $id, $authuser, $worker);
+    }});
+
+1;
diff --git a/src/PVE/API2/Storage/Scan.pm b/src/PVE/API2/Storage/Scan.pm
new file mode 100644 (file)
index 0000000..d7a8743
--- /dev/null
@@ -0,0 +1,449 @@
+package PVE::API2::Storage::Scan;
+
+use strict;
+use warnings;
+
+# NOTE: This API endpoints are mounted by pve-manager's API2::Node module and pvesm CLI
+
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::RESTHandler;
+use PVE::SafeSyslog;
+use PVE::Storage::LVMPlugin;
+use PVE::Storage;
+use PVE::SysFSTools;
+
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method({
+    name => 'index',
+    path => '',
+    method => 'GET',
+    description => "Index of available scan methods",
+    permissions => {
+       user => 'all',
+    },
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+       },
+    },
+    returns => {
+       type => 'array',
+       items => {
+           type => "object",
+           properties => {
+               method => { type => 'string'},
+           },
+       },
+       links => [ { rel => 'child', href => "{method}" } ],
+    },
+    code => sub {
+       my ($param) = @_;
+
+       my $res = [
+           { method => 'cifs' },
+           { method => 'glusterfs' },
+           { method => 'iscsi' },
+           { method => 'lvm' },
+           { method => 'nfs' },
+           { method => 'pbs' },
+           { method => 'zfs' },
+       ];
+
+       return $res;
+    }});
+
+__PACKAGE__->register_method({
+    name => 'nfsscan',
+    path => 'nfs',
+    method => 'GET',
+    description => "Scan remote NFS server.",
+    protected => 1,
+    proxyto => "node",
+    permissions => {
+       check => ['perm', '/storage', ['Datastore.Allocate']],
+    },
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           server => {
+               description => "The server address (name or IP).",
+               type => 'string', format => 'pve-storage-server',
+           },
+       },
+    },
+    returns => {
+       type => 'array',
+       items => {
+           type => "object",
+           properties => {
+               path => {
+                   description => "The exported path.",
+                   type => 'string',
+               },
+               options => {
+                   description => "NFS export options.",
+                   type => 'string',
+               },
+           },
+       },
+    },
+    code => sub {
+       my ($param) = @_;
+
+       my $server = $param->{server};
+       my $res = PVE::Storage::scan_nfs($server);
+
+       my $data = [];
+       foreach my $k (sort keys %$res) {
+           push @$data, { path => $k, options => $res->{$k} };
+       }
+       return $data;
+    }});
+
+__PACKAGE__->register_method({
+    name => 'cifsscan',
+    path => 'cifs',
+    method => 'GET',
+    description => "Scan remote CIFS server.",
+    protected => 1,
+    proxyto => "node",
+    permissions => {
+       check => ['perm', '/storage', ['Datastore.Allocate']],
+    },
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           server => {
+               description => "The server address (name or IP).",
+               type => 'string', format => 'pve-storage-server',
+           },
+           username => {
+               description => "User name.",
+               type => 'string',
+               optional => 1,
+           },
+           password => {
+               description => "User password.",
+               type => 'string',
+               optional => 1,
+           },
+           domain => {
+               description => "SMB domain (Workgroup).",
+               type => 'string',
+               optional => 1,
+           },
+       },
+    },
+    returns => {
+       type => 'array',
+       items => {
+           type => "object",
+           properties => {
+               share => {
+                   description => "The cifs share name.",
+                   type => 'string',
+               },
+               description => {
+                   description => "Descriptive text from server.",
+                   type => 'string',
+               },
+           },
+       },
+    },
+    code => sub {
+       my ($param) = @_;
+
+       my $server = $param->{server};
+
+       my $username = $param->{username};
+       my $password = $param->{password};
+       my $domain = $param->{domain};
+
+       my $res = PVE::Storage::scan_cifs($server, $username, $password, $domain);
+
+       my $data = [];
+       foreach my $k (sort keys %$res) {
+           next if $k =~ m/NT_STATUS_/;
+           push @$data, { share => $k, description => $res->{$k} };
+       }
+
+       return $data;
+    }});
+
+__PACKAGE__->register_method({
+    name => 'pbsscan',
+    path => 'pbs',
+    method => 'GET',
+    description => "Scan remote Proxmox Backup Server.",
+    protected => 1,
+    proxyto => "node",
+    permissions => {
+       check => ['perm', '/storage', ['Datastore.Allocate']],
+    },
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           server => {
+               description => "The server address (name or IP).",
+               type => 'string', format => 'pve-storage-server',
+           },
+           username => {
+               description => "User-name or API token-ID.",
+               type => 'string',
+           },
+           password => {
+               description => "User password or API token secret.",
+               type => 'string',
+           },
+           fingerprint => get_standard_option('fingerprint-sha256', {
+               optional => 1,
+           }),
+           port => {
+               description => "Optional port.",
+               type => 'integer',
+               minimum => 1,
+               maximum => 65535,
+               default => 8007,
+               optional => 1,
+           },
+       },
+    },
+    returns => {
+       type => 'array',
+       items => {
+           type => "object",
+           properties => {
+               store => {
+                   description => "The datastore name.",
+                   type => 'string',
+               },
+               comment => {
+                   description => "Comment from server.",
+                   type => 'string',
+                   optional => 1,
+               },
+           },
+       },
+    },
+    code => sub {
+       my ($param) = @_;
+
+       my $password = delete $param->{password};
+
+       return PVE::Storage::PBSPlugin::scan_datastores($param, $password);
+    }
+});
+
+# Note: GlusterFS currently does not have an equivalent of showmount.
+# As workaround, we simply use nfs showmount.
+# see http://www.gluster.org/category/volumes/
+__PACKAGE__->register_method({
+    name => 'glusterfsscan',
+    path => 'glusterfs',
+    method => 'GET',
+    description => "Scan remote GlusterFS server.",
+    protected => 1,
+    proxyto => "node",
+    permissions => {
+       check => ['perm', '/storage', ['Datastore.Allocate']],
+    },
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           server => {
+               description => "The server address (name or IP).",
+               type => 'string', format => 'pve-storage-server',
+           },
+       },
+    },
+    returns => {
+       type => 'array',
+       items => {
+           type => "object",
+           properties => {
+               volname => {
+                   description => "The volume name.",
+                   type => 'string',
+               },
+           },
+       },
+    },
+    code => sub {
+       my ($param) = @_;
+
+       my $server = $param->{server};
+       my $res = PVE::Storage::scan_nfs($server);
+
+       my $data = [];
+       foreach my $path (sort keys %$res) {
+           if ($path =~ m!^/([^\s/]+)$!) {
+               push @$data, { volname => $1 };
+           }
+       }
+       return $data;
+    }});
+
+__PACKAGE__->register_method({
+    name => 'iscsiscan',
+    path => 'iscsi',
+    method => 'GET',
+    description => "Scan remote iSCSI server.",
+    protected => 1,
+    proxyto => "node",
+    permissions => {
+       check => ['perm', '/storage', ['Datastore.Allocate']],
+    },
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           portal => {
+               description => "The iSCSI portal (IP or DNS name with optional port).",
+               type => 'string', format => 'pve-storage-portal-dns',
+           },
+       },
+    },
+    returns => {
+       type => 'array',
+       items => {
+           type => "object",
+           properties => {
+               target => {
+                   description => "The iSCSI target name.",
+                   type => 'string',
+               },
+               portal => {
+                   description => "The iSCSI portal name.",
+                   type => 'string',
+               },
+           },
+       },
+    },
+    code => sub {
+       my ($param) = @_;
+
+       my $res = PVE::Storage::scan_iscsi($param->{portal});
+
+       my $data = [];
+       foreach my $k (sort keys %$res) {
+           push @$data, { target => $k, portal => join(',', @{$res->{$k}}) };
+       }
+
+       return $data;
+    }});
+
+__PACKAGE__->register_method({
+    name => 'lvmscan',
+    path => 'lvm',
+    method => 'GET',
+    description => "List local LVM volume groups.",
+    protected => 1,
+    proxyto => "node",
+    permissions => {
+       check => ['perm', '/storage', ['Datastore.Allocate']],
+    },
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+       },
+    },
+    returns => {
+       type => 'array',
+       items => {
+           type => "object",
+           properties => {
+               vg => {
+                   description => "The LVM logical volume group name.",
+                   type => 'string',
+               },
+           },
+       },
+    },
+    code => sub {
+       my ($param) = @_;
+
+       my $res = PVE::Storage::LVMPlugin::lvm_vgs();
+       return PVE::RESTHandler::hash_to_array($res, 'vg');
+    }});
+
+__PACKAGE__->register_method({
+    name => 'lvmthinscan',
+    path => 'lvmthin',
+    method => 'GET',
+    description => "List local LVM Thin Pools.",
+    protected => 1,
+    proxyto => "node",
+    permissions => {
+       check => ['perm', '/storage', ['Datastore.Allocate']],
+    },
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           vg => {
+               type => 'string',
+               pattern => '[a-zA-Z0-9\.\+\_][a-zA-Z0-9\.\+\_\-]+', # see lvm(8) manpage
+               maxLength => 100,
+           },
+       },
+    },
+    returns => {
+       type => 'array',
+       items => {
+           type => "object",
+           properties => {
+               lv => {
+                   description => "The LVM Thin Pool name (LVM logical volume).",
+                   type => 'string',
+               },
+           },
+       },
+    },
+    code => sub {
+       my ($param) = @_;
+
+       return PVE::Storage::LvmThinPlugin::list_thinpools($param->{vg});
+    }});
+
+__PACKAGE__->register_method({
+    name => 'zfsscan',
+    path => 'zfs',
+    method => 'GET',
+    description => "Scan zfs pool list on local node.",
+    protected => 1,
+    proxyto => "node",
+    permissions => {
+       check => ['perm', '/storage', ['Datastore.Allocate']],
+    },
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+       },
+    },
+    returns => {
+       type => 'array',
+       items => {
+           type => "object",
+           properties => {
+               pool => {
+                   description => "ZFS pool name.",
+                   type => 'string',
+               },
+           },
+       },
+    },
+    code => sub {
+       my ($param) = @_;
+
+       return PVE::Storage::scan_zfs();
+    }});
+
+1;
diff --git a/src/PVE/API2/Storage/Status.pm b/src/PVE/API2/Storage/Status.pm
new file mode 100644 (file)
index 0000000..e4ce698
--- /dev/null
@@ -0,0 +1,660 @@
+package PVE::API2::Storage::Status;
+
+use strict;
+use warnings;
+
+use File::Basename;
+use File::Path;
+use POSIX qw(ENOENT);
+
+use PVE::Cluster;
+use PVE::Exception qw(raise_param_exc);
+use PVE::INotify;
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::RESTHandler;
+use PVE::RPCEnvironment;
+use PVE::RRD;
+use PVE::Tools qw(run_command);
+
+use PVE::API2::Storage::Content;
+use PVE::API2::Storage::FileRestore;
+use PVE::API2::Storage::PruneBackups;
+use PVE::Storage;
+
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method ({
+    subclass => "PVE::API2::Storage::PruneBackups",
+    path => '{storage}/prunebackups',
+});
+
+__PACKAGE__->register_method ({
+    subclass => "PVE::API2::Storage::Content",
+    # set fragment delimiter (no subdirs) - we need that, because volume
+    # IDs may contain a slash '/'
+    fragmentDelimiter => '',
+    path => '{storage}/content',
+});
+
+__PACKAGE__->register_method ({
+   subclass => "PVE::API2::Storage::FileRestore",
+   path => '{storage}/file-restore',
+});
+
+__PACKAGE__->register_method ({
+    name => 'index',
+    path => '',
+    method => 'GET',
+    description => "Get status for all datastores.",
+    permissions => {
+       description => "Only list entries where you have 'Datastore.Audit' or 'Datastore.AllocateSpace' permissions on '/storage/<storage>'",
+       user => 'all',
+    },
+    protected => 1,
+    proxyto => 'node',
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           storage => get_standard_option('pve-storage-id', {
+               description => "Only list status for  specified storage",
+               optional => 1,
+               completion => \&PVE::Storage::complete_storage_enabled,
+           }),
+           content => {
+               description => "Only list stores which support this content type.",
+               type => 'string', format => 'pve-storage-content-list',
+               optional => 1,
+               completion => \&PVE::Storage::complete_content_type,
+           },
+           enabled => {
+               description => "Only list stores which are enabled (not disabled in config).",
+               type => 'boolean',
+               optional => 1,
+               default => 0,
+           },
+           target => get_standard_option('pve-node', {
+               description => "If target is different to 'node', we only lists shared storages which " .
+                   "content is accessible on this 'node' and the specified 'target' node.",
+               optional => 1,
+               completion => \&PVE::Cluster::get_nodelist,
+           }),
+           'format' => {
+               description => "Include information about formats",
+               type => 'boolean',
+               optional => 1,
+               default => 0,
+           },
+       },
+    },
+    returns => {
+       type => 'array',
+       items => {
+           type => "object",
+           properties => {
+               storage => get_standard_option('pve-storage-id'),
+               type => {
+                   description => "Storage type.",
+                   type => 'string',
+               },
+               content => {
+                   description => "Allowed storage content types.",
+                   type => 'string', format => 'pve-storage-content-list',
+               },
+               enabled => {
+                   description => "Set when storage is enabled (not disabled).",
+                   type => 'boolean',
+                   optional => 1,
+               },
+               active => {
+                   description => "Set when storage is accessible.",
+                   type => 'boolean',
+                   optional => 1,
+               },
+               shared => {
+                   description => "Shared flag from storage configuration.",
+                   type => 'boolean',
+                   optional => 1,
+               },
+               total => {
+                   description => "Total storage space in bytes.",
+                   type => 'integer',
+                   renderer => 'bytes',
+                   optional => 1,
+               },
+               used => {
+                   description => "Used storage space in bytes.",
+                   type => 'integer',
+                   renderer => 'bytes',
+                   optional => 1,
+               },
+               avail => {
+                   description => "Available storage space in bytes.",
+                   type => 'integer',
+                   renderer => 'bytes',
+                   optional => 1,
+               },
+               used_fraction => {
+                   description => "Used fraction (used/total).",
+                   type => 'number',
+                   renderer => 'fraction_as_percentage',
+                   optional => 1,
+               },
+           },
+       },
+       links => [ { rel => 'child', href => "{storage}" } ],
+    },
+    code => sub {
+       my ($param) = @_;
+
+       my $rpcenv = PVE::RPCEnvironment::get();
+       my $authuser = $rpcenv->get_user();
+
+       my $localnode = PVE::INotify::nodename();
+
+       my $target = $param->{target};
+
+       undef $target if $target && ($target eq $localnode || $target eq 'localhost');
+
+       my $cfg = PVE::Storage::config();
+
+       my $info = PVE::Storage::storage_info($cfg, $param->{content}, $param->{format});
+
+       raise_param_exc({ storage => "No such storage." })
+           if $param->{storage} && !defined($info->{$param->{storage}});
+
+       my $res = {};
+       my @sids = PVE::Storage::storage_ids($cfg);
+       foreach my $storeid (@sids) {
+           my $data = $info->{$storeid};
+           next if !$data;
+           my $privs = [ 'Datastore.Audit', 'Datastore.AllocateSpace' ];
+           next if !$rpcenv->check_any($authuser, "/storage/$storeid", $privs, 1);
+           next if $param->{storage} && $param->{storage} ne $storeid;
+
+           my $scfg = PVE::Storage::storage_config($cfg, $storeid);
+
+           next if $param->{enabled} && $scfg->{disable};
+
+           if ($target) {
+               # check if storage content is accessible on local node and specified target node
+               # we use this on the Clone GUI
+
+               next if !$scfg->{shared};
+               next if !PVE::Storage::storage_check_node($cfg, $storeid, undef, 1);
+               next if !PVE::Storage::storage_check_node($cfg, $storeid, $target, 1);
+           }
+
+           if ($data->{total}) {
+               $data->{used_fraction} = ($data->{used} // 0) / $data->{total};
+           }
+
+           $res->{$storeid} = $data;
+       }
+
+       return PVE::RESTHandler::hash_to_array($res, 'storage');
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'diridx',
+    path => '{storage}',
+    method => 'GET',
+    description => "",
+    permissions => {
+       check => ['perm', '/storage/{storage}', ['Datastore.Audit', 'Datastore.AllocateSpace'], any => 1],
+    },
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           storage => get_standard_option('pve-storage-id'),
+       },
+    },
+    returns => {
+       type => 'array',
+       items => {
+           type => "object",
+           properties => {
+               subdir => { type => 'string' },
+           },
+       },
+       links => [ { rel => 'child', href => "{subdir}" } ],
+    },
+    code => sub {
+       my ($param) = @_;
+
+       my $res = [
+           { subdir => 'content' },
+           { subdir => 'download-url' },
+           { subdir => 'file-restore' },
+           { subdir => 'prunebackups' },
+           { subdir => 'rrd' },
+           { subdir => 'rrddata' },
+           { subdir => 'status' },
+           { subdir => 'upload' },
+       ];
+
+       return $res;
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'read_status',
+    path => '{storage}/status',
+    method => 'GET',
+    description => "Read storage status.",
+    permissions => {
+       check => ['perm', '/storage/{storage}', ['Datastore.Audit', 'Datastore.AllocateSpace'], any => 1],
+    },
+    protected => 1,
+    proxyto => 'node',
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           storage => get_standard_option('pve-storage-id'),
+       },
+    },
+    returns => {
+       type => "object",
+       properties => {},
+    },
+    code => sub {
+       my ($param) = @_;
+
+       my $cfg = PVE::Storage::config();
+
+       my $info = PVE::Storage::storage_info($cfg, $param->{content});
+
+       my $data = $info->{$param->{storage}};
+
+       raise_param_exc({ storage => "No such storage." })
+           if !defined($data);
+
+       return $data;
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'rrd',
+    path => '{storage}/rrd',
+    method => 'GET',
+    description => "Read storage RRD statistics (returns PNG).",
+    permissions => {
+       check => ['perm', '/storage/{storage}', ['Datastore.Audit', 'Datastore.AllocateSpace'], any => 1],
+    },
+    protected => 1,
+    proxyto => 'node',
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           storage => get_standard_option('pve-storage-id'),
+           timeframe => {
+               description => "Specify the time frame you are interested in.",
+               type => 'string',
+               enum => [ 'hour', 'day', 'week', 'month', 'year' ],
+           },
+           ds => {
+               description => "The list of datasources you want to display.",
+               type => 'string', format => 'pve-configid-list',
+           },
+           cf => {
+               description => "The RRD consolidation function",
+               type => 'string',
+               enum => [ 'AVERAGE', 'MAX' ],
+               optional => 1,
+           },
+       },
+    },
+    returns => {
+       type => "object",
+       properties => {
+           filename => { type => 'string' },
+       },
+    },
+    code => sub {
+       my ($param) = @_;
+
+       return PVE::RRD::create_rrd_graph(
+           "pve2-storage/$param->{node}/$param->{storage}",
+           $param->{timeframe}, $param->{ds}, $param->{cf});
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'rrddata',
+    path => '{storage}/rrddata',
+    method => 'GET',
+    description => "Read storage RRD statistics.",
+    permissions => {
+       check => ['perm', '/storage/{storage}', ['Datastore.Audit', 'Datastore.AllocateSpace'], any => 1],
+    },
+    protected => 1,
+    proxyto => 'node',
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           storage => get_standard_option('pve-storage-id'),
+           timeframe => {
+               description => "Specify the time frame you are interested in.",
+               type => 'string',
+               enum => [ 'hour', 'day', 'week', 'month', 'year' ],
+           },
+           cf => {
+               description => "The RRD consolidation function",
+               type => 'string',
+               enum => [ 'AVERAGE', 'MAX' ],
+               optional => 1,
+           },
+       },
+    },
+    returns => {
+       type => "array",
+       items => {
+           type => "object",
+           properties => {},
+       },
+    },
+    code => sub {
+       my ($param) = @_;
+
+       return PVE::RRD::create_rrd_data(
+           "pve2-storage/$param->{node}/$param->{storage}",
+           $param->{timeframe}, $param->{cf});
+    }});
+
+# makes no sense for big images and backup files (because it
+# create a copy of the file).
+__PACKAGE__->register_method ({
+    name => 'upload',
+    path => '{storage}/upload',
+    method => 'POST',
+    description => "Upload templates and ISO images.",
+    permissions => {
+       check => ['perm', '/storage/{storage}', ['Datastore.AllocateTemplate']],
+    },
+    protected => 1,
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           storage => get_standard_option('pve-storage-id'),
+           content => {
+               description => "Content type.",
+               type => 'string', format => 'pve-storage-content',
+               enum => ['iso', 'vztmpl'],
+           },
+           filename => {
+               description => "The name of the file to create. Caution: This will be normalized!",
+               maxLength => 255,
+               type => 'string',
+           },
+           checksum => {
+               description => "The expected checksum of the file.",
+               type => 'string',
+               requires => 'checksum-algorithm',
+               optional => 1,
+           },
+           'checksum-algorithm' => {
+               description => "The algorithm to calculate the checksum of the file.",
+               type => 'string',
+               enum => ['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512'],
+               requires => 'checksum',
+               optional => 1,
+           },
+           tmpfilename => {
+               description => "The source file name. This parameter is usually set by the REST handler. You can only overwrite it when connecting to the trusted port on localhost.",
+               type => 'string',
+               optional => 1,
+               pattern => '/var/tmp/pveupload-[0-9a-f]+',
+           },
+       },
+    },
+    returns => { type => "string" },
+    code => sub {
+       my ($param) = @_;
+
+       my $rpcenv = PVE::RPCEnvironment::get();
+
+       my $user = $rpcenv->get_user();
+
+       my $cfg = PVE::Storage::config();
+
+       my $node = $param->{node};
+       my $scfg = PVE::Storage::storage_check_enabled($cfg, $param->{storage}, $node);
+
+       die "can't upload to storage type '$scfg->{type}'\n"
+           if !defined($scfg->{path});
+
+       my $content = $param->{content};
+
+       my $tmpfilename = $param->{tmpfilename};
+       die "missing temporary file name\n" if !$tmpfilename;
+
+       my $size = -s $tmpfilename;
+       die "temporary file '$tmpfilename' does not exist\n" if !defined($size);
+
+       my $filename = PVE::Storage::normalize_content_filename($param->{filename});
+
+       my $path;
+
+       if ($content eq 'iso') {
+           if ($filename !~ m![^/]+$PVE::Storage::ISO_EXT_RE_0$!) {
+               raise_param_exc({ filename => "wrong file extension" });
+           }
+           $path = PVE::Storage::get_iso_dir($cfg, $param->{storage});
+       } elsif ($content eq 'vztmpl') {
+           if ($filename !~ m![^/]+$PVE::Storage::VZTMPL_EXT_RE_1$!) {
+               raise_param_exc({ filename => "wrong file extension" });
+           }
+           $path = PVE::Storage::get_vztmpl_dir($cfg, $param->{storage});
+       } else {
+           raise_param_exc({ content => "upload content type '$content' not allowed" });
+       }
+
+       die "storage '$param->{storage}' does not support '$content' content\n"
+           if !$scfg->{content}->{$content};
+
+       my $dest = "$path/$filename";
+       my $dirname = dirname($dest);
+
+       # best effort to match apl_download behaviour
+       chmod 0644, $tmpfilename;
+
+       my $err_cleanup = sub { unlink $dest; die "cleanup failed: $!\n" if $! && $! != ENOENT };
+
+       my $cmd;
+       if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) {
+           my $remip = PVE::Cluster::remote_node_ip($node);
+
+           my @ssh_options = ('-o', 'BatchMode=yes');
+
+           my @remcmd = ('/usr/bin/ssh', @ssh_options, $remip, '--');
+
+           eval { # activate remote storage
+               run_command([@remcmd, '/usr/sbin/pvesm', 'status', '--storage', $param->{storage}]);
+           };
+           die "can't activate storage '$param->{storage}' on node '$node': $@\n" if $@;
+
+           run_command(
+               [@remcmd, '/bin/mkdir', '-p', '--', PVE::Tools::shell_quote($dirname)],
+               errmsg => "mkdir failed",
+           );
+           $cmd = ['/usr/bin/scp', @ssh_options, '-p', '--', $tmpfilename, "[$remip]:" . PVE::Tools::shell_quote($dest)];
+
+           $err_cleanup = sub { run_command([@remcmd, 'rm', '-f', '--', $dest]) };
+       } else {
+           PVE::Storage::activate_storage($cfg, $param->{storage});
+           File::Path::make_path($dirname);
+           $cmd = ['cp', '--', $tmpfilename, $dest];
+       }
+
+       # NOTE: we simply overwrite the destination file if it already exists
+       my $worker = sub {
+           my $upid = shift;
+
+           print "starting file import from: $tmpfilename\n";
+
+           eval {
+               my ($checksum, $checksum_algorithm) = $param->@{'checksum', 'checksum-algorithm'};
+               if ($checksum_algorithm) {
+                   print "calculating checksum...";
+
+                   my $checksum_got = PVE::Tools::get_file_hash($checksum_algorithm, $tmpfilename);
+
+                   if (lc($checksum_got) eq lc($checksum)) {
+                       print "OK, checksum verified\n";
+                   } else {
+                       print "\n";  # the front end expects the error to reside at the last line without any noise
+                       die "checksum mismatch: got '$checksum_got' != expect '$checksum'\n";
+                   }
+               }
+           };
+           if (my $err = $@) {
+               # unlinks only the temporary file from the http server
+               unlink $tmpfilename;
+               warn "unable to clean up temporory file '$tmpfilename' - $!\n"
+                   if $! && $! != ENOENT;
+               die $err;
+           }
+
+           print "target node: $node\n";
+           print "target file: $dest\n";
+           print "file size is: $size\n";
+           print "command: " . join(' ', @$cmd) . "\n";
+
+           eval { run_command($cmd, errmsg => 'import failed'); };
+
+           unlink $tmpfilename; # the temporary file got only uploaded locally, no need to rm remote
+           warn "unable to clean up temporary file '$tmpfilename' - $!\n" if $! && $! != ENOENT;
+
+           if (my $err = $@) {
+               eval { $err_cleanup->() };
+               warn "$@" if $@;
+               die $err;
+           }
+           print "finished file import successfully\n";
+       };
+
+       return $rpcenv->fork_worker('imgcopy', undef, $user, $worker);
+   }});
+
+__PACKAGE__->register_method({
+    name => 'download_url',
+    path => '{storage}/download-url',
+    method => 'POST',
+    description => "Download templates and ISO images by using an URL.",
+    proxyto => 'node',
+    permissions => {
+       check => [ 'and',
+           ['perm', '/storage/{storage}', [ 'Datastore.AllocateTemplate' ]],
+           ['perm', '/', [ 'Sys.Audit', 'Sys.Modify' ]],
+       ],
+    },
+    protected => 1,
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           storage => get_standard_option('pve-storage-id'),
+           url => {
+               description => "The URL to download the file from.",
+               type => 'string',
+               pattern => 'https?://.*',
+           },
+           content => {
+               description => "Content type.", # TODO: could be optional & detected in most cases
+               type => 'string', format => 'pve-storage-content',
+               enum => ['iso', 'vztmpl'],
+           },
+           filename => {
+               description => "The name of the file to create. Caution: This will be normalized!",
+               maxLength => 255,
+               type => 'string',
+           },
+           checksum => {
+               description => "The expected checksum of the file.",
+               type => 'string',
+               requires => 'checksum-algorithm',
+               optional => 1,
+           },
+           'checksum-algorithm' => {
+               description => "The algorithm to calculate the checksum of the file.",
+               type => 'string',
+               enum => ['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512'],
+               requires => 'checksum',
+               optional => 1,
+           },
+           'verify-certificates' => {
+               description => "If false, no SSL/TLS certificates will be verified.",
+               type => 'boolean',
+               optional => 1,
+               default => 1,
+           },
+       },
+    },
+    returns => {
+       type => "string"
+    },
+    code => sub {
+       my ($param) = @_;
+
+       my $rpcenv = PVE::RPCEnvironment::get();
+       my $user = $rpcenv->get_user();
+
+       my $cfg = PVE::Storage::config();
+
+       my ($node, $storage) = $param->@{'node', 'storage'};
+       my $scfg = PVE::Storage::storage_check_enabled($cfg, $storage, $node);
+
+       die "can't upload to storage type '$scfg->{type}', not a file based storage!\n"
+           if !defined($scfg->{path});
+
+       my ($content, $url) = $param->@{'content', 'url'};
+
+       die "storage '$storage' is not configured for content-type '$content'\n"
+           if !$scfg->{content}->{$content};
+
+       my $filename = PVE::Storage::normalize_content_filename($param->{filename});
+
+       my $path;
+       if ($content eq 'iso') {
+           if ($filename !~ m![^/]+$PVE::Storage::ISO_EXT_RE_0$!) {
+               raise_param_exc({ filename => "wrong file extension" });
+           }
+           $path = PVE::Storage::get_iso_dir($cfg, $storage);
+       } elsif ($content eq 'vztmpl') {
+           if ($filename !~ m![^/]+$PVE::Storage::VZTMPL_EXT_RE_1$!) {
+               raise_param_exc({ filename => "wrong file extension" });
+           }
+           $path = PVE::Storage::get_vztmpl_dir($cfg, $storage);
+       } else {
+           raise_param_exc({ content => "upload content-type '$content' is not allowed" });
+       }
+
+       PVE::Storage::activate_storage($cfg, $storage);
+       File::Path::make_path($path);
+
+       my $dccfg = PVE::Cluster::cfs_read_file('datacenter.cfg');
+       my $opts = {
+           hash_required => 0,
+           verify_certificates => $param->{'verify-certificates'} // 1,
+           http_proxy => $dccfg->{http_proxy},
+       };
+
+       my ($checksum, $checksum_algorithm) = $param->@{'checksum', 'checksum-algorithm'};
+       if ($checksum) {
+           $opts->{"${checksum_algorithm}sum"} = $checksum;
+           $opts->{hash_required} = 1;
+       }
+
+       my $worker = sub {
+           PVE::Tools::download_file_from_url("$path/$filename", $url, $opts);
+       };
+
+       my $worker_id = PVE::Tools::encode_text($filename); # must not pass : or the like as w-ID
+
+       return $rpcenv->fork_worker('download', $worker_id, $user, $worker);
+    }});
+
+1;
diff --git a/src/PVE/CLI/Makefile b/src/PVE/CLI/Makefile
new file mode 100644 (file)
index 0000000..6c6e258
--- /dev/null
@@ -0,0 +1,9 @@
+SOURCES=pvesm.pm
+
+.PHONY: install
+install: ${SOURCES}
+       install -d -m 0755 ${DESTDIR}${PERLDIR}/PVE/CLI
+       for i in ${SOURCES}; do install -D -m 0644 $$i ${DESTDIR}${PERLDIR}/PVE/CLI/$$i; done
+
+
+clean:
diff --git a/src/PVE/CLI/pvesm.pm b/src/PVE/CLI/pvesm.pm
new file mode 100755 (executable)
index 0000000..9b9676b
--- /dev/null
@@ -0,0 +1,731 @@
+package PVE::CLI::pvesm;
+
+use strict;
+use warnings;
+
+use POSIX qw(O_RDONLY O_WRONLY O_CREAT O_TRUNC);
+use Fcntl ':flock';
+use File::Path;
+use MIME::Base64 qw(encode_base64);
+
+use IO::Socket::IP;
+use IO::Socket::UNIX;
+use Socket qw(SOCK_STREAM);
+
+use PVE::SafeSyslog;
+use PVE::Cluster;
+use PVE::INotify;
+use PVE::RPCEnvironment;
+use PVE::Storage;
+use PVE::Tools qw(extract_param);
+use PVE::API2::Storage::Config;
+use PVE::API2::Storage::Content;
+use PVE::API2::Storage::PruneBackups;
+use PVE::API2::Storage::Scan;
+use PVE::API2::Storage::Status;
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::PTY;
+
+use PVE::CLIHandler;
+
+use base qw(PVE::CLIHandler);
+
+my $nodename = PVE::INotify::nodename();
+
+sub param_mapping {
+    my ($name) = @_;
+
+    my $password_map = PVE::CLIHandler::get_standard_mapping('pve-password', {
+       func => sub {
+           my ($value) = @_;
+           return $value if $value;
+           return PVE::PTY::read_password("Enter Password: ");
+       },
+    });
+
+    my $enc_key_map = {
+       name => 'encryption-key',
+       desc => 'a file containing an encryption key, or the special value "autogen"',
+       func => sub {
+           my ($value) = @_;
+           return $value if $value eq 'autogen';
+           return PVE::Tools::file_get_contents($value);
+       }
+    };
+
+    my $master_key_map = {
+       name => 'master-pubkey',
+       desc => 'a file containing a PEM-formatted master public key',
+       func => sub {
+           my ($value) = @_;
+           return encode_base64(PVE::Tools::file_get_contents($value), '');
+       }
+    };
+
+    my $keyring_map = {
+       name => 'keyring',
+       desc => 'file containing the keyring to authenticate in the Ceph cluster',
+       func => sub {
+           my ($value) = @_;
+           return PVE::Tools::file_get_contents($value);
+       },
+    };
+
+    my $mapping = {
+       'cifsscan' => [ $password_map ],
+       'cifs' => [ $password_map ],
+       'pbs' => [ $password_map ],
+       'create' => [ $password_map, $enc_key_map, $master_key_map, $keyring_map ],
+       'update' => [ $password_map, $enc_key_map, $master_key_map, $keyring_map ],
+    };
+    return $mapping->{$name};
+}
+
+sub setup_environment {
+    PVE::RPCEnvironment->setup_default_cli_env();
+}
+
+__PACKAGE__->register_method ({
+    name => 'apiinfo',
+    path => 'apiinfo',
+    method => 'GET',
+    description => "Returns APIVER and APIAGE.",
+    parameters => {
+       additionalProperties => 0,
+       properties => {},
+    },
+    returns => {
+       type => 'object',
+       properties => {
+           apiver => { type => 'integer' },
+           apiage => { type => 'integer' },
+       },
+    },
+    code => sub {
+       return {
+           apiver => PVE::Storage::APIVER,
+           apiage => PVE::Storage::APIAGE,
+       };
+    }
+});
+
+__PACKAGE__->register_method ({
+    name => 'path',
+    path => 'path',
+    method => 'GET',
+    description => "Get filesystem path for specified volume",
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           volume => {
+               description => "Volume identifier",
+               type => 'string', format => 'pve-volume-id',
+               completion => \&PVE::Storage::complete_volume,
+           },
+       },
+    },
+    returns => { type => 'null' },
+
+    code => sub {
+       my ($param) = @_;
+
+       my $cfg = PVE::Storage::config();
+
+       my $path = PVE::Storage::path ($cfg, $param->{volume});
+
+       print "$path\n";
+
+       return undef;
+
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'extractconfig',
+    path => 'extractconfig',
+    method => 'GET',
+    description => "Extract configuration from vzdump backup archive.",
+    permissions => {
+       description => "The user needs 'VM.Backup' permissions on the backed up guest ID, and 'Datastore.AllocateSpace' on the backup storage.",
+       user => 'all',
+    },
+    protected => 1,
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           volume => {
+               description => "Volume identifier",
+               type => 'string',
+               completion => \&PVE::Storage::complete_volume,
+           },
+       },
+    },
+    returns => { type => 'null' },
+    code => sub {
+       my ($param) = @_;
+       my $volume = $param->{volume};
+
+       my $rpcenv = PVE::RPCEnvironment::get();
+       my $authuser = $rpcenv->get_user();
+
+       my $storage_cfg = PVE::Storage::config();
+       PVE::Storage::check_volume_access(
+           $rpcenv,
+           $authuser,
+           $storage_cfg,
+           undef,
+           $volume,
+           'backup',
+       );
+
+       if (PVE::Storage::parse_volume_id($volume, 1)) {
+           my (undef, undef, $ownervm) = PVE::Storage::parse_volname($storage_cfg, $volume);
+           $rpcenv->check($authuser, "/vms/$ownervm", ['VM.Backup']);
+       }
+
+       my $config_raw = PVE::Storage::extract_vzdump_config($storage_cfg, $volume);
+
+       print "$config_raw\n";
+       return;
+    }});
+
+my $print_content = sub {
+    my ($list) = @_;
+
+    my ($maxlenname, $maxsize) = (0, 0);
+    foreach my $info (@$list) {
+       my $volid = $info->{volid};
+       my $sidlen =  length ($volid);
+       $maxlenname = $sidlen if $sidlen > $maxlenname;
+       $maxsize = $info->{size} if ($info->{size} // 0) > $maxsize;
+    }
+    my $sizemaxdigits = length($maxsize);
+
+    my $basefmt = "%-${maxlenname}s %-7s %-9s %${sizemaxdigits}s";
+    printf "$basefmt %s\n", "Volid", "Format", "Type", "Size", "VMID";
+
+    foreach my $info (@$list) {
+       next if !$info->{vmid};
+       my $volid = $info->{volid};
+
+       printf "$basefmt %d\n", $volid, $info->{format}, $info->{content}, $info->{size}, $info->{vmid};
+    }
+
+    foreach my $info (sort { $a->{format} cmp $b->{format} } @$list) {
+       next if $info->{vmid};
+       my $volid = $info->{volid};
+
+       printf "$basefmt\n", $volid, $info->{format}, $info->{content}, $info->{size};
+    }
+};
+
+my $print_status = sub {
+    my $res = shift;
+
+    my $maxlen = 0;
+    foreach my $res (@$res) {
+       my $storeid = $res->{storage};
+       $maxlen = length ($storeid) if length ($storeid) > $maxlen;
+    }
+    $maxlen+=1;
+
+    printf "%-${maxlen}s %10s %10s %15s %15s %15s %8s\n", 'Name', 'Type',
+       'Status', 'Total', 'Used', 'Available', '%';
+
+    foreach my $res (sort { $a->{storage} cmp $b->{storage} } @$res) {
+       my $storeid = $res->{storage};
+
+       my $active = $res->{active} ? 'active' : 'inactive';
+       my ($per, $per_fmt) = (0, '% 7.2f%%');
+       $per = ($res->{used}*100)/$res->{total} if $res->{total} > 0;
+
+       if (!$res->{enabled}) {
+           $per = 'N/A';
+           $per_fmt = '% 8s';
+           $active = 'disabled';
+       }
+
+       printf "%-${maxlen}s %10s %10s %15d %15d %15d $per_fmt\n", $storeid,
+           $res->{type}, $active, $res->{total}/1024, $res->{used}/1024,
+           $res->{avail}/1024, $per;
+    }
+};
+
+__PACKAGE__->register_method ({
+    name => 'export',
+    path => 'export',
+    method => 'GET',
+    description => "Used internally to export a volume.",
+    protected => 1,
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           volume => {
+               description => "Volume identifier",
+               type => 'string',
+               completion => \&PVE::Storage::complete_volume,
+           },
+           format => {
+               description => "Export stream format",
+               type => 'string',
+               enum => $PVE::Storage::KNOWN_EXPORT_FORMATS,
+           },
+           filename => {
+               description => "Destination file name",
+               type => 'string',
+           },
+           base => {
+               description => "Snapshot to start an incremental stream from",
+               type => 'string',
+               pattern => qr/[a-z0-9_\-]{1,40}/i,
+               maxLength => 40,
+               optional => 1,
+           },
+           snapshot => {
+               description => "Snapshot to export",
+               type => 'string',
+               pattern => qr/[a-z0-9_\-]{1,40}/i,
+               maxLength => 40,
+               optional => 1,
+           },
+           'with-snapshots' => {
+               description =>
+                   "Whether to include intermediate snapshots in the stream",
+               type => 'boolean',
+               optional => 1,
+               default => 0,
+           },
+           'snapshot-list' => {
+               description => "Ordered list of snapshots to transfer",
+               type => 'string',
+               format => 'string-list',
+               optional => 1,
+           },
+       },
+    },
+    returns => { type => 'null' },
+    code => sub {
+       my ($param) = @_;
+
+       my $with_snapshots = $param->{'with-snapshots'};
+       if (defined(my $list = $param->{'snapshot-list'})) {
+           $with_snapshots = PVE::Tools::split_list($list);
+       }
+
+       my $filename = $param->{filename};
+
+       my $outfh;
+       if ($filename eq '-') {
+           $outfh = \*STDOUT;
+       } else {
+           sysopen($outfh, $filename, O_CREAT|O_WRONLY|O_TRUNC)
+               or die "open($filename): $!\n";
+       }
+
+       eval {
+           my $cfg = PVE::Storage::config();
+           PVE::Storage::volume_export($cfg, $outfh, $param->{volume}, $param->{format},
+               $param->{snapshot}, $param->{base}, $with_snapshots);
+       };
+       my $err = $@;
+       if ($filename ne '-') {
+           close($outfh);
+           unlink($filename) if $err;
+       }
+       die $err if $err;
+       return;
+    }
+});
+
+__PACKAGE__->register_method ({
+    name => 'import',
+    path => 'import',
+    method => 'PUT',
+    description => "Used internally to import a volume.",
+    protected => 1,
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           volume => {
+               description => "Volume identifier",
+               type => 'string',
+               completion => \&PVE::Storage::complete_volume,
+           },
+           format => {
+               description => "Import stream format",
+               type => 'string',
+               enum => $PVE::Storage::KNOWN_EXPORT_FORMATS,
+           },
+           filename => {
+               description => "Source file name. For '-' stdin is used, the " .
+                 "tcp://<IP-or-CIDR> format allows to use a TCP connection, " .
+                 "the unix://PATH-TO-SOCKET format a UNIX socket as input." .
+                 "Else, the file is treated as common file.",
+               type => 'string',
+           },
+           base => {
+               description => "Base snapshot of an incremental stream",
+               type => 'string',
+               pattern => qr/[a-z0-9_\-]{1,40}/i,
+               maxLength => 40,
+               optional => 1,
+           },
+           'with-snapshots' => {
+               description =>
+                   "Whether the stream includes intermediate snapshots",
+               type => 'boolean',
+               optional => 1,
+               default => 0,
+           },
+           'delete-snapshot' => {
+               description => "A snapshot to delete on success",
+               type => 'string',
+               pattern => qr/[a-z0-9_\-]{1,80}/i,
+               maxLength => 80,
+               optional => 1,
+           },
+           'allow-rename' => {
+               description => "Choose a new volume ID if the requested " .
+                 "volume ID already exists, instead of throwing an error.",
+               type => 'boolean',
+               optional => 1,
+               default => 0,
+           },
+           snapshot => {
+               description => "The current-state snapshot if the stream contains snapshots",
+               type => 'string',
+               pattern => qr/[a-z0-9_\-]{1,40}/i,
+               maxLength => 40,
+               optional => 1,
+           },
+       },
+    },
+    returns => { type => 'string' },
+    code => sub {
+       my ($param) = @_;
+
+       my $filename = $param->{filename};
+
+       my $infh;
+       if ($filename eq '-') {
+           $infh = \*STDIN;
+       } elsif ($filename =~ m!^tcp://(([^/]+)(/\d+)?)$!) {
+           my ($cidr, $ip, $subnet) = ($1, $2, $3);
+           if ($subnet) { # got real CIDR notation, not just IP
+               my $ips = PVE::Network::get_local_ip_from_cidr($cidr);
+               die "Unable to get any local IP address in network '$cidr'\n"
+                   if scalar(@$ips) < 1;
+               die "Got multiple local IP address in network '$cidr'\n"
+                   if scalar(@$ips) > 1;
+
+               $ip = $ips->[0];
+           }
+           my $family = PVE::Tools::get_host_address_family($ip);
+           my $port = PVE::Tools::next_migrate_port($family, $ip);
+
+           my $sock_params = {
+               Listen => 1,
+               ReuseAddr => 1,
+               Proto => &Socket::IPPROTO_TCP,
+               GetAddrInfoFlags => 0,
+               LocalAddr => $ip,
+               LocalPort => $port,
+           };
+           my $socket = IO::Socket::IP->new(%$sock_params)
+               or die "failed to open socket: $!\n";
+
+           print "$ip\n$port\n"; # tell remote where to connect
+           *STDOUT->flush();
+
+           my $prev_alarm = alarm 0;
+           local $SIG{ALRM} = sub { die "timed out waiting for client\n" };
+           alarm 30;
+           my $client = $socket->accept; # Wait for a client
+           alarm $prev_alarm;
+           close($socket);
+
+           $infh = \*$client;
+       } elsif ($filename =~ m!^unix://(.*)$!) {
+           my $socket_path = $1;
+           my $socket = IO::Socket::UNIX->new(
+               Type => SOCK_STREAM(),
+               Local => $socket_path,
+               Listen => 1,
+           ) or die "failed to open socket: $!\n";
+
+           print "ready\n";
+           *STDOUT->flush();
+
+           my $prev_alarm = alarm 0;
+           local $SIG{ALRM} = sub { die "timed out waiting for client\n" };
+           alarm 30;
+           my $client = $socket->accept; # Wait for a client
+           alarm $prev_alarm;
+           close($socket);
+
+           $infh = \*$client;
+       } else {
+           sysopen($infh, $filename, O_RDONLY)
+               or die "open($filename): $!\n";
+       }
+
+       my $cfg = PVE::Storage::config();
+       my $volume = $param->{volume};
+       my $delete = $param->{'delete-snapshot'};
+       my $imported_volid = PVE::Storage::volume_import($cfg, $infh, $volume, $param->{format},
+           $param->{snapshot}, $param->{base}, $param->{'with-snapshots'},
+           $param->{'allow-rename'});
+       PVE::Storage::volume_snapshot_delete($cfg, $imported_volid, $delete)
+           if defined($delete);
+       return $imported_volid;
+    }
+});
+
+__PACKAGE__->register_method ({
+    name => 'prunebackups',
+    path => 'prunebackups',
+    method => 'GET',
+    description => "Prune backups. Only those using the standard naming scheme are considered. " .
+                  "If no keep options are specified, those from the storage configuration are used.",
+    protected => 1,
+    proxyto => 'node',
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           'dry-run' => {
+               description => "Only show what would be pruned, don't delete anything.",
+               type => 'boolean',
+               optional => 1,
+           },
+           node => get_standard_option('pve-node'),
+           storage => get_standard_option('pve-storage-id', {
+               completion => \&PVE::Storage::complete_storage_enabled,
+            }),
+           %{$PVE::Storage::Plugin::prune_backups_format},
+           type => {
+               description => "Either 'qemu' or 'lxc'. Only consider backups for guests of this type.",
+               type => 'string',
+               optional => 1,
+               enum => ['qemu', 'lxc'],
+           },
+           vmid => get_standard_option('pve-vmid', {
+               description => "Only consider backups for this guest.",
+               optional => 1,
+               completion => \&PVE::Cluster::complete_vmid,
+           }),
+       },
+    },
+    returns => {
+       type => 'object',
+       properties => {
+           dryrun => {
+               description => 'If it was a dry run or not. The list will only be defined in that case.',
+               type => 'boolean',
+           },
+           list => {
+               type => 'array',
+               items => {
+                   type => 'object',
+                   properties => {
+                       volid => {
+                           description => "Backup volume ID.",
+                           type => 'string',
+                       },
+                       'ctime' => {
+                           description => "Creation time of the backup (seconds since the UNIX epoch).",
+                           type => 'integer',
+                       },
+                       'mark' => {
+                           description => "Whether the backup would be kept or removed. For backups that don't " .
+                                          "use the standard naming scheme, it's 'protected'.",
+                           type => 'string',
+                       },
+                       type => {
+                           description => "One of 'qemu', 'lxc', 'openvz' or 'unknown'.",
+                           type => 'string',
+                       },
+                       'vmid' => {
+                           description => "The VM the backup belongs to.",
+                           type => 'integer',
+                           optional => 1,
+                       },
+                   },
+               },
+           },
+       },
+    },
+    code => sub {
+       my ($param) = @_;
+
+       my $dryrun = extract_param($param, 'dry-run') ? 1 : 0;
+
+       my $keep_opts;
+       foreach my $keep (keys %{$PVE::Storage::Plugin::prune_backups_format}) {
+           $keep_opts->{$keep} = extract_param($param, $keep) if defined($param->{$keep});
+       }
+       $param->{'prune-backups'} = PVE::JSONSchema::print_property_string(
+           $keep_opts, $PVE::Storage::Plugin::prune_backups_format) if $keep_opts;
+
+       my $list = [];
+       if ($dryrun) {
+           $list = PVE::API2::Storage::PruneBackups->dryrun($param);
+       } else {
+           PVE::API2::Storage::PruneBackups->delete($param);
+       }
+
+       return {
+           dryrun => $dryrun,
+           list => $list,
+       };
+    }});
+
+my $print_api_result = sub {
+    my ($data, $schema, $options) = @_;
+    PVE::CLIFormatter::print_api_result($data, $schema, undef, $options);
+};
+
+our $cmddef = {
+    add => [ "PVE::API2::Storage::Config", 'create', ['type', 'storage'] ],
+    set => [ "PVE::API2::Storage::Config", 'update', ['storage'] ],
+    remove => [ "PVE::API2::Storage::Config", 'delete', ['storage'] ],
+    status => [ "PVE::API2::Storage::Status", 'index', [],
+               { node => $nodename }, $print_status ],
+    list => [ "PVE::API2::Storage::Content", 'index', ['storage'],
+             { node => $nodename }, $print_content ],
+    alloc => [ "PVE::API2::Storage::Content", 'create', ['storage', 'vmid', 'filename', 'size'],
+              { node => $nodename }, sub {
+                  my $volid = shift;
+                  print "successfully created '$volid'\n";
+              }],
+    free => [ "PVE::API2::Storage::Content", 'delete', ['volume'],
+             { node => $nodename } ],
+    scan => {
+       nfs => [ "PVE::API2::Storage::Scan", 'nfsscan', ['server'], { node => $nodename }, sub  {
+           my $res = shift;
+
+           my $maxlen = 0;
+           foreach my $rec (@$res) {
+               my $len = length ($rec->{path});
+               $maxlen = $len if $len > $maxlen;
+           }
+           foreach my $rec (@$res) {
+               printf "%-${maxlen}s %s\n", $rec->{path}, $rec->{options};
+           }
+       }],
+       cifs => [ "PVE::API2::Storage::Scan", 'cifsscan', ['server'], { node => $nodename }, sub  {
+           my $res = shift;
+
+           my $maxlen = 0;
+           foreach my $rec (@$res) {
+               my $len = length ($rec->{share});
+               $maxlen = $len if $len > $maxlen;
+           }
+           foreach my $rec (@$res) {
+               printf "%-${maxlen}s %s\n", $rec->{share}, $rec->{description};
+           }
+       }],
+       glusterfs => [ "PVE::API2::Storage::Scan", 'glusterfsscan', ['server'], { node => $nodename }, sub  {
+           my $res = shift;
+
+           foreach my $rec (@$res) {
+               printf "%s\n", $rec->{volname};
+           }
+       }],
+       iscsi => [ "PVE::API2::Storage::Scan", 'iscsiscan', ['portal'], { node => $nodename }, sub  {
+           my $res = shift;
+
+           my $maxlen = 0;
+           foreach my $rec (@$res) {
+               my $len = length ($rec->{target});
+               $maxlen = $len if $len > $maxlen;
+           }
+           foreach my $rec (@$res) {
+               printf "%-${maxlen}s %s\n", $rec->{target}, $rec->{portal};
+           }
+       }],
+       lvm => [ "PVE::API2::Storage::Scan", 'lvmscan', [], { node => $nodename }, sub  {
+           my $res = shift;
+           foreach my $rec (@$res) {
+               printf "$rec->{vg}\n";
+           }
+       }],
+       lvmthin => [ "PVE::API2::Storage::Scan", 'lvmthinscan', ['vg'], { node => $nodename }, sub  {
+           my $res = shift;
+           foreach my $rec (@$res) {
+               printf "$rec->{lv}\n";
+           }
+       }],
+       pbs => [
+           "PVE::API2::Storage::Scan",
+           'pbsscan',
+           ['server', 'username'],
+           { node => $nodename },
+           $print_api_result,
+           $PVE::RESTHandler::standard_output_options,
+       ],
+       zfs => [ "PVE::API2::Storage::Scan", 'zfsscan', [], { node => $nodename }, sub  {
+           my $res = shift;
+
+           foreach my $rec (@$res) {
+                printf "$rec->{pool}\n";
+           }
+       }],
+    },
+    nfsscan => { alias => 'scan nfs' },
+    cifsscan => { alias => 'scan cifs' },
+    glusterfsscan => { alias => 'scan glusterfs' },
+    iscsiscan => { alias => 'scan iscsi' },
+    lvmscan => { alias => 'scan lvm' },
+    lvmthinscan => { alias => 'scan lvmthin' },
+    zfsscan => { alias => 'scan zfs' },
+    path => [ __PACKAGE__, 'path', ['volume']],
+    extractconfig => [__PACKAGE__, 'extractconfig', ['volume']],
+    export => [ __PACKAGE__, 'export', ['volume', 'format', 'filename']],
+    import => [ __PACKAGE__, 'import', ['volume', 'format', 'filename'], {}, sub  {
+       my $volid = shift;
+       print PVE::Storage::volume_imported_message($volid);
+    }],
+    apiinfo => [ __PACKAGE__, 'apiinfo', [], {}, sub {
+       my $res = shift;
+
+       print "APIVER $res->{apiver}\n";
+       print "APIAGE $res->{apiage}\n";
+    }],
+    'prune-backups' => [ __PACKAGE__, 'prunebackups', ['storage'], { node => $nodename }, sub {
+       my $res = shift;
+
+       my ($dryrun, $list) = ($res->{dryrun}, $res->{list});
+
+       return if !$dryrun;
+
+       if (!scalar(@{$list})) {
+           print "No backups found\n";
+           return;
+       }
+
+       print "NOTE: this is only a preview and might not be what a subsequent\n" .
+             "prune call does if backups are removed/added in the meantime.\n\n";
+
+       my @sorted = sort {
+           my $vmcmp = PVE::Tools::safe_compare($a->{vmid}, $b->{vmid}, sub { $_[0] <=> $_[1] });
+           return $vmcmp if $vmcmp ne 0;
+           return $a->{ctime} <=> $b->{ctime};
+       } @{$list};
+
+       my $maxlen = 0;
+       foreach my $backup (@sorted) {
+           my $volid = $backup->{volid};
+           $maxlen = length($volid) if length($volid) > $maxlen;
+       }
+       $maxlen+=1;
+
+       printf("%-${maxlen}s %15s %10s\n", 'Backup', 'Backup-ID', 'Prune-Mark');
+       foreach my $backup (@sorted) {
+           my $type = $backup->{type};
+           my $vmid = $backup->{vmid};
+           my $backup_id = defined($vmid) ? "$type/$vmid" : "$type";
+           printf("%-${maxlen}s %15s %10s\n", $backup->{volid}, $backup_id, $backup->{mark});
+       }
+    }],
+};
+
+1;
diff --git a/src/PVE/CephConfig.pm b/src/PVE/CephConfig.pm
new file mode 100644 (file)
index 0000000..6b10d46
--- /dev/null
@@ -0,0 +1,291 @@
+package PVE::CephConfig;
+
+use strict;
+use warnings;
+use Net::IP;
+use PVE::Tools qw(run_command);
+use PVE::Cluster qw(cfs_register_file);
+
+cfs_register_file('ceph.conf',
+                 \&parse_ceph_config,
+                 \&write_ceph_config);
+
+sub parse_ceph_config {
+    my ($filename, $raw) = @_;
+
+    my $cfg = {};
+    return $cfg if !defined($raw);
+
+    my @lines = split /\n/, $raw;
+
+    my $section;
+
+    foreach my $line (@lines) {
+       $line =~ s/#.*$//;
+       $line =~ s/^\s+//;
+       $line =~ s/^;.*$//;
+       $line =~ s/\s+$//;
+       next if !$line;
+
+       $section = $1 if $line =~ m/^\[(\S+)\]$/;
+       if (!$section) {
+           warn "no section - skip: $line\n";
+           next;
+       }
+
+       if ($line =~ m/^(.*?\S)\s*=\s*(\S.*)$/) {
+           my ($key, $val) = ($1, $2);
+           # ceph treats ' ', '_' and '-' in keys the same, so lets do too
+           $key =~ s/[-\ ]/_/g;
+           $cfg->{$section}->{$key} = $val;
+       }
+
+    }
+
+    return $cfg;
+}
+
+my $parse_ceph_file = sub {
+    my ($filename) = @_;
+
+    my $cfg = {};
+
+    return $cfg if ! -f $filename;
+
+    my $content = PVE::Tools::file_get_contents($filename);
+
+    return parse_ceph_config($filename, $content);
+};
+
+sub write_ceph_config {
+    my ($filename, $cfg) = @_;
+
+    my $out = '';
+
+    my $cond_write_sec = sub {
+       my $re = shift;
+
+       foreach my $section (sort keys %$cfg) {
+           next if $section !~ m/^$re$/;
+           $out .= "[$section]\n";
+           foreach my $key (sort keys %{$cfg->{$section}}) {
+               $out .= "\t $key = $cfg->{$section}->{$key}\n";
+           }
+           $out .= "\n";
+       }
+    };
+
+    &$cond_write_sec('global');
+    &$cond_write_sec('client');
+
+    &$cond_write_sec('mds');
+    &$cond_write_sec('mon');
+    &$cond_write_sec('osd');
+    &$cond_write_sec('mgr');
+
+    &$cond_write_sec('mds\..*');
+    &$cond_write_sec('mon\..*');
+    &$cond_write_sec('osd\..*');
+    &$cond_write_sec('mgr\..*');
+
+    return $out;
+}
+
+my $ceph_get_key = sub {
+    my ($keyfile, $username) = @_;
+
+    my $key = $parse_ceph_file->($keyfile);
+    my $secret = $key->{"client.$username"}->{key};
+
+    return $secret;
+};
+
+my $get_host = sub {
+    my ($hostport) = @_;
+    my ($host, $port) = PVE::Tools::parse_host_and_port($hostport);
+    if (!defined($host)) {
+       return "";
+    }
+    $port = defined($port) ? ":$port" : '';
+    $host = "[$host]" if Net::IP::ip_is_ipv6($host);
+    return "${host}${port}";
+};
+
+sub get_monaddr_list {
+    my ($configfile) = shift;
+
+    if (!defined($configfile)) {
+       warn "No ceph config specified\n";
+       return;
+    }
+
+    my $config = $parse_ceph_file->($configfile);
+
+    my $monhostlist = {};
+
+    # get all ip addresses from mon_host
+    my $monhosts = [ split (/[ ,;]+/, $config->{global}->{mon_host} // "") ];
+
+    foreach my $monhost (@$monhosts) {
+       $monhost =~ s/^\[?v\d\://; # remove beginning of vector
+       $monhost =~ s|/\d+\]?||; # remove end of vector
+       my $host = $get_host->($monhost);
+       if ($host ne "") {
+           $monhostlist->{$host} = 1;
+       }
+    }
+
+    # then get all addrs from mon. sections
+    for my $section ( keys %$config ) {
+       next if $section !~ m/^mon\./;
+
+       if (my $addr = $config->{$section}->{mon_addr}) {
+           $monhostlist->{$addr} = 1;
+       }
+    }
+
+    return join(',', sort keys %$monhostlist);
+}
+
+sub hostlist {
+    my ($list_text, $separator) = @_;
+
+    my @monhostlist = PVE::Tools::split_list($list_text);
+    return join($separator, map { $get_host->($_) } @monhostlist);
+}
+
+my $ceph_check_keyfile = sub {
+    my ($filename, $type) = @_;
+
+    return if ! -f $filename;
+
+    my $content = PVE::Tools::file_get_contents($filename);
+    eval {
+       die if !$content;
+
+       if ($type eq 'rbd') {
+           die if $content !~ /\s*\[\S+\]\s*key\s*=\s*\S+==\s*$/m;
+       } elsif ($type eq 'cephfs') {
+           die if $content !~ /\S+==\s*$/;
+       }
+    };
+    die "Not a proper $type authentication file: $filename\n" if $@;
+
+    return undef;
+};
+
+sub ceph_connect_option {
+    my ($scfg, $storeid, %options) = @_;
+
+    my $cmd_option = {};
+    my $keyfile = "/etc/pve/priv/ceph/${storeid}.keyring";
+    $keyfile = "/etc/pve/priv/ceph/${storeid}.secret" if ($scfg->{type} eq 'cephfs');
+    my $pveceph_managed = !defined($scfg->{monhost});
+
+    $cmd_option->{ceph_conf} = '/etc/pve/ceph.conf' if $pveceph_managed;
+
+    $ceph_check_keyfile->($keyfile, $scfg->{type});
+
+    if (-e "/etc/pve/priv/ceph/${storeid}.conf") {
+       # allow custom ceph configuration for external clusters
+       if ($pveceph_managed) {
+           warn "ignoring custom ceph config for storage '$storeid', 'monhost' is not set (assuming pveceph managed cluster)!\n";
+       } else {
+           $cmd_option->{ceph_conf} = "/etc/pve/priv/ceph/${storeid}.conf";
+       }
+    }
+
+    $cmd_option->{keyring} = $keyfile if (-e $keyfile);
+    $cmd_option->{auth_supported} = (defined $cmd_option->{keyring}) ? 'cephx' : 'none';
+    $cmd_option->{userid} =  $scfg->{username} ? $scfg->{username} : 'admin';
+    $cmd_option->{mon_host} = hostlist($scfg->{monhost}, ',') if (defined($scfg->{monhost}));
+
+    if (%options) {
+       foreach my $k (keys %options) {
+           $cmd_option->{$k} = $options{$k};
+       }
+    }
+
+    return $cmd_option;
+
+}
+
+sub ceph_create_keyfile {
+    my ($type, $storeid, $secret) = @_;
+
+    my $extension = 'keyring';
+    $extension = 'secret' if ($type eq 'cephfs');
+
+    my $ceph_admin_keyring = '/etc/pve/priv/ceph.client.admin.keyring';
+    my $ceph_storage_keyring = "/etc/pve/priv/ceph/${storeid}.$extension";
+
+    die "ceph authx keyring file for storage '$storeid' already exists!\n"
+       if -e $ceph_storage_keyring && !defined($secret);
+
+    if (-e $ceph_admin_keyring || defined($secret)) {
+       eval {
+           if (defined($secret)) {
+               mkdir '/etc/pve/priv/ceph';
+               chomp $secret;
+               PVE::Tools::file_set_contents($ceph_storage_keyring, "${secret}\n", 0400);
+           } elsif ($type eq 'rbd') {
+               mkdir '/etc/pve/priv/ceph';
+               PVE::Tools::file_copy($ceph_admin_keyring, $ceph_storage_keyring);
+           } elsif ($type eq 'cephfs') {
+               my $cephfs_secret = $ceph_get_key->($ceph_admin_keyring, 'admin');
+               mkdir '/etc/pve/priv/ceph';
+               chomp $cephfs_secret;
+               PVE::Tools::file_set_contents($ceph_storage_keyring, "${cephfs_secret}\n", 0400);
+          }
+       };
+       if (my $err = $@) {
+          unlink $ceph_storage_keyring;
+          die "failed to copy ceph authx $extension for storage '$storeid': $err\n";
+       }
+    } else {
+       warn "$ceph_admin_keyring not found, authentication is disabled.\n";
+    }
+}
+
+sub ceph_remove_keyfile {
+    my ($type, $storeid) = @_;
+
+    my $extension = 'keyring';
+    $extension = 'secret' if ($type eq 'cephfs');
+    my $ceph_storage_keyring = "/etc/pve/priv/ceph/${storeid}.$extension";
+
+    if (-f $ceph_storage_keyring) {
+       unlink($ceph_storage_keyring) or warn "removing keyring of storage failed: $!\n";
+    }
+}
+
+my $ceph_version_parser = sub {
+    my $ceph_version = shift;
+    # FIXME this is the same as pve-manager PVE::Ceph::Tools get_local_version
+    if ($ceph_version =~ /^ceph.*\sv?(\d+(?:\.\d+)+(?:-pve\d+)?)\s+(?:\(([a-zA-Z0-9]+)\))?/) {
+       my ($version, $buildcommit) = ($1, $2);
+       my $subversions = [ split(/\.|-/, $version) ];
+
+       return ($subversions, $version, $buildcommit);
+    }
+    warn "Could not parse Ceph version: '$ceph_version'\n";
+};
+
+sub local_ceph_version {
+    my ($cache) = @_;
+
+    my $version_string = $cache;
+    if (!defined($version_string)) {
+       run_command('ceph --version', outfunc => sub {
+           $version_string = shift;
+       });
+    }
+    return undef if !defined($version_string);
+    # subversion is an array ref. with the version parts from major to minor
+    # version is the filtered version string
+    my ($subversions, $version) = $ceph_version_parser->($version_string);
+
+    return wantarray ? ($subversions, $version) : $version;
+}
+
+1;
diff --git a/src/PVE/Diskmanage.pm b/src/PVE/Diskmanage.pm
new file mode 100644 (file)
index 0000000..a311ffd
--- /dev/null
@@ -0,0 +1,913 @@
+package PVE::Diskmanage;
+
+use strict;
+use warnings;
+
+use PVE::ProcFSTools;
+use Data::Dumper;
+use Cwd qw(abs_path);
+use Fcntl ':mode';
+use File::Basename;
+use File::stat;
+use JSON;
+
+use PVE::Tools qw(extract_param run_command file_get_contents file_read_firstline dir_glob_regex dir_glob_foreach trim);
+
+my $SMARTCTL = "/usr/sbin/smartctl";
+my $ZPOOL = "/sbin/zpool";
+my $SGDISK = "/sbin/sgdisk";
+my $PVS = "/sbin/pvs";
+my $LVS = "/sbin/lvs";
+my $LSBLK = "/bin/lsblk";
+
+my sub strip_dev :prototype($) {
+    my ($devpath) = @_;
+    $devpath =~ s|^/dev/||;
+    return $devpath;
+}
+
+sub check_bin {
+    my ($path) = @_;
+    return -x $path;
+}
+
+sub verify_blockdev_path {
+    my ($rel_path) = @_;
+
+    die "missing path" if !$rel_path;
+    my $path = abs_path($rel_path);
+    die "failed to get absolute path to $rel_path\n" if !$path;
+
+    die "got unusual device path '$path'\n" if $path !~  m|^/dev/(.*)$|;
+
+    $path = "/dev/$1"; # untaint
+
+    assert_blockdev($path);
+
+    return $path;
+}
+
+sub assert_blockdev {
+    my ($dev, $noerr) = @_;
+
+    if ($dev !~ m|^/dev/| || !(-b $dev)) {
+       return if $noerr;
+       die "not a valid block device\n";
+    }
+
+    return 1;
+}
+
+sub init_disk {
+    my ($disk, $uuid) = @_;
+
+    assert_blockdev($disk);
+
+    # we should already have checked these in the api call, but we check again for safety
+    die "$disk is a partition\n" if is_partition($disk);
+    die "disk $disk is already in use\n" if disk_is_used($disk);
+
+    my $id = $uuid || 'R';
+    run_command([$SGDISK, $disk, '-U', $id]);
+    return 1;
+}
+
+sub disk_is_used {
+    my ($disk) = @_;
+
+    my $dev = $disk;
+    $dev =~ s|^/dev/||;
+
+    my $disklist = get_disks($dev, 1, 1);
+
+    die "'$disk' is not a valid local disk\n" if !defined($disklist->{$dev});
+    return 1 if $disklist->{$dev}->{used};
+
+    return 0;
+}
+
+sub get_smart_data {
+    my ($disk, $healthonly) = @_;
+
+    assert_blockdev($disk);
+    my $smartdata = {};
+    my $type;
+
+    my $cmd = [$SMARTCTL, '-H'];
+    push @$cmd, '-A', '-f', 'brief' if !$healthonly;
+    push @$cmd, $disk;
+
+    my $returncode = eval {
+       run_command($cmd, noerr => 1, outfunc => sub {
+           my ($line) = @_;
+
+# ATA SMART attributes, e.g.:
+# ID# ATTRIBUTE_NAME          FLAGS    VALUE WORST THRESH FAIL RAW_VALUE
+#   1 Raw_Read_Error_Rate     POSR-K   100   100   000    -    0
+#
+# SAS and NVME disks, e.g.:
+# Data Units Written:                 5,584,952 [2.85 TB]
+# Accumulated start-stop cycles:  34
+
+           if (defined($type) && $type eq 'ata' && $line =~ m/^([ \d]{2}\d)\s+(\S+)\s+(\S{6})\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(.*)$/) {
+               my $entry = {};
+
+               $entry->{name} = $2 if defined $2;
+               $entry->{flags} = $3 if defined $3;
+               # the +0 makes a number out of the strings
+               # FIXME: 'value' is depreacated by 'normalized'; remove with PVE 7.0
+               $entry->{value} = $4+0 if defined $4;
+               $entry->{normalized} = $4+0 if defined $4;
+               $entry->{worst} = $5+0 if defined $5;
+               # some disks report the default threshold as --- instead of 000
+               if (defined($6) && $6 eq '---') {
+                   $entry->{threshold} = 0;
+               } else {
+                   $entry->{threshold} = $6+0 if defined $6;
+               }
+               $entry->{fail} = $7 if defined $7;
+               $entry->{raw} = $8 if defined $8;
+               $entry->{id} = $1 if defined $1;
+               push @{$smartdata->{attributes}}, $entry;
+           } elsif ($line =~ m/(?:Health Status|self\-assessment test result): (.*)$/ ) {
+               $smartdata->{health} = $1;
+           } elsif ($line =~ m/Vendor Specific SMART Attributes with Thresholds:/) {
+               $type = 'ata';
+               delete $smartdata->{text};
+           } elsif ($line =~ m/=== START OF (READ )?SMART DATA SECTION ===/) {
+               $type = 'text';
+           } elsif (defined($type) && $type eq 'text') {
+               $smartdata->{text} = '' if !defined $smartdata->{text};
+               $smartdata->{text} .= "$line\n";
+               # extract wearout from nvme/sas text, allow for decimal values
+               if ($line =~ m/Percentage Used(?: endurance indicator)?:\s*(\d+(?:\.\d+)?)\%/i) {
+                   $smartdata->{wearout} = 100 - $1;
+               }
+           } elsif ($line =~ m/SMART Disabled/) {
+               $smartdata->{health} = "SMART Disabled";
+           }
+       })
+    };
+    my $err = $@;
+
+    # bit 0 and 1 mark a fatal error, other bits are for disk status -> ignore (see man 8 smartctl)
+    if ((defined($returncode) && ($returncode & 0b00000011)) || $err) {
+       die "Error getting S.M.A.R.T. data: Exit code: $returncode\n";
+    }
+
+    $smartdata->{type} = $type;
+
+    return $smartdata;
+}
+
+sub get_lsblk_info {
+    my $cmd = [$LSBLK, '--json', '-o', 'path,parttype,fstype'];
+    my $output = "";
+    eval { run_command($cmd, outfunc => sub { $output .= "$_[0]\n"; }) };
+    warn "$@\n" if $@;
+    return {} if $output eq '';
+
+    my $parsed = eval { decode_json($output) } // {};
+    warn "$@\n" if $@;
+    my $list = $parsed->{blockdevices} // [];
+
+    return {
+       map {
+           $_->{path} => {
+               parttype => $_->{parttype},
+               fstype => $_->{fstype}
+           }
+       } @{$list}
+    };
+}
+
+my sub get_devices_by_partuuid {
+    my ($lsblk_info, $uuids, $res) = @_;
+
+    $res = {} if !defined($res);
+
+    foreach my $dev (sort keys %{$lsblk_info}) {
+       my $uuid = $lsblk_info->{$dev}->{parttype};
+       next if !defined($uuid) || !defined($uuids->{$uuid});
+       $res->{$dev} = $uuids->{$uuid};
+    }
+
+    return $res;
+}
+
+sub get_zfs_devices {
+    my ($lsblk_info) = @_;
+    my $res = {};
+
+    return {} if !check_bin($ZPOOL);
+
+    # use zpool and parttype uuid, because log and cache do not have zfs type uuid
+    eval {
+       run_command([$ZPOOL, 'list', '-HPLv'], outfunc => sub {
+            my ($line) = @_;
+            if ($line =~ m|^\t([^\t]+)\t|) {
+               $res->{$1} = 1;
+            }
+       });
+    };
+
+    # only warn here, because maybe zfs tools are not installed
+    warn "$@\n" if $@;
+
+    my $uuids = {
+       "6a898cc3-1dd2-11b2-99a6-080020736631" => 1, # apple
+       "516e7cba-6ecf-11d6-8ff8-00022d09712b" => 1, # bsd
+    };
+
+
+    $res = get_devices_by_partuuid($lsblk_info, $uuids, $res);
+
+    return $res;
+}
+
+sub get_lvm_devices {
+    my ($lsblk_info) = @_;
+    my $res = {};
+    eval {
+       run_command([$PVS, '--noheadings', '--readonly', '-o', 'pv_name'], outfunc => sub{
+           my ($line) = @_;
+           $line = trim($line);
+           if ($line =~ m|^/dev/|) {
+               $res->{$line} = 1;
+           }
+       });
+    };
+
+    # if something goes wrong, we do not want to give up, but indicate an error has occurred
+    warn "$@\n" if $@;
+
+    my $uuids = {
+       "e6d6d379-f507-44c2-a23c-238f2a3df928" => 1,
+    };
+
+    $res = get_devices_by_partuuid($lsblk_info, $uuids, $res);
+
+    return $res;
+}
+
+sub get_ceph_journals {
+    my ($lsblk_info) = @_;
+    my $res = {};
+
+    my $uuids = {
+       '45b0969e-9b03-4f30-b4c6-b4b80ceff106' => 1, # journal
+       '30cd0809-c2b2-499c-8879-2d6b78529876' => 2, # db
+       '5ce17fce-4087-4169-b7ff-056cc58473f9' => 3, # wal
+       'cafecafe-9b03-4f30-b4c6-b4b80ceff106' => 4, # block
+    };
+
+    $res = get_devices_by_partuuid($lsblk_info, $uuids, $res);
+
+    return $res;
+}
+
+# reads the lv_tags and matches them with the devices
+sub get_ceph_volume_infos {
+    my $result = {};
+
+    my $cmd = [ $LVS, '-S', 'lv_name=~^osd-', '-o', 'devices,lv_name,lv_tags',
+              '--noheadings', '--readonly', '--separator', ';' ];
+
+    run_command($cmd, outfunc => sub {
+       my $line = shift;
+       $line =~ s/(?:^\s+)|(?:\s+$)//g; # trim whitespaces
+
+       my $fields = [ split(';', $line) ];
+
+       # lvs syntax is /dev/sdX(Y) where Y is the start (which we do not need)
+       my ($dev) = $fields->[0] =~ m|^(/dev/[a-z]+[^(]*)|;
+       if ($fields->[1] =~ m|^osd-([^-]+)-|) {
+           my $type = $1;
+           # $result autovivification is wanted, to not creating empty hashes
+           if (($type eq 'block' || $type eq 'data') && $fields->[2] =~ m/ceph.osd_id=([^,]+)/) {
+               $result->{$dev}->{osdid} = $1;
+               $result->{$dev}->{bluestore} = ($type eq 'block');
+               if ($fields->[2] =~ m/ceph\.encrypted=1/) {
+                   $result->{$dev}->{encrypted} = 1;
+               }
+           } else {
+               # undef++ becomes '1' (see `perldoc perlop`: Auto-increment)
+               $result->{$dev}->{$type}++;
+           }
+       }
+    });
+
+    return $result;
+}
+
+sub get_udev_info {
+    my ($dev) = @_;
+
+    my $info = "";
+    my $data = {};
+    eval {
+       run_command(['udevadm', 'info', '-p', $dev, '--query', 'all'], outfunc => sub {
+           my ($line) = @_;
+           $info .= "$line\n";
+       });
+    };
+    warn $@ if $@;
+    return if !$info;
+
+    return if $info !~ m/^E: DEVTYPE=(disk|partition)$/m;
+    return if $info =~ m/^E: ID_CDROM/m;
+
+    # we use this, because some disks are not simply in /dev e.g. /dev/cciss/c0d0
+    if ($info =~ m/^E: DEVNAME=(\S+)$/m) {
+       $data->{devpath} = $1;
+    }
+    return if !defined($data->{devpath});
+
+    $data->{serial} = 'unknown';
+    $data->{serial} = $1 if $info =~ m/^E: ID_SERIAL_SHORT=(\S+)$/m;
+
+    $data->{gpt} = $info =~ m/^E: ID_PART_TABLE_TYPE=gpt$/m ? 1 : 0;
+
+    $data->{rpm} = -1;
+    $data->{rpm} = $1 if $info =~ m/^E: ID_ATA_ROTATION_RATE_RPM=(\d+)$/m; # detects SSD implicit
+
+    $data->{usb} = 1 if $info =~ m/^E: ID_BUS=usb$/m;
+
+    $data->{model} = $1 if $info =~ m/^E: ID_MODEL=(.+)$/m;
+
+    $data->{wwn} = 'unknown';
+    $data->{wwn} = $1 if $info =~ m/^E: ID_WWN=(.*)$/m;
+
+    if ($info =~ m/^E: DEVLINKS=(.+)$/m) {
+       my @devlinks = grep(m#^/dev/disk/by-id/(ata|scsi|nvme(?!-eui))#, split (/ /, $1));
+       $data->{by_id_link} = $devlinks[0] if defined($devlinks[0]);
+    }
+
+    return $data;
+}
+
+sub get_sysdir_size {
+    my ($sysdir) = @_;
+
+    my $size = file_read_firstline("$sysdir/size");
+    return if !$size;
+
+    # linux always considers sectors to be 512 bytes, independently of real block size
+    return $size * 512;
+}
+
+sub get_sysdir_info {
+    my ($sysdir) = @_;
+
+    return if ! -d "$sysdir/device";
+
+    my $data = {};
+
+    $data->{size} = get_sysdir_size($sysdir) or return;
+
+    # dir/queue/rotational should be 1 for hdd, 0 for ssd
+    $data->{rotational} = file_read_firstline("$sysdir/queue/rotational") // -1;
+
+    $data->{vendor} = file_read_firstline("$sysdir/device/vendor") || 'unknown';
+    $data->{model} = file_read_firstline("$sysdir/device/model") || 'unknown';
+
+    return $data;
+}
+
+sub get_wear_leveling_info {
+    my ($smartdata) = @_;
+    my $attributes = $smartdata->{attributes};
+
+    if (defined($smartdata->{wearout})) {
+       return $smartdata->{wearout};
+    }
+
+    my $wearout;
+
+    # Common register names that represent percentage values of potential failure indicators used
+    # in drivedb.h of smartmontool's. Order matters, as some drives may have multiple definitions
+    my @wearoutregisters = (
+       "Media_Wearout_Indicator",
+       "SSD_Life_Left",
+       "Wear_Leveling_Count",
+       "Perc_Write\/Erase_Ct_BC",
+       "Perc_Rated_Life_Remain",
+       "Remaining_Lifetime_Perc",
+       "Percent_Lifetime_Remain",
+       "Lifetime_Left",
+       "PCT_Life_Remaining",
+       "Lifetime_Remaining",
+       "Percent_Life_Remaining",
+       "Percent_Lifetime_Used",
+       "Perc_Rated_Life_Used"
+    );
+
+    # Search for S.M.A.R.T. attributes for known register
+    foreach my $register (@wearoutregisters) {
+       last if defined $wearout;
+       foreach my $attr (@$attributes) {
+          next if $attr->{name} !~ m/$register/;
+          $wearout = $attr->{value};
+          last;
+       }
+    }
+
+    return $wearout;
+}
+
+sub dir_is_empty {
+    my ($dir) = @_;
+
+    my $dh = IO::Dir->new ($dir);
+    return 1 if !$dh;
+
+    while (defined(my $tmp = $dh->read)) {
+       next if $tmp eq '.' || $tmp eq '..';
+       $dh->close;
+       return 0;
+    }
+    $dh->close;
+    return 1;
+}
+
+sub is_iscsi {
+    my ($sysdir) = @_;
+
+    if (-l $sysdir && readlink($sysdir) =~ m|host[^/]*/session[^/]*|) {
+       return 1;
+    }
+
+    return 0;
+}
+
+my sub is_ssdlike {
+    my ($type) = @_;
+    return $type eq 'ssd' || $type eq 'nvme';
+}
+
+sub mounted_blockdevs {
+    my $mounted = {};
+
+    my $mounts = PVE::ProcFSTools::parse_proc_mounts();
+
+    foreach my $mount (@$mounts) {
+       next if $mount->[0] !~ m|^/dev/|;
+       $mounted->{abs_path($mount->[0])} = $mount->[1];
+    };
+
+    return $mounted;
+}
+
+# returns hashmap of abs mount path -> first part of /proc/mounts (what)
+sub mounted_paths {
+    my $mounted = {};
+
+    my $mounts = PVE::ProcFSTools::parse_proc_mounts();
+
+    foreach my $mount (@$mounts) {
+       $mounted->{abs_path($mount->[1])} = $mount->[0];
+    };
+
+    return $mounted;
+}
+
+sub get_disks {
+    my ($disks, $nosmart, $include_partitions) = @_;
+    my $disklist = {};
+
+    my $mounted = mounted_blockdevs();
+
+    my $lsblk_info = get_lsblk_info();
+
+    my $journalhash = get_ceph_journals($lsblk_info);
+    my $ceph_volume_infos = get_ceph_volume_infos();
+
+    my $zfshash = get_zfs_devices($lsblk_info);
+
+    my $lvmhash = get_lvm_devices($lsblk_info);
+
+    my $disk_regex = ".*";
+    if (defined($disks)) {
+       if (!ref($disks)) {
+           $disks = [ $disks ];
+       } elsif (ref($disks) ne 'ARRAY') {
+           die "disks is not a string or array reference\n";
+       }
+       # we get cciss/c0d0 but need cciss!c0d0
+       $_ =~ s|cciss/|cciss!| for @$disks;
+
+       if ($include_partitions) {
+           # Proper blockdevice is needed for the regex, use parent for partitions.
+           for my $disk ($disks->@*) {
+               next if !is_partition("/dev/$disk");
+               $disk = strip_dev(get_blockdev("/dev/$disk"));
+           }
+       }
+
+       $disk_regex = "(?:" . join('|', @$disks) . ")";
+    }
+
+    dir_glob_foreach('/sys/block', $disk_regex, sub {
+       my ($dev) = @_;
+       # whitelisting following devices
+       # - hdX         ide block device
+       # - sdX         scsi/sata block device
+       # - vdX         virtIO block device
+       # - xvdX:       xen virtual block device
+       # - nvmeXnY:    nvme devices
+       # - cciss!cXnY  cciss devices
+       return if $dev !~ m/^(h|s|x?v)d[a-z]+$/ &&
+                 $dev !~ m/^nvme\d+n\d+$/ &&
+                 $dev !~ m/^cciss\!c\d+d\d+$/;
+
+       my $data = get_udev_info("/sys/block/$dev") // return;
+       my $devpath = $data->{devpath};
+
+       my $sysdir = "/sys/block/$dev";
+
+       # we do not want iscsi devices
+       return if is_iscsi($sysdir);
+
+       my $sysdata = get_sysdir_info($sysdir);
+       return if !defined($sysdata);
+
+       my $type = 'unknown';
+
+       if ($sysdata->{rotational} == 0) {
+           $type = 'ssd';
+           $type = 'nvme' if $dev =~ m/^nvme\d+n\d+$/;
+           $data->{rpm} = 0;
+       } elsif ($sysdata->{rotational} == 1) {
+           if ($data->{rpm} != -1) {
+               $type = 'hdd';
+           } elsif ($data->{usb}) {
+               $type = 'usb';
+               $data->{rpm} = 0;
+           }
+       }
+
+       my ($health, $wearout) = ('UNKNOWN', 'N/A');
+       if (!$nosmart) {
+           eval {
+               my $smartdata = get_smart_data($devpath, !is_ssdlike($type));
+               $health = $smartdata->{health} if $smartdata->{health};
+
+               if (is_ssdlike($type)) { # if we have an ssd we try to get the wearout indicator
+                   my $wear_level = get_wear_leveling_info($smartdata);
+                   $wearout = $wear_level if defined($wear_level);
+               }
+           };
+       }
+
+       # we replaced cciss/ with cciss! above, but in the result we need cciss/ again because the
+       # caller might want to check the result again with the original parameter
+       if ($dev =~ m|^cciss!|) {
+           $dev =~ s|^cciss!|cciss/|;
+       }
+
+       $disklist->{$dev} = {
+           vendor => $sysdata->{vendor},
+           model => $data->{model} || $sysdata->{model},
+           size => $sysdata->{size},
+           serial => $data->{serial},
+           gpt => $data->{gpt},
+           rpm => $data->{rpm},
+           type =>  $type,
+           wwn => $data->{wwn},
+           health => $health,
+           devpath => $devpath,
+           wearout => $wearout,
+       };
+       $disklist->{$dev}->{mounted} = 1 if exists $mounted->{$devpath};
+
+       my $by_id_link = $data->{by_id_link};
+       $disklist->{$dev}->{by_id_link} = $by_id_link if defined($by_id_link);
+
+       my ($osdid, $bluestore, $osdencrypted) = (-1, 0, 0);
+       my ($journal_count, $db_count, $wal_count) = (0, 0, 0);
+
+       my $partpath = $devpath;
+       # remove trailing part to get the partition base path, e.g. /dev/cciss/c0d0 -> /dev/cciss
+       $partpath =~ s/\/[^\/]+$//;
+
+       my $determine_usage = sub {
+           my ($devpath, $sysdir, $is_partition) = @_;
+
+           return 'LVM' if $lvmhash->{$devpath};
+           return 'ZFS' if $zfshash->{$devpath};
+
+           my $info = $lsblk_info->{$devpath} // {};
+
+           if (defined(my $parttype = $info->{parttype})) {
+               return 'BIOS boot'if $parttype eq '21686148-6449-6e6f-744e-656564454649';
+               return 'EFI' if $parttype eq 'c12a7328-f81f-11d2-ba4b-00a0c93ec93b';
+               return 'ZFS reserved' if $parttype eq '6a945a3b-1dd2-11b2-99a6-080020736631';
+           }
+
+           return "$info->{fstype}" if defined($info->{fstype});
+           return 'mounted' if $mounted->{$devpath};
+
+           return if !$is_partition;
+
+           # for devices, this check is done explicitly later
+           return 'Device Mapper' if !dir_is_empty("$sysdir/holders");
+
+           return; # unused partition
+       };
+
+       my $collect_ceph_info = sub {
+           my ($devpath) = @_;
+
+           my $ceph_volume = $ceph_volume_infos->{$devpath} or return;
+           $journal_count += $ceph_volume->{journal} // 0;
+           $db_count += $ceph_volume->{db} // 0;
+           $wal_count += $ceph_volume->{wal} // 0;
+           if (defined($ceph_volume->{osdid})) {
+               $osdid = $ceph_volume->{osdid};
+               $bluestore = 1 if $ceph_volume->{bluestore};
+               $osdencrypted = 1 if $ceph_volume->{encrypted};
+           }
+
+           my $result = { %{$ceph_volume} };
+           $result->{journals} = delete $result->{journal} if $result->{journal};
+           return $result;
+       };
+
+       my $partitions = {};
+       dir_glob_foreach("$sysdir", "$dev.+", sub {
+           my ($part) = @_;
+
+           $partitions->{$part} = $collect_ceph_info->("$partpath/$part");
+           my $lvm_based_osd = defined($partitions->{$part});
+
+           $partitions->{$part}->{devpath} = "$partpath/$part";
+           $partitions->{$part}->{parent} = "$devpath";
+           $partitions->{$part}->{mounted} = 1 if exists $mounted->{"$partpath/$part"};
+           $partitions->{$part}->{gpt} = $data->{gpt};
+           $partitions->{$part}->{type} = 'partition';
+           $partitions->{$part}->{size} = get_sysdir_size("$sysdir/$part") // 0;
+           $partitions->{$part}->{used} = $determine_usage->("$partpath/$part", "$sysdir/$part", 1);
+           $partitions->{$part}->{osdid} //= -1;
+
+           # avoid counting twice (e.g. partition with the LVM for the DB OSD is in $journalhash)
+           return if $lvm_based_osd;
+
+           # Legacy handling for non-LVM based OSDs
+           if (my $mp = $mounted->{"$partpath/$part"}) {
+               if ($mp =~ m|^/var/lib/ceph/osd/ceph-(\d+)$|) {
+                   $osdid = $1;
+                   $partitions->{$part}->{osdid} = $osdid;
+               }
+           }
+
+           if (my $journal_part = $journalhash->{"$partpath/$part"}) {
+               $journal_count++ if $journal_part == 1;
+               $db_count++ if $journal_part == 2;
+               $wal_count++ if $journal_part == 3;
+               $bluestore = 1 if $journal_part == 4;
+
+               $partitions->{$part}->{journals} = 1 if $journal_part == 1;
+               $partitions->{$part}->{db} = 1 if $journal_part == 2;
+               $partitions->{$part}->{wal} = 1 if $journal_part == 3;
+               $partitions->{$part}->{bluestore} = 1 if $journal_part == 4;
+           }
+       });
+
+       my $used = $determine_usage->($devpath, $sysdir, 0);
+       if (!$include_partitions) {
+           foreach my $part (sort keys %{$partitions}) {
+               $used //= $partitions->{$part}->{used};
+           }
+       } else {
+           # fstype might be set even if there are partitions, but showing that is confusing
+           $used = 'partitions' if scalar(keys %{$partitions});
+       }
+       $used //= 'partitions' if scalar(keys %{$partitions});
+       # multipath, software raid, etc.
+       # this check comes in last, to show more specific info
+       # if we have it
+       $used //= 'Device Mapper' if !dir_is_empty("$sysdir/holders");
+
+       $disklist->{$dev}->{used} = $used if $used;
+
+       $collect_ceph_info->($devpath);
+
+       $disklist->{$dev}->{osdid} = $osdid;
+       $disklist->{$dev}->{journals} = $journal_count if $journal_count;
+       $disklist->{$dev}->{bluestore} = $bluestore if $osdid != -1;
+       $disklist->{$dev}->{osdencrypted} = $osdencrypted if $osdid != -1;
+       $disklist->{$dev}->{db} = $db_count if $db_count;
+       $disklist->{$dev}->{wal} = $wal_count if $wal_count;
+
+       if ($include_partitions) {
+           $disklist->{$_} = $partitions->{$_} for keys %{$partitions};
+       }
+    });
+
+    return $disklist;
+}
+
+sub get_partnum {
+    my ($part_path) = @_;
+
+    my $st = stat($part_path);
+
+    die "error detecting block device '$part_path'\n"
+       if !$st || !$st->mode || !S_ISBLK($st->mode) || !$st->rdev;
+
+    my $major = PVE::Tools::dev_t_major($st->rdev);
+    my $minor = PVE::Tools::dev_t_minor($st->rdev);
+    my $partnum_path = "/sys/dev/block/$major:$minor/";
+
+    my $partnum = file_read_firstline("${partnum_path}partition");
+    die "Partition does not exist\n" if !defined($partnum);
+    die "Failed to get partition number\n" if $partnum !~ m/(\d+)/; # untaint
+    $partnum = $1;
+    die "Partition number $partnum is invalid\n" if $partnum > 128;
+
+    return $partnum;
+}
+
+sub get_blockdev {
+    my ($part_path) = @_;
+
+    my ($dev, $block_dev);
+    if ($part_path =~ m|^/dev/(.*)$|) {
+       $dev = $1;
+       my $link = readlink "/sys/class/block/$dev";
+       $block_dev = $1 if $link =~ m|([^/]*)/$dev$|;
+    }
+
+    die "Can't parse parent device\n" if !defined($block_dev);
+    die "No valid block device\n" if index($dev, $block_dev) == -1;
+
+    $block_dev = "/dev/$block_dev";
+    die "Block device does not exists\n" if !(-b $block_dev);
+
+    return $block_dev;
+}
+
+sub is_partition {
+    my ($dev_path) = @_;
+
+    return defined(eval { get_partnum($dev_path) });
+}
+
+sub locked_disk_action {
+    my ($sub) = @_;
+    my $res = PVE::Tools::lock_file('/run/lock/pve-diskmanage.lck', undef, $sub);
+    die $@ if $@;
+    return $res;
+}
+
+sub assert_disk_unused {
+    my ($dev) = @_;
+    die "device '$dev' is already in use\n" if disk_is_used($dev);
+    return;
+}
+
+sub append_partition {
+    my ($dev, $size) = @_;
+
+    my $devname = $dev;
+    $devname =~ s|^/dev/||;
+
+    my $newpartid = 1;
+    dir_glob_foreach("/sys/block/$devname", qr/\Q$devname\E.*?(\d+)/, sub {
+       my ($part, $partid) = @_;
+
+       if ($partid >= $newpartid) {
+           $newpartid = $partid + 1;
+       }
+    });
+
+    $size = PVE::Tools::convert_size($size, 'b' => 'mb');
+
+    run_command([ $SGDISK, '-n', "$newpartid:0:+${size}M", $dev ],
+               errmsg => "error creating partition '$newpartid' on '$dev'");
+
+    my $partition;
+
+    # loop again to detect the real partition device which does not always follow
+    # a strict $devname$partition scheme like /dev/nvme0n1 -> /dev/nvme0n1p1
+    dir_glob_foreach("/sys/block/$devname", qr/\Q$devname\E.*$newpartid/, sub {
+       my ($part) = @_;
+
+       $partition = "/dev/$part";
+    });
+
+    return $partition;
+}
+
+# Check if a disk or any of its partitions has a holder.
+# Can also be called with a partition.
+# Expected to be called with a result of verify_blockdev_path().
+sub has_holder {
+    my ($devpath) = @_;
+
+    my $dev = strip_dev($devpath);
+
+    return $devpath if !dir_is_empty("/sys/class/block/${dev}/holders");
+
+    my $found;
+    dir_glob_foreach("/sys/block/${dev}", "${dev}.+", sub {
+       my ($part) = @_;
+       $found = "/dev/${part}" if !dir_is_empty("/sys/class/block/${part}/holders");
+    });
+
+    return $found;
+}
+
+# Basic check if a disk or any of its partitions is mounted.
+# Can also be called with a partition.
+# Expected to be called with a result of verify_blockdev_path().
+sub is_mounted {
+    my ($devpath) = @_;
+
+    my $mounted = mounted_blockdevs();
+
+    return $devpath if $mounted->{$devpath};
+
+    my $dev = strip_dev($devpath);
+
+    my $found;
+    dir_glob_foreach("/sys/block/${dev}", "${dev}.+", sub {
+       my ($part) = @_;
+       my $partpath = "/dev/${part}";
+
+       $found = $partpath if $mounted->{$partpath};
+    });
+
+    return $found;
+}
+
+# Currently only supports GPT-partitioned disks.
+sub change_parttype {
+    my ($partpath, $parttype) = @_;
+
+    my $err = "unable to change partition type for $partpath";
+
+    my $partnum = get_partnum($partpath);
+    my $blockdev = get_blockdev($partpath);
+    my $dev = strip_dev($blockdev);
+
+    my $info = get_disks($dev, 1);
+    die "$err - unable to get disk info for '$blockdev'\n" if !defined($info->{$dev});
+    die "$err - disk '$blockdev' is not GPT partitioned\n" if !$info->{$dev}->{gpt};
+
+    run_command(['sgdisk', "-t${partnum}:${parttype}", $blockdev], errmsg => $err);
+}
+
+# Wipes all labels and the first 200 MiB of a disk/partition (or the whole if it is smaller).
+# If called with a partition, also sets the partition type to 0x83 'Linux filesystem'.
+# Expected to be called with a result of verify_blockdev_path().
+sub wipe_blockdev {
+    my ($devpath) = @_;
+
+    my $devname = basename($devpath);
+    my $dev_size = PVE::Tools::file_get_contents("/sys/class/block/$devname/size");
+
+    ($dev_size) = $dev_size =~ m|(\d+)|; # untaint $dev_size
+    die "Couldn't get the size of the device $devname\n" if !defined($dev_size);
+
+    my $size = ($dev_size * 512 / 1024 / 1024);
+    my $count = ($size < 200) ? $size : 200;
+
+    my $to_wipe = [];
+    dir_glob_foreach("/sys/class/block/${devname}", "${devname}.+", sub {
+       my ($part) = @_;
+       push $to_wipe->@*, "/dev/${part}" if -b "/dev/${part}";
+    });
+
+    if (scalar($to_wipe->@*) > 0) {
+       print "found child partitions to wipe: ". join(', ', $to_wipe->@*) ."\n";
+    }
+    push $to_wipe->@*, $devpath; # put actual device last
+
+    print "wiping block device ${devpath}\n";
+
+    run_command(['wipefs', '--all', $to_wipe->@*], errmsg => "error wiping '${devpath}'");
+
+    run_command(
+       ['dd', 'if=/dev/zero', "of=${devpath}", 'bs=1M', 'conv=fdatasync', "count=${count}"],
+       errmsg => "error wiping '${devpath}'",
+    );
+
+    if (is_partition($devpath)) {
+       eval { change_parttype($devpath, '8300'); };
+       warn $@ if $@;
+    }
+}
+
+# FIXME: Remove once we depend on systemd >= v249.
+# Work around udev bug https://github.com/systemd/systemd/issues/18525 ensuring database is updated.
+sub udevadm_trigger {
+    my @devs = @_;
+
+    return if scalar(@devs) == 0;
+
+    eval { run_command(['udevadm', 'trigger', @devs]); };
+    warn $@ if $@;
+}
+
+1;
diff --git a/src/PVE/Makefile b/src/PVE/Makefile
new file mode 100644 (file)
index 0000000..5fe4a0a
--- /dev/null
@@ -0,0 +1,12 @@
+
+
+.PHONY: install
+install:
+       install -D -m 0644 Storage.pm ${DESTDIR}${PERLDIR}/PVE/Storage.pm
+       install -D -m 0644 Diskmanage.pm ${DESTDIR}${PERLDIR}/PVE/Diskmanage.pm
+       install -D -m 0644 CephConfig.pm ${DESTDIR}${PERLDIR}/PVE/CephConfig.pm
+       make -C Storage install
+       make -C API2 install
+       make -C CLI install
+
+clean:
diff --git a/src/PVE/Storage.pm b/src/PVE/Storage.pm
new file mode 100755 (executable)
index 0000000..cec3996
--- /dev/null
@@ -0,0 +1,2151 @@
+package PVE::Storage;
+
+use strict;
+use warnings;
+use Data::Dumper;
+
+use POSIX;
+use IO::Select;
+use IO::File;
+use IO::Socket::IP;
+use IPC::Open3;
+use File::Basename;
+use File::Path;
+use Cwd 'abs_path';
+use Socket;
+use Time::Local qw(timelocal);
+
+use PVE::Tools qw(run_command file_read_firstline dir_glob_foreach $IPV6RE);
+use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_lock_file);
+use PVE::DataCenterConfig;
+use PVE::Exception qw(raise_param_exc raise);
+use PVE::JSONSchema;
+use PVE::INotify;
+use PVE::RPCEnvironment;
+use PVE::SSHInfo;
+use PVE::RESTEnvironment qw(log_warn);
+
+use PVE::Storage::Plugin;
+use PVE::Storage::DirPlugin;
+use PVE::Storage::LVMPlugin;
+use PVE::Storage::LvmThinPlugin;
+use PVE::Storage::NFSPlugin;
+use PVE::Storage::CIFSPlugin;
+use PVE::Storage::ISCSIPlugin;
+use PVE::Storage::RBDPlugin;
+use PVE::Storage::CephFSPlugin;
+use PVE::Storage::ISCSIDirectPlugin;
+use PVE::Storage::GlusterfsPlugin;
+use PVE::Storage::ZFSPoolPlugin;
+use PVE::Storage::ZFSPlugin;
+use PVE::Storage::PBSPlugin;
+use PVE::Storage::BTRFSPlugin;
+
+# Storage API version. Increment it on changes in storage API interface.
+use constant APIVER => 10;
+# Age is the number of versions we're backward compatible with.
+# This is like having 'current=APIVER' and age='APIAGE' in libtool,
+# see https://www.gnu.org/software/libtool/manual/html_node/Libtool-versioning.html
+use constant APIAGE => 1;
+
+our $KNOWN_EXPORT_FORMATS = ['raw+size', 'tar+size', 'qcow2+size', 'vmdk+size', 'zfs', 'btrfs'];
+
+# load standard plugins
+PVE::Storage::DirPlugin->register();
+PVE::Storage::LVMPlugin->register();
+PVE::Storage::LvmThinPlugin->register();
+PVE::Storage::NFSPlugin->register();
+PVE::Storage::CIFSPlugin->register();
+PVE::Storage::ISCSIPlugin->register();
+PVE::Storage::RBDPlugin->register();
+PVE::Storage::CephFSPlugin->register();
+PVE::Storage::ISCSIDirectPlugin->register();
+PVE::Storage::GlusterfsPlugin->register();
+PVE::Storage::ZFSPoolPlugin->register();
+PVE::Storage::ZFSPlugin->register();
+PVE::Storage::PBSPlugin->register();
+PVE::Storage::BTRFSPlugin->register();
+
+# load third-party plugins
+if ( -d '/usr/share/perl5/PVE/Storage/Custom' ) {
+    dir_glob_foreach('/usr/share/perl5/PVE/Storage/Custom', '.*\.pm$', sub {
+       my ($file) = @_;
+       my $modname = 'PVE::Storage::Custom::' . $file;
+       $modname =~ s!\.pm$!!;
+       $file = 'PVE/Storage/Custom/' . $file;
+
+       eval {
+           require $file;
+
+           # Check perl interface:
+           die "not derived from PVE::Storage::Plugin\n" if !$modname->isa('PVE::Storage::Plugin');
+           die "does not provide an api() method\n" if !$modname->can('api');
+           # Check storage API version and that file is really storage plugin.
+           my $version = $modname->api();
+           die "implements an API version newer than current ($version > " . APIVER . ")\n"
+               if $version > APIVER;
+           my $min_version = (APIVER - APIAGE);
+           die "API version too old, please update the plugin ($version < $min_version)\n"
+               if $version < $min_version;
+           # all OK, do import and register (i.e., "use")
+           import $file;
+           $modname->register();
+
+           # If we got this far and the API version is not the same, make some noise:
+           warn "Plugin \"$modname\" is implementing an older storage API, an upgrade is recommended\n"
+               if $version != APIVER;
+       };
+       if ($@) {
+           warn "Error loading storage plugin \"$modname\": $@";
+       }
+    });
+}
+
+# initialize all plugins
+PVE::Storage::Plugin->init();
+
+# the following REs indicate the number or capture groups via the trailing digit
+# CAUTION don't forget to update the digits accordingly after messing with the capture groups
+
+our $ISO_EXT_RE_0 = qr/\.(?:iso|img)/i;
+
+our $VZTMPL_EXT_RE_1 = qr/\.tar\.(gz|xz|zst)/i;
+
+our $BACKUP_EXT_RE_2 = qr/\.(tgz|(?:tar|vma)(?:\.(${\PVE::Storage::Plugin::COMPRESSOR_RE}))?)/;
+
+# FIXME remove with PVE 8.0, add versioned breaks for pve-manager
+our $vztmpl_extension_re = $VZTMPL_EXT_RE_1;
+
+#  PVE::Storage utility functions
+
+sub config {
+    return cfs_read_file("storage.cfg");
+}
+
+sub write_config {
+    my ($cfg) = @_;
+
+    cfs_write_file('storage.cfg', $cfg);
+}
+
+sub lock_storage_config {
+    my ($code, $errmsg) = @_;
+
+    cfs_lock_file("storage.cfg", undef, $code);
+    my $err = $@;
+    if ($err) {
+       $errmsg ? die "$errmsg: $err" : die $err;
+    }
+}
+
+# FIXME remove maxfiles for PVE 8.0 or PVE 9.0
+my $convert_maxfiles_to_prune_backups = sub {
+    my ($scfg) = @_;
+
+    return if !$scfg;
+
+    my $maxfiles = delete $scfg->{maxfiles};
+
+    if (!defined($scfg->{'prune-backups'}) && defined($maxfiles)) {
+       my $prune_backups;
+       if ($maxfiles) {
+           $prune_backups = { 'keep-last' => $maxfiles };
+       } else { # maxfiles 0 means no limit
+           $prune_backups = { 'keep-all' => 1 };
+       }
+       $scfg->{'prune-backups'} = PVE::JSONSchema::print_property_string(
+           $prune_backups,
+           'prune-backups'
+       );
+    }
+};
+
+sub storage_config {
+    my ($cfg, $storeid, $noerr) = @_;
+
+    die "no storage ID specified\n" if !$storeid;
+
+    my $scfg = $cfg->{ids}->{$storeid};
+
+    die "storage '$storeid' does not exist\n" if (!$noerr && !$scfg);
+
+    $convert_maxfiles_to_prune_backups->($scfg);
+
+    return $scfg;
+}
+
+sub storage_check_node {
+    my ($cfg, $storeid, $node, $noerr) = @_;
+
+    my $scfg = storage_config($cfg, $storeid);
+
+    if ($scfg->{nodes}) {
+       $node = PVE::INotify::nodename() if !$node || ($node eq 'localhost');
+       if (!$scfg->{nodes}->{$node}) {
+           die "storage '$storeid' is not available on node '$node'\n" if !$noerr;
+           return undef;
+       }
+    }
+
+    return $scfg;
+}
+
+sub storage_check_enabled {
+    my ($cfg, $storeid, $node, $noerr) = @_;
+
+    my $scfg = storage_config($cfg, $storeid);
+
+    if ($scfg->{disable}) {
+       die "storage '$storeid' is disabled\n" if !$noerr;
+       return undef;
+    }
+
+    return storage_check_node($cfg, $storeid, $node, $noerr);
+}
+
+# storage_can_replicate:
+# return true if storage supports replication
+# (volumes allocated with vdisk_alloc() has replication feature)
+sub storage_can_replicate {
+    my ($cfg, $storeid, $format) = @_;
+
+    my $scfg = storage_config($cfg, $storeid);
+    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+    return $plugin->storage_can_replicate($scfg, $storeid, $format);
+}
+
+sub get_max_protected_backups {
+    my ($scfg, $storeid) = @_;
+
+    return $scfg->{'max-protected-backups'} if defined($scfg->{'max-protected-backups'});
+
+    my $rpcenv = PVE::RPCEnvironment::get();
+    my $authuser = $rpcenv->get_user();
+
+    return $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.Allocate'], 1) ? -1 : 5;
+}
+
+sub storage_ids {
+    my ($cfg) = @_;
+
+    return keys %{$cfg->{ids}};
+}
+
+sub file_size_info {
+    my ($filename, $timeout) = @_;
+
+    return PVE::Storage::Plugin::file_size_info($filename, $timeout);
+}
+
+sub get_volume_attribute {
+    my ($cfg, $volid, $attribute) = @_;
+
+    my ($storeid, $volname) = parse_volume_id($volid);
+    my $scfg = storage_config($cfg, $storeid);
+    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+
+    return $plugin->get_volume_attribute($scfg, $storeid, $volname, $attribute);
+}
+
+sub update_volume_attribute {
+    my ($cfg, $volid, $attribute, $value) = @_;
+
+    my ($storeid, $volname) = parse_volume_id($volid);
+    my $scfg = storage_config($cfg, $storeid);
+    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+
+    my ($vtype, undef, $vmid) = $plugin->parse_volname($volname);
+    my $max_protected_backups = get_max_protected_backups($scfg, $storeid);
+
+    if (
+       $vtype eq 'backup'
+       && $vmid
+       && $attribute eq 'protected'
+       && $value
+       && !$plugin->get_volume_attribute($scfg, $storeid, $volname, 'protected')
+       && $max_protected_backups > -1 # -1 is unlimited
+    ) {
+       my $backups = $plugin->list_volumes($storeid, $scfg, $vmid, ['backup']);
+       my ($backup_type) = map { $_->{subtype} } grep { $_->{volid} eq $volid } $backups->@*;
+
+       my $protected_count = grep {
+           $_->{protected} && (!$backup_type || ($_->{subtype} && $_->{subtype} eq $backup_type))
+       } $backups->@*;
+
+       if ($max_protected_backups <= $protected_count) {
+           die "The number of protected backups per guest is limited to $max_protected_backups ".
+               "on storage '$storeid'\n";
+       }
+    }
+
+    return $plugin->update_volume_attribute($scfg, $storeid, $volname, $attribute, $value);
+}
+
+sub volume_size_info {
+    my ($cfg, $volid, $timeout) = @_;
+
+    my ($storeid, $volname) = parse_volume_id($volid, 1);
+    if ($storeid) {
+       my $scfg = storage_config($cfg, $storeid);
+       my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+       return $plugin->volume_size_info($scfg, $storeid, $volname, $timeout);
+    } elsif ($volid =~ m|^(/.+)$| && -e $volid) {
+       return file_size_info($volid, $timeout);
+    } else {
+       return 0;
+    }
+}
+
+sub volume_resize {
+    my ($cfg, $volid, $size, $running) = @_;
+
+    my $padding = (1024 - $size % 1024) % 1024;
+    $size = $size + $padding;
+
+    my ($storeid, $volname) = parse_volume_id($volid, 1);
+    if ($storeid) {
+        my $scfg = storage_config($cfg, $storeid);
+        my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+        return $plugin->volume_resize($scfg, $storeid, $volname, $size, $running);
+    } elsif ($volid =~ m|^(/.+)$| && -e $volid) {
+        die "resize file/device '$volid' is not possible\n";
+    } else {
+       die "unable to parse volume ID '$volid'\n";
+    }
+}
+
+sub volume_rollback_is_possible {
+    my ($cfg, $volid, $snap, $blockers) = @_;
+
+    my ($storeid, $volname) = parse_volume_id($volid, 1);
+    if ($storeid) {
+        my $scfg = storage_config($cfg, $storeid);
+        my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+        return $plugin->volume_rollback_is_possible($scfg, $storeid, $volname, $snap, $blockers);
+    } elsif ($volid =~ m|^(/.+)$| && -e $volid) {
+        die "snapshot rollback file/device '$volid' is not possible\n";
+    } else {
+       die "unable to parse volume ID '$volid'\n";
+    }
+}
+
+sub volume_snapshot {
+    my ($cfg, $volid, $snap) = @_;
+
+    my ($storeid, $volname) = parse_volume_id($volid, 1);
+    if ($storeid) {
+        my $scfg = storage_config($cfg, $storeid);
+        my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+        return $plugin->volume_snapshot($scfg, $storeid, $volname, $snap);
+    } elsif ($volid =~ m|^(/.+)$| && -e $volid) {
+        die "snapshot file/device '$volid' is not possible\n";
+    } else {
+       die "unable to parse volume ID '$volid'\n";
+    }
+}
+
+sub volume_snapshot_rollback {
+    my ($cfg, $volid, $snap) = @_;
+
+    my ($storeid, $volname) = parse_volume_id($volid, 1);
+    if ($storeid) {
+        my $scfg = storage_config($cfg, $storeid);
+        my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+       $plugin->volume_rollback_is_possible($scfg, $storeid, $volname, $snap);
+        return $plugin->volume_snapshot_rollback($scfg, $storeid, $volname, $snap);
+    } elsif ($volid =~ m|^(/.+)$| && -e $volid) {
+        die "snapshot rollback file/device '$volid' is not possible\n";
+    } else {
+       die "unable to parse volume ID '$volid'\n";
+    }
+}
+
+# FIXME PVE 8.x remove $running parameter (needs APIAGE reset)
+sub volume_snapshot_delete {
+    my ($cfg, $volid, $snap, $running) = @_;
+
+    my ($storeid, $volname) = parse_volume_id($volid, 1);
+    if ($storeid) {
+        my $scfg = storage_config($cfg, $storeid);
+        my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+        return $plugin->volume_snapshot_delete($scfg, $storeid, $volname, $snap, $running);
+    } elsif ($volid =~ m|^(/.+)$| && -e $volid) {
+        die "snapshot delete file/device '$volid' is not possible\n";
+    } else {
+       die "unable to parse volume ID '$volid'\n";
+    }
+}
+
+# check if a filesystem on top of a volume needs to flush its journal for
+# consistency (see fsfreeze(8)) before a snapshot is taken - needed for
+# container mountpoints
+sub volume_snapshot_needs_fsfreeze {
+    my ($cfg, $volid) = @_;
+
+    my ($storeid, $volname) = parse_volume_id($volid);
+    my $scfg = storage_config($cfg, $storeid);
+    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+    return $plugin->volume_snapshot_needs_fsfreeze();
+}
+
+# check if a volume or snapshot supports a given feature
+# $feature - one of:
+#            clone - linked clone is possible
+#            copy  - full clone is possible
+#            replicate - replication is possible
+#            snapshot - taking a snapshot is possible
+#            sparseinit - volume is sparsely initialized
+#            template - conversion to base image is possible
+#            rename - renaming volumes is possible
+# $snap - check if the feature is supported for a given snapshot
+# $running - if the guest owning the volume is running
+# $opts - hash with further options:
+#         valid_target_formats - list of formats for the target of a copy/clone
+#                                operation that the caller could work with. The
+#                                format of $volid is always considered valid and if
+#                                no list is specified, all formats are considered valid.
+sub volume_has_feature {
+    my ($cfg, $feature, $volid, $snap, $running, $opts) = @_;
+
+    my ($storeid, $volname) = parse_volume_id($volid, 1);
+    if ($storeid) {
+        my $scfg = storage_config($cfg, $storeid);
+        my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+        return $plugin->volume_has_feature($scfg, $feature, $storeid, $volname, $snap, $running, $opts);
+    } elsif ($volid =~ m|^(/.+)$| && -e $volid) {
+       return undef;
+    } else {
+       return undef;
+    }
+}
+
+sub volume_snapshot_info {
+    my ($cfg, $volid) = @_;
+
+    my ($storeid, $volname) = parse_volume_id($volid);
+    my $scfg = storage_config($cfg, $storeid);
+    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+    return $plugin->volume_snapshot_info($scfg, $storeid, $volname);
+}
+
+sub get_image_dir {
+    my ($cfg, $storeid, $vmid) = @_;
+
+    my $scfg = storage_config($cfg, $storeid);
+    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+
+    my $path = $plugin->get_subdir($scfg, 'images');
+
+    return $vmid ? "$path/$vmid" : $path;
+}
+
+sub get_private_dir {
+    my ($cfg, $storeid, $vmid) = @_;
+
+    my $scfg = storage_config($cfg, $storeid);
+    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+
+    my $path = $plugin->get_subdir($scfg, 'rootdir');
+
+    return $vmid ? "$path/$vmid" : $path;
+}
+
+sub get_iso_dir {
+    my ($cfg, $storeid) = @_;
+
+    my $scfg = storage_config($cfg, $storeid);
+    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+
+    return $plugin->get_subdir($scfg, 'iso');
+}
+
+sub get_vztmpl_dir {
+    my ($cfg, $storeid) = @_;
+
+    my $scfg = storage_config($cfg, $storeid);
+    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+
+    return $plugin->get_subdir($scfg, 'vztmpl');
+}
+
+sub get_backup_dir {
+    my ($cfg, $storeid) = @_;
+
+    my $scfg = storage_config($cfg, $storeid);
+    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+
+    return $plugin->get_subdir($scfg, 'backup');
+}
+
+# library implementation
+
+sub parse_vmid {
+    my $vmid = shift;
+
+    die "VMID '$vmid' contains illegal characters\n" if $vmid !~ m/^\d+$/;
+
+    return int($vmid);
+}
+
+# NOTE: basename and basevmid are always undef for LVM-thin, where the
+# clone -> base reference is not encoded in the volume ID.
+# see note in PVE::Storage::LvmThinPlugin for details.
+sub parse_volname {
+    my ($cfg, $volid) = @_;
+
+    my ($storeid, $volname) = parse_volume_id($volid);
+
+    my $scfg = storage_config($cfg, $storeid);
+
+    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+
+    # returns ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format)
+
+    return $plugin->parse_volname($volname);
+}
+
+sub parse_volume_id {
+    my ($volid, $noerr) = @_;
+
+    return PVE::Storage::Plugin::parse_volume_id($volid, $noerr);
+}
+
+# test if we have read access to volid
+sub check_volume_access {
+    my ($rpcenv, $user, $cfg, $vmid, $volid, $type) = @_;
+
+    my ($sid, $volname) = parse_volume_id($volid, 1);
+    if ($sid) {
+       my ($vtype, undef, $ownervm) = parse_volname($cfg, $volid);
+
+       # Need to allow 'images' when expecting 'rootdir' too - not cleanly separated in plugins.
+       die "unable to use volume $volid - content type needs to be '$type'\n"
+           if defined($type) && $vtype ne $type && ($type ne 'rootdir' || $vtype ne 'images');
+
+       return if $rpcenv->check($user, "/storage/$sid", ['Datastore.Allocate'], 1);
+
+       if ($vtype eq 'iso' || $vtype eq 'vztmpl') {
+           # require at least read access to storage, (custom) templates/ISOs could be sensitive
+           $rpcenv->check_any($user, "/storage/$sid", ['Datastore.AllocateSpace', 'Datastore.Audit']);
+       } elsif (defined($ownervm) && defined($vmid) && ($ownervm == $vmid)) {
+           # we are owner - allow access
+       } elsif ($vtype eq 'backup' && $ownervm) {
+           $rpcenv->check($user, "/storage/$sid", ['Datastore.AllocateSpace']);
+           $rpcenv->check($user, "/vms/$ownervm", ['VM.Backup']);
+       } elsif (($vtype eq 'images' || $vtype eq 'rootdir') && $ownervm) {
+           $rpcenv->check($user, "/storage/$sid", ['Datastore.Audit']);
+           $rpcenv->check($user, "/vms/$ownervm", ['VM.Config.Disk']);
+       } else {
+           die "missing privileges to access $volid\n";
+       }
+    } else {
+       die "Only root can pass arbitrary filesystem paths."
+           if $user ne 'root@pam';
+    }
+
+    return undef;
+}
+
+# NOTE: this check does not work for LVM-thin, where the clone -> base
+# reference is not encoded in the volume ID.
+# see note in PVE::Storage::LvmThinPlugin for details.
+sub volume_is_base_and_used {
+    my ($cfg, $volid) = @_;
+
+    my ($storeid, $volname) = parse_volume_id($volid);
+    my $scfg = storage_config($cfg, $storeid);
+    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+
+    my ($vtype, $name, $vmid, undef, undef, $isBase, undef) =
+       $plugin->parse_volname($volname);
+
+    if ($isBase) {
+       my $vollist = $plugin->list_images($storeid, $scfg);
+       foreach my $info (@$vollist) {
+           my (undef, $tmpvolname) = parse_volume_id($info->{volid});
+           my $basename = undef;
+           my $basevmid = undef;
+
+           eval{
+               (undef, undef, undef, $basename, $basevmid) =
+                   $plugin->parse_volname($tmpvolname);
+           };
+
+           if ($basename && defined($basevmid) && $basevmid == $vmid && $basename eq $name) {
+               return 1;
+           }
+       }
+    }
+    return 0;
+}
+
+# try to map a filesystem path to a volume identifier
+sub path_to_volume_id {
+    my ($cfg, $path) = @_;
+
+    my $ids = $cfg->{ids};
+
+    my ($sid, $volname) = parse_volume_id($path, 1);
+    if ($sid) {
+       if (my $scfg = $ids->{$sid}) {
+           if ($scfg->{path}) {
+               my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+               my ($vtype, $name, $vmid) = $plugin->parse_volname($volname);
+               return ($vtype, $path);
+           }
+       }
+       return ('');
+    }
+
+    # Note: abs_path() return undef if $path doesn not exist
+    # for example when nfs storage is not mounted
+    $path = abs_path($path) || $path;
+
+    foreach my $sid (keys %$ids) {
+       my $scfg = $ids->{$sid};
+       next if !$scfg->{path};
+       my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+       my $imagedir = $plugin->get_subdir($scfg, 'images');
+       my $isodir = $plugin->get_subdir($scfg, 'iso');
+       my $tmpldir = $plugin->get_subdir($scfg, 'vztmpl');
+       my $backupdir = $plugin->get_subdir($scfg, 'backup');
+       my $privatedir = $plugin->get_subdir($scfg, 'rootdir');
+       my $snippetsdir = $plugin->get_subdir($scfg, 'snippets');
+
+       if ($path =~ m!^$imagedir/(\d+)/([^/\s]+)$!) {
+           my $vmid = $1;
+           my $name = $2;
+
+           my $vollist = $plugin->list_images($sid, $scfg, $vmid);
+           foreach my $info (@$vollist) {
+               my ($storeid, $volname) = parse_volume_id($info->{volid});
+               my $volpath = $plugin->path($scfg, $volname, $storeid);
+               if ($volpath eq $path) {
+                   return ('images', $info->{volid});
+               }
+           }
+       } elsif ($path =~ m!^$isodir/([^/]+$ISO_EXT_RE_0)$!) {
+           my $name = $1;
+           return ('iso', "$sid:iso/$name");
+       } elsif ($path =~ m!^$tmpldir/([^/]+$VZTMPL_EXT_RE_1)$!) {
+           my $name = $1;
+           return ('vztmpl', "$sid:vztmpl/$name");
+       } elsif ($path =~ m!^$privatedir/(\d+)$!) {
+           my $vmid = $1;
+           return ('rootdir', "$sid:rootdir/$vmid");
+       } elsif ($path =~ m!^$backupdir/([^/]+$BACKUP_EXT_RE_2)$!) {
+           my $name = $1;
+           return ('backup', "$sid:backup/$name");
+       } elsif ($path =~ m!^$snippetsdir/([^/]+)$!) {
+           my $name = $1;
+           return ('snippets', "$sid:snippets/$name");
+       }
+    }
+
+    # can't map path to volume id
+    return ('');
+}
+
+sub path {
+    my ($cfg, $volid, $snapname) = @_;
+
+    my ($storeid, $volname) = parse_volume_id($volid);
+
+    my $scfg = storage_config($cfg, $storeid);
+
+    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+    my ($path, $owner, $vtype) = $plugin->path($scfg, $volname, $storeid, $snapname);
+    return wantarray ? ($path, $owner, $vtype) : $path;
+}
+
+sub abs_filesystem_path {
+    my ($cfg, $volid, $allow_blockdev) = @_;
+
+    my $path;
+    if (parse_volume_id ($volid, 1)) {
+       activate_volumes($cfg, [ $volid ]);
+       $path = PVE::Storage::path($cfg, $volid);
+    } else {
+       if (-f $volid || ($allow_blockdev && -b $volid)) {
+           my $abspath = abs_path($volid);
+           if ($abspath && $abspath =~ m|^(/.+)$|) {
+               $path = $1; # untaint any path
+           }
+       }
+    }
+    die "can't find file '$volid'\n"
+       if !($path && (-f $path || ($allow_blockdev && -b $path)));
+
+    return $path;
+}
+
+# used as last resort to adapt volnames when migrating
+my $volname_for_storage = sub {
+    my ($cfg, $storeid, $name, $vmid, $format) = @_;
+
+    my $scfg = storage_config($cfg, $storeid);
+
+    my (undef, $valid_formats) = PVE::Storage::Plugin::default_format($scfg);
+    my $format_is_valid = grep { $_ eq $format } @$valid_formats;
+    die "unsupported format '$format' for storage type $scfg->{type}\n"
+       if !$format_is_valid;
+
+    (my $name_without_extension = $name) =~ s/\.$format$//;
+
+    if ($scfg->{path}) {
+       return "$vmid/$name_without_extension.$format";
+    } else {
+       return "$name_without_extension";
+    }
+};
+
+# whether a migration snapshot is needed for a given storage
+sub storage_migrate_snapshot {
+    my ($cfg, $storeid, $existing_snapshots) = @_;
+    my $scfg = storage_config($cfg, $storeid);
+
+    return $scfg->{type} eq 'zfspool' || ($scfg->{type} eq 'btrfs' && $existing_snapshots);
+}
+
+my $volume_import_prepare = sub {
+    my ($volid, $format, $path, $apiver, $opts) = @_;
+
+    my $base_snapshot = $opts->{base_snapshot};
+    my $snapshot = $opts->{snapshot};
+    my $with_snapshots = $opts->{with_snapshots} ? 1 : 0;
+    my $migration_snapshot = $opts->{migration_snapshot} ? 1 : 0;
+    my $allow_rename = $opts->{allow_rename} ? 1 : 0;
+
+    my $recv = ['pvesm', 'import', $volid, $format, $path, '-with-snapshots', $with_snapshots];
+    if (defined($snapshot)) {
+       push @$recv, '-snapshot', $snapshot;
+    }
+    if ($migration_snapshot) {
+       push @$recv, '-delete-snapshot', $snapshot;
+    }
+    push @$recv, '-allow-rename', $allow_rename if $apiver >= 5;
+
+    if (defined($base_snapshot)) {
+       # Check if the snapshot exists on the remote side:
+       push @$recv, '-base', $base_snapshot if $apiver >= 9;
+    }
+
+    return $recv;
+};
+
+my $volume_export_prepare = sub {
+    my ($cfg, $volid, $format, $logfunc, $opts) = @_;
+    my $base_snapshot = $opts->{base_snapshot};
+    my $snapshot = $opts->{snapshot};
+    my $with_snapshots = $opts->{with_snapshots} ? 1 : 0;
+    my $migration_snapshot = $opts->{migration_snapshot} ? 1 : 0;
+    my $ratelimit_bps = $opts->{ratelimit_bps};
+
+    my $send = ['pvesm', 'export', $volid, $format, '-', '-with-snapshots', $with_snapshots];
+    if (defined($snapshot)) {
+       push @$send, '-snapshot', $snapshot;
+    }
+    if (defined($base_snapshot)) {
+       push @$send, '-base', $base_snapshot;
+    }
+
+    my $cstream;
+    if (defined($ratelimit_bps)) {
+       $cstream = [ '/usr/bin/cstream', '-t', $ratelimit_bps ];
+       $logfunc->("using a bandwidth limit of $ratelimit_bps bps for transferring '$volid'") if $logfunc;
+    }
+
+    volume_snapshot($cfg, $volid, $snapshot) if $migration_snapshot;
+
+    if (defined($snapshot)) {
+       activate_volumes($cfg, [$volid], $snapshot);
+    } else {
+       activate_volumes($cfg, [$volid]);
+    }
+
+    return $cstream ? [ $send, $cstream ] : [ $send ];
+};
+
+sub storage_migrate {
+    my ($cfg, $volid, $target_sshinfo, $target_storeid, $opts, $logfunc) = @_;
+
+    my $insecure = $opts->{insecure};
+
+    my ($storeid, $volname) = parse_volume_id($volid);
+
+    my $scfg = storage_config($cfg, $storeid);
+
+    # no need to migrate shared content
+    return $volid if $storeid eq $target_storeid && $scfg->{shared};
+
+    my $tcfg = storage_config($cfg, $target_storeid);
+
+    my $target_volname;
+    if ($opts->{target_volname}) {
+       $target_volname = $opts->{target_volname};
+    } elsif ($scfg->{type} eq $tcfg->{type}) {
+       $target_volname = $volname;
+    } else {
+       my (undef, $name, $vmid, undef, undef, undef, $format) = parse_volname($cfg, $volid);
+       $target_volname = $volname_for_storage->($cfg, $target_storeid, $name, $vmid, $format);
+    }
+
+    my $target_volid = "${target_storeid}:${target_volname}";
+
+    my $target_ip = $target_sshinfo->{ip};
+
+    my $ssh = PVE::SSHInfo::ssh_info_to_command($target_sshinfo);
+    my $ssh_base = PVE::SSHInfo::ssh_info_to_command_base($target_sshinfo);
+    local $ENV{RSYNC_RSH} = PVE::Tools::cmd2string($ssh_base);
+
+    if (!defined($opts->{snapshot})) {
+       $opts->{migration_snapshot} = storage_migrate_snapshot($cfg, $storeid, $opts->{with_snapshots});
+       $opts->{snapshot} = '__migration__' if $opts->{migration_snapshot};
+    }
+
+    my @formats = volume_transfer_formats($cfg, $volid, $target_volid, $opts->{snapshot}, $opts->{base_snapshot}, $opts->{with_snapshots});
+    die "cannot migrate from storage type '$scfg->{type}' to '$tcfg->{type}'\n" if !@formats;
+    my $format = $formats[0];
+
+    my $import_fn = '-'; # let pvesm import read from stdin per default
+    if ($insecure) {
+       my $net = $target_sshinfo->{network} // $target_sshinfo->{ip};
+       $import_fn = "tcp://$net";
+    }
+
+    my $target_apiver = 1; # if there is no apiinfo call, assume 1
+    my $get_api_version = [@$ssh, 'pvesm', 'apiinfo'];
+    my $match_api_version = sub { $target_apiver = $1 if $_[0] =~ m!^APIVER (\d+)$!; };
+    eval { run_command($get_api_version, logfunc => $match_api_version); };
+
+    my $recv = [ @$ssh, '--', $volume_import_prepare->($target_volid, $format, $import_fn, $target_apiver, $opts)->@* ];
+
+    my $new_volid;
+    my $pattern = volume_imported_message(undef, 1);
+    my $match_volid_and_log = sub {
+       my $line = shift;
+
+       $new_volid = $1 if ($line =~ $pattern);
+
+       if ($logfunc) {
+           chomp($line);
+           $logfunc->($line);
+       }
+    };
+
+    my $cmds = $volume_export_prepare->($cfg, $volid, $format, $logfunc, $opts);
+
+    eval {
+       if ($insecure) {
+           my $input = IO::File->new();
+           my $info = IO::File->new();
+           open3($input, $info, $info, @$recv)
+               or die "receive command failed: $!\n";
+           close($input);
+
+           my $try_ip = <$info> // '';
+           my ($ip) = $try_ip =~ /^($PVE::Tools::IPRE)$/ # untaint
+               or die "no tunnel IP received, got '$try_ip'\n";
+
+           my $try_port = <$info> // '';
+           my ($port) = $try_port =~ /^(\d+)$/ # untaint
+               or die "no tunnel port received, got '$try_port'\n";
+
+           my $socket = IO::Socket::IP->new(PeerHost => $ip, PeerPort => $port, Type => SOCK_STREAM)
+               or die "failed to connect to tunnel at $ip:$port\n";
+           # we won't be reading from the socket
+           shutdown($socket, 0);
+
+           eval { run_command($cmds, output => '>&'.fileno($socket), errfunc => $logfunc); };
+           my $send_error = $@;
+
+           # don't close the connection entirely otherwise the receiving end
+           # might not get all buffered data (and fails with 'connection reset by peer')
+           shutdown($socket, 1);
+
+           # wait for the remote process to finish
+           while (my $line = <$info>) {
+               $match_volid_and_log->("[$target_sshinfo->{name}] $line");
+           }
+
+           # now close the socket
+           close($socket);
+           if (!close($info)) { # does waitpid()
+               die "import failed: $!\n" if $!;
+               die "import failed: exit code ".($?>>8)."\n";
+           }
+
+           die $send_error if $send_error;
+       } else {
+           push @$cmds, $recv;
+           run_command($cmds, logfunc => $match_volid_and_log);
+       }
+
+       die "unable to get ID of the migrated volume\n"
+           if !defined($new_volid) && $target_apiver >= 5;
+    };
+    my $err = $@;
+    warn "send/receive failed, cleaning up snapshot(s)..\n" if $err;
+    if ($opts->{migration_snapshot}) {
+       eval { volume_snapshot_delete($cfg, $volid, $opts->{snapshot}, 0) };
+       warn "could not remove source snapshot: $@\n" if $@;
+    }
+    die $err if $err;
+
+    return $new_volid // $target_volid;
+}
+
+sub vdisk_clone {
+    my ($cfg, $volid, $vmid, $snap) = @_;
+
+    my ($storeid, $volname) = parse_volume_id($volid);
+
+    my $scfg = storage_config($cfg, $storeid);
+
+    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+
+    activate_storage($cfg, $storeid);
+
+    # lock shared storage
+    return $plugin->cluster_lock_storage($storeid, $scfg->{shared}, undef, sub {
+       my $volname = $plugin->clone_image($scfg, $storeid, $volname, $vmid, $snap);
+       return "$storeid:$volname";
+    });
+}
+
+sub vdisk_create_base {
+    my ($cfg, $volid) = @_;
+
+    my ($storeid, $volname) = parse_volume_id($volid);
+
+    my $scfg = storage_config($cfg, $storeid);
+
+    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+
+    activate_storage($cfg, $storeid);
+
+    # lock shared storage
+    return $plugin->cluster_lock_storage($storeid, $scfg->{shared}, undef, sub {
+       my $volname = $plugin->create_base($storeid, $scfg, $volname);
+       return "$storeid:$volname";
+    });
+}
+
+sub map_volume {
+    my ($cfg, $volid, $snapname) = @_;
+
+    my ($storeid, $volname) = parse_volume_id($volid);
+
+    my $scfg = storage_config($cfg, $storeid);
+
+    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+
+    return $plugin->map_volume($storeid, $scfg, $volname, $snapname);
+}
+
+sub unmap_volume {
+    my ($cfg, $volid, $snapname) = @_;
+
+    my ($storeid, $volname) = parse_volume_id($volid);
+
+    my $scfg = storage_config($cfg, $storeid);
+
+    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+
+    return $plugin->unmap_volume($storeid, $scfg, $volname, $snapname);
+}
+
+sub vdisk_alloc {
+    my ($cfg, $storeid, $vmid, $fmt, $name, $size) = @_;
+
+    die "no storage ID specified\n" if !$storeid;
+
+    PVE::JSONSchema::parse_storage_id($storeid);
+
+    my $scfg = storage_config($cfg, $storeid);
+
+    die "no VMID specified\n" if !$vmid;
+
+    $vmid = parse_vmid($vmid);
+
+    my $defformat = PVE::Storage::Plugin::default_format($scfg);
+
+    $fmt = $defformat if !$fmt;
+
+    activate_storage($cfg, $storeid);
+
+    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+
+    # lock shared storage
+    return $plugin->cluster_lock_storage($storeid, $scfg->{shared}, undef, sub {
+       my $old_umask = umask(umask|0037);
+       my $volname = eval { $plugin->alloc_image($storeid, $scfg, $vmid, $fmt, $name, $size) };
+       my $err = $@;
+       umask $old_umask;
+       die $err if $err;
+       return "$storeid:$volname";
+    });
+}
+
+sub vdisk_free {
+    my ($cfg, $volid) = @_;
+
+    my ($storeid, $volname) = parse_volume_id($volid);
+    my $scfg = storage_config($cfg, $storeid);
+    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+
+    activate_storage($cfg, $storeid);
+
+    my $cleanup_worker;
+
+    # lock shared storage
+    $plugin->cluster_lock_storage($storeid, $scfg->{shared}, undef, sub {
+       # LVM-thin allows deletion of still referenced base volumes!
+       die "base volume '$volname' is still in use by linked clones\n"
+           if volume_is_base_and_used($cfg, $volid);
+
+       my (undef, undef, undef, undef, undef, $isBase, $format) =
+           $plugin->parse_volname($volname);
+       $cleanup_worker = $plugin->free_image($storeid, $scfg, $volname, $isBase, $format);
+    });
+
+    return if !$cleanup_worker;
+
+    my $rpcenv = PVE::RPCEnvironment::get();
+    my $authuser = $rpcenv->get_user();
+
+    $rpcenv->fork_worker('imgdel', undef, $authuser, $cleanup_worker);
+}
+
+sub vdisk_list {
+    my ($cfg, $storeid, $vmid, $vollist, $ctype) = @_;
+
+    my $ids = $cfg->{ids};
+
+    storage_check_enabled($cfg, $storeid) if ($storeid);
+
+    my $res = $storeid ? { $storeid => [] } : {};
+
+    # prepare/activate/refresh all storages
+
+    my $storage_list = [];
+    if ($vollist) {
+       foreach my $volid (@$vollist) {
+           my ($sid, undef) = parse_volume_id($volid);
+           next if !defined($ids->{$sid});
+           next if !storage_check_enabled($cfg, $sid, undef, 1);
+           push @$storage_list, $sid;
+       }
+    } else {
+       foreach my $sid (keys %$ids) {
+           next if $storeid && $storeid ne $sid;
+           next if !storage_check_enabled($cfg, $sid, undef, 1);
+           my $content = $ids->{$sid}->{content};
+           next if defined($ctype) && !$content->{$ctype};
+           next if !($content->{rootdir} || $content->{images});
+           push @$storage_list, $sid;
+       }
+    }
+
+    my $cache = {};
+
+    activate_storage_list($cfg, $storage_list, $cache);
+
+    for my $sid ($storage_list->@*) {
+       next if $storeid && $storeid ne $sid;
+
+       my $scfg = $ids->{$sid};
+       my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+       $res->{$sid} = $plugin->list_images($sid, $scfg, $vmid, $vollist, $cache);
+       @{$res->{$sid}} = sort {lc($a->{volid}) cmp lc ($b->{volid}) } @{$res->{$sid}} if $res->{$sid};
+    }
+
+    return $res;
+}
+
+sub template_list {
+    my ($cfg, $storeid, $tt) = @_;
+
+    die "unknown template type '$tt'\n"
+       if !($tt eq 'iso' || $tt eq 'vztmpl' || $tt eq 'backup' || $tt eq 'snippets');
+
+    my $ids = $cfg->{ids};
+
+    storage_check_enabled($cfg, $storeid) if ($storeid);
+
+    my $res = {};
+
+    # query the storage
+    foreach my $sid (keys %$ids) {
+       next if $storeid && $storeid ne $sid;
+
+       my $scfg = $ids->{$sid};
+       my $type = $scfg->{type};
+
+       next if !$scfg->{content}->{$tt};
+
+       next if !storage_check_enabled($cfg, $sid, undef, 1);
+
+       $res->{$sid} = volume_list($cfg, $sid, undef, $tt);
+    }
+
+    return $res;
+}
+
+sub volume_list {
+    my ($cfg, $storeid, $vmid, $content) = @_;
+
+    my @ctypes = qw(rootdir images vztmpl iso backup snippets);
+
+    my $cts = $content ? [ $content ] : [ @ctypes ];
+
+    my $scfg = PVE::Storage::storage_config($cfg, $storeid);
+
+    $cts = [ grep { defined($scfg->{content}->{$_}) } @$cts ];
+
+    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+
+    activate_storage($cfg, $storeid);
+
+    my $res = $plugin->list_volumes($storeid, $scfg, $vmid, $cts);
+
+    @$res = sort {lc($a->{volid}) cmp lc ($b->{volid}) } @$res;
+
+    return $res;
+}
+
+sub uevent_seqnum {
+
+    my $filename = "/sys/kernel/uevent_seqnum";
+
+    my $seqnum = 0;
+    if (my $fh = IO::File->new($filename, "r")) {
+       my $line = <$fh>;
+       if ($line =~ m/^(\d+)$/) {
+           $seqnum = int($1);
+       }
+       close ($fh);
+    }
+    return $seqnum;
+}
+
+sub activate_storage {
+    my ($cfg, $storeid, $cache) = @_;
+
+    $cache = {} if !$cache;
+
+    my $scfg = storage_check_enabled($cfg, $storeid);
+
+    return if $cache->{activated}->{$storeid};
+
+    $cache->{uevent_seqnum} = uevent_seqnum() if !$cache->{uevent_seqnum};
+
+    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+
+    if ($scfg->{base}) {
+       my ($baseid, undef) = parse_volume_id ($scfg->{base});
+       activate_storage($cfg, $baseid, $cache);
+    }
+
+    if (! eval { $plugin->check_connection($storeid, $scfg) }) {
+       die "connection check for storage '$storeid' failed - $@\n" if $@;
+       die "storage '$storeid' is not online\n";
+    }
+
+    $plugin->activate_storage($storeid, $scfg, $cache);
+
+    my $newseq = uevent_seqnum ();
+
+    # only call udevsettle if there are events
+    if ($newseq > $cache->{uevent_seqnum}) {
+       system ("udevadm settle --timeout=30"); # ignore errors
+       $cache->{uevent_seqnum} = $newseq;
+    }
+
+    $cache->{activated}->{$storeid} = 1;
+}
+
+sub activate_storage_list {
+    my ($cfg, $storeid_list, $cache) = @_;
+
+    $cache = {} if !$cache;
+
+    foreach my $storeid (@$storeid_list) {
+       activate_storage($cfg, $storeid, $cache);
+    }
+}
+
+sub deactivate_storage {
+    my ($cfg, $storeid) = @_;
+
+    my $scfg = storage_config ($cfg, $storeid);
+    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+
+    my $cache = {};
+    $plugin->deactivate_storage($storeid, $scfg, $cache);
+}
+
+sub activate_volumes {
+    my ($cfg, $vollist, $snapname) = @_;
+
+    return if !($vollist && scalar(@$vollist));
+
+    my $storagehash = {};
+    foreach my $volid (@$vollist) {
+       my ($storeid, undef) = parse_volume_id($volid);
+       $storagehash->{$storeid} = 1;
+    }
+
+    my $cache = {};
+
+    activate_storage_list($cfg, [keys %$storagehash], $cache);
+
+    foreach my $volid (@$vollist) {
+       my ($storeid, $volname) = parse_volume_id($volid);
+       my $scfg = storage_config($cfg, $storeid);
+       my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+       $plugin->activate_volume($storeid, $scfg, $volname, $snapname, $cache);
+    }
+}
+
+sub deactivate_volumes {
+    my ($cfg, $vollist, $snapname) = @_;
+
+    return if !($vollist && scalar(@$vollist));
+
+    my $cache = {};
+
+    my @errlist = ();
+    foreach my $volid (@$vollist) {
+       my ($storeid, $volname) = parse_volume_id($volid);
+
+       my $scfg = storage_config($cfg, $storeid);
+       my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+
+       eval {
+           $plugin->deactivate_volume($storeid, $scfg, $volname, $snapname, $cache);
+       };
+       if (my $err = $@) {
+           warn $err;
+           push @errlist, $volid;
+       }
+    }
+
+    die "volume deactivation failed: " . join(' ', @errlist)
+       if scalar(@errlist);
+}
+
+sub storage_info {
+    my ($cfg, $content, $includeformat) = @_;
+
+    my $ids = $cfg->{ids};
+
+    my $info = {};
+
+    my @ctypes = PVE::Tools::split_list($content);
+
+    my $slist = [];
+    foreach my $storeid (keys %$ids) {
+       my $storage_enabled = defined(storage_check_enabled($cfg, $storeid, undef, 1));
+
+       if (defined($content)) {
+           my $want_ctype = 0;
+           foreach my $ctype (@ctypes) {
+               if ($ids->{$storeid}->{content}->{$ctype}) {
+                   $want_ctype = 1;
+                   last;
+               }
+           }
+           next if !$want_ctype || !$storage_enabled;
+       }
+
+       my $type = $ids->{$storeid}->{type};
+
+       $info->{$storeid} = {
+           type => $type,
+           total => 0,
+           avail => 0,
+           used => 0,
+           shared => $ids->{$storeid}->{shared} ? 1 : 0,
+           content => PVE::Storage::Plugin::content_hash_to_string($ids->{$storeid}->{content}),
+           active => 0,
+           enabled => $storage_enabled ? 1 : 0,
+       };
+
+       push @$slist, $storeid;
+    }
+
+    my $cache = {};
+
+    foreach my $storeid (keys %$ids) {
+       my $scfg = $ids->{$storeid};
+
+       next if !$info->{$storeid};
+       next if !$info->{$storeid}->{enabled};
+
+       my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+       if ($includeformat) {
+           my $pd = $plugin->plugindata();
+           $info->{$storeid}->{format} = $pd->{format}
+               if $pd->{format};
+           $info->{$storeid}->{select_existing} = $pd->{select_existing}
+               if $pd->{select_existing};
+       }
+
+       eval { activate_storage($cfg, $storeid, $cache); };
+       if (my $err = $@) {
+           warn $err;
+           next;
+       }
+
+       my ($total, $avail, $used, $active) = eval { $plugin->status($storeid, $scfg, $cache); };
+       warn $@ if $@;
+       next if !$active;
+       $info->{$storeid}->{total} = int($total);
+       $info->{$storeid}->{avail} = int($avail);
+       $info->{$storeid}->{used} = int($used);
+       $info->{$storeid}->{active} = $active;
+    }
+
+    return $info;
+}
+
+sub resolv_server {
+    my ($server) = @_;
+
+    my ($packed_ip, $family);
+    eval {
+       my @res = PVE::Tools::getaddrinfo_all($server);
+       $family = $res[0]->{family};
+       $packed_ip = (PVE::Tools::unpack_sockaddr_in46($res[0]->{addr}))[2];
+    };
+    if (defined $packed_ip) {
+       return Socket::inet_ntop($family, $packed_ip);
+    }
+    return undef;
+}
+
+sub scan_nfs {
+    my ($server_in) = @_;
+
+    my $server;
+    if (!($server = resolv_server ($server_in))) {
+       die "unable to resolve address for server '${server_in}'\n";
+    }
+
+    my $cmd = ['/sbin/showmount',  '--no-headers', '--exports', $server];
+
+    my $res = {};
+    run_command($cmd, outfunc => sub {
+       my $line = shift;
+
+       # note: howto handle white spaces in export path??
+       if ($line =~ m!^(/\S+)\s+(.+)$!) {
+           $res->{$1} = $2;
+       }
+    });
+
+    return $res;
+}
+
+sub scan_cifs {
+    my ($server_in, $user, $password, $domain) = @_;
+
+    my $server = resolv_server($server_in);
+    die "unable to resolve address for server '${server_in}'\n" if !$server;
+
+    # we only support Windows 2012 and newer, so just use smb3
+    my $cmd = ['/usr/bin/smbclient', '-m', 'smb3', '-d', '0', '-L', $server];
+    push @$cmd, '-W', $domain if defined($domain);
+
+    push @$cmd, '-N' if !defined($password);
+    local $ENV{USER} = $user if defined($user);
+    local $ENV{PASSWD} = $password if defined($password);
+
+    my $res = {};
+    my $err = '';
+    run_command($cmd,
+       noerr => 1,
+       errfunc => sub {
+           $err .= "$_[0]\n"
+       },
+       outfunc => sub {
+           my $line = shift;
+           if ($line =~ m/(\S+)\s*Disk\s*(\S*)/) {
+               $res->{$1} = $2;
+           } elsif ($line =~ m/(NT_STATUS_(\S+))/) {
+               my $status = $1;
+               $err .= "unexpected status: $1\n" if uc($1) ne 'SUCCESS';
+           }
+       },
+    );
+    # only die if we got no share, else it's just some followup check error
+    # (like workgroup querying)
+    raise($err) if $err && !%$res;
+
+    return $res;
+}
+
+sub scan_zfs {
+
+    my $cmd = ['zfs',  'list', '-t', 'filesystem', '-Hp', '-o', 'name,avail,used'];
+
+    my $res = [];
+    run_command($cmd, outfunc => sub {
+       my $line = shift;
+
+       if ($line =~m/^(\S+)\s+(\S+)\s+(\S+)$/) {
+           my ($pool, $size_str, $used_str) = ($1, $2, $3);
+           my $size = $size_str + 0;
+           my $used = $used_str + 0;
+           # ignore subvolumes generated by our ZFSPoolPlugin
+           return if $pool =~ m!/subvol-\d+-[^/]+$!;
+           return if $pool =~ m!/basevol-\d+-[^/]+$!;
+           push @$res, { pool => $pool, size => $size, free => $size-$used };
+       }
+    });
+
+    return $res;
+}
+
+sub resolv_portal {
+    my ($portal, $noerr) = @_;
+
+    my ($server, $port) = PVE::Tools::parse_host_and_port($portal);
+    if ($server) {
+       if (my $ip = resolv_server($server)) {
+           $server = $ip;
+           $server = "[$server]" if $server =~ /^$IPV6RE$/;
+           return $port ? "$server:$port" : $server;
+       }
+    }
+    return undef if $noerr;
+
+    raise_param_exc({ portal => "unable to resolve portal address '$portal'" });
+}
+
+
+sub scan_iscsi {
+    my ($portal_in) = @_;
+
+    my $portal;
+    if (!($portal = resolv_portal($portal_in))) {
+       die "unable to parse/resolve portal address '${portal_in}'\n";
+    }
+
+    return PVE::Storage::ISCSIPlugin::iscsi_discovery($portal);
+}
+
+sub storage_default_format {
+    my ($cfg, $storeid) = @_;
+
+    my $scfg = storage_config ($cfg, $storeid);
+
+    return PVE::Storage::Plugin::default_format($scfg);
+}
+
+sub vgroup_is_used {
+    my ($cfg, $vgname) = @_;
+
+    foreach my $storeid (keys %{$cfg->{ids}}) {
+       my $scfg = storage_config($cfg, $storeid);
+       if ($scfg->{type} eq 'lvm' && $scfg->{vgname} eq $vgname) {
+           return 1;
+       }
+    }
+
+    return undef;
+}
+
+sub target_is_used {
+    my ($cfg, $target) = @_;
+
+    foreach my $storeid (keys %{$cfg->{ids}}) {
+       my $scfg = storage_config($cfg, $storeid);
+       if ($scfg->{type} eq 'iscsi' && $scfg->{target} eq $target) {
+           return 1;
+       }
+    }
+
+    return undef;
+}
+
+sub volume_is_used {
+    my ($cfg, $volid) = @_;
+
+    foreach my $storeid (keys %{$cfg->{ids}}) {
+       my $scfg = storage_config($cfg, $storeid);
+       if ($scfg->{base} && $scfg->{base} eq $volid) {
+           return 1;
+       }
+    }
+
+    return undef;
+}
+
+sub storage_is_used {
+    my ($cfg, $storeid) = @_;
+
+    foreach my $sid (keys %{$cfg->{ids}}) {
+       my $scfg = storage_config($cfg, $sid);
+       next if !$scfg->{base};
+       my ($st) = parse_volume_id($scfg->{base});
+       return 1 if $st && $st eq $storeid;
+    }
+
+    return undef;
+}
+
+sub foreach_volid {
+    my ($list, $func) = @_;
+
+    return if !$list;
+
+    foreach my $sid (keys %$list) {
+       foreach my $info (@{$list->{$sid}}) {
+           my $volid = $info->{volid};
+          my ($sid1, $volname) = parse_volume_id($volid, 1);
+          if ($sid1 && $sid1 eq $sid) {
+              &$func ($volid, $sid, $info);
+          } else {
+              warn "detected strange volid '$volid' in volume list for '$sid'\n";
+          }
+       }
+    }
+}
+
+sub decompressor_info {
+    my ($format, $comp) = @_;
+
+    if ($format eq 'tgz' && !defined($comp)) {
+       ($format, $comp) = ('tar', 'gz');
+    }
+
+    my $decompressor = {
+       tar => {
+           gz => ['tar', '-z'],
+           lzo => ['tar', '--lzop'],
+           zst => ['tar', '--zstd'],
+       },
+       vma => {
+           gz => ['zcat'],
+           lzo => ['lzop', '-d', '-c'],
+           zst => ['zstd', '-q', '-d', '-c'],
+       },
+    };
+
+    die "ERROR: archive format not defined\n"
+       if !defined($decompressor->{$format});
+
+    my $decomp;
+    $decomp = $decompressor->{$format}->{$comp} if $comp;
+
+    my $info = {
+       format => $format,
+       compression => $comp,
+       decompressor => $decomp,
+    };
+
+    return $info;
+}
+
+sub protection_file_path {
+    my ($path) = @_;
+
+    return "${path}.protected";
+}
+
+sub archive_info {
+    my ($archive) = shift;
+    my $info;
+
+    my $volid = basename($archive);
+    if ($volid =~ /^(vzdump-(lxc|openvz|qemu)-.+$BACKUP_EXT_RE_2)$/) {
+       my $filename = "$1"; # untaint
+       my ($type, $extension, $comp) = ($2, $3, $4);
+       (my $format = $extension) =~ s/\..*//;
+       $info = decompressor_info($format, $comp);
+       $info->{filename} = $filename;
+       $info->{type} = $type;
+
+       if ($volid =~ /^(vzdump-${type}-([1-9][0-9]{2,8})-(\d{4})_(\d{2})_(\d{2})-(\d{2})_(\d{2})_(\d{2}))\.${extension}$/) {
+           $info->{logfilename} = "$1".PVE::Storage::Plugin::LOG_EXT;
+           $info->{notesfilename} = "$filename".PVE::Storage::Plugin::NOTES_EXT;
+           $info->{vmid} = int($2);
+           $info->{ctime} = timelocal($8, $7, $6, $5, $4 - 1, $3);
+           $info->{is_std_name} = 1;
+       } else {
+           $info->{is_std_name} = 0;
+       }
+    } else {
+       die "ERROR: couldn't determine archive info from '$archive'\n";
+    }
+
+    return $info;
+}
+
+sub archive_remove {
+    my ($archive_path) = @_;
+
+    die "cannot remove protected archive '$archive_path'\n"
+       if -e protection_file_path($archive_path);
+
+    unlink $archive_path or $! == ENOENT or die "removing archive $archive_path failed: $!\n";
+
+    archive_auxiliaries_remove($archive_path);
+}
+
+sub archive_auxiliaries_remove {
+    my ($archive_path) = @_;
+
+    my $dirname = dirname($archive_path);
+    my $archive_info = eval { archive_info($archive_path) } // {};
+
+    for my $type (qw(log notes)) {
+       my $filename = $archive_info->{"${type}filename"} or next;
+       my $path = "$dirname/$filename";
+
+       if (-e $path) {
+           unlink $path or $! == ENOENT or log_warn("Removing $type file failed: $!");
+       }
+    }
+}
+
+sub extract_vzdump_config_tar {
+    my ($archive, $conf_re) = @_;
+
+    die "ERROR: file '$archive' does not exist\n" if ! -f $archive;
+
+    my $pid = open(my $fh, '-|', 'tar', 'tf', $archive) ||
+       die "unable to open file '$archive'\n";
+
+    my $file;
+    while (defined($file = <$fh>)) {
+       if ($file =~ $conf_re) {
+           $file = $1; # untaint
+           last;
+       }
+    }
+
+    kill 15, $pid;
+    waitpid $pid, 0;
+    close $fh;
+
+    die "ERROR: archive contains no configuration file\n" if !$file;
+    chomp $file;
+
+    my $raw = '';
+    my $out = sub {
+       my $output = shift;
+       $raw .= "$output\n";
+    };
+
+    run_command(['tar', '-xpOf', $archive, $file, '--occurrence'], outfunc => $out);
+
+    return wantarray ? ($raw, $file) : $raw;
+}
+
+sub extract_vzdump_config_vma {
+    my ($archive, $comp) = @_;
+
+    my $raw = '';
+    my $out = sub { $raw .= "$_[0]\n"; };
+
+    my $info = archive_info($archive);
+    $comp //= $info->{compression};
+    my $decompressor = $info->{decompressor};
+
+    if ($comp) {
+       my $cmd = [ [@$decompressor, $archive], ["vma", "config", "-"] ];
+
+       # lzop/zcat exits with 1 when the pipe is closed early by vma, detect this and ignore the exit code later
+       my $broken_pipe;
+       my $errstring;
+       my $err = sub {
+           my $output = shift;
+           if ($output =~ m/lzop: Broken pipe: <stdout>/ || $output =~ m/gzip: stdout: Broken pipe/ || $output =~ m/zstd: error 70 : Write error.*Broken pipe/) {
+               $broken_pipe = 1;
+           } elsif (!defined ($errstring) && $output !~ m/^\s*$/) {
+               $errstring = "Failed to extract config from VMA archive: $output\n";
+           }
+       };
+
+       my $rc = eval { run_command($cmd, outfunc => $out, errfunc => $err, noerr => 1) };
+       my $rerr = $@;
+
+       $broken_pipe ||= $rc == 141; # broken pipe from vma POV
+
+       if (!$errstring && !$broken_pipe && $rc != 0) {
+           die "$rerr\n" if $rerr;
+           die "config extraction failed with exit code $rc\n";
+       }
+       die "$errstring\n" if $errstring;
+    } else {
+       run_command(["vma", "config", $archive], outfunc => $out);
+    }
+
+    return wantarray ? ($raw, undef) : $raw;
+}
+
+sub extract_vzdump_config {
+    my ($cfg, $volid) = @_;
+
+    my ($storeid, $volname) = parse_volume_id($volid);
+    if (defined($storeid)) {
+       my $scfg = storage_config($cfg, $storeid);
+       if ($scfg->{type} eq 'pbs') {
+           storage_check_enabled($cfg, $storeid);
+           return PVE::Storage::PBSPlugin->extract_vzdump_config($scfg, $volname, $storeid);
+       }
+    }
+
+    my $archive = abs_filesystem_path($cfg, $volid);
+    my $info = archive_info($archive);
+    my $format = $info->{format};
+    my $comp = $info->{compression};
+    my $type = $info->{type};
+
+    if ($type eq 'lxc' || $type eq 'openvz') {
+       return extract_vzdump_config_tar($archive, qr!^(\./etc/vzdump/(pct|vps)\.conf)$!);
+    } elsif ($type eq 'qemu') {
+       if ($format eq 'tar') {
+           return extract_vzdump_config_tar($archive, qr!\(\./qemu-server\.conf\)!);
+       } else {
+           return extract_vzdump_config_vma($archive, $comp);
+       }
+    } else {
+       die "cannot determine backup guest type for backup archive '$volid'\n";
+    }
+}
+
+sub prune_backups {
+    my ($cfg, $storeid, $keep, $vmid, $type, $dryrun, $logfunc) = @_;
+
+    my $scfg = storage_config($cfg, $storeid);
+    die "storage '$storeid' does not support backups\n" if !$scfg->{content}->{backup};
+
+    if (!defined($keep)) {
+       die "no prune-backups options configured for storage '$storeid'\n"
+           if !defined($scfg->{'prune-backups'});
+       $keep = PVE::JSONSchema::parse_property_string('prune-backups', $scfg->{'prune-backups'});
+    }
+
+    activate_storage($cfg, $storeid);
+
+    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+    return $plugin->prune_backups($scfg, $storeid, $keep, $vmid, $type, $dryrun, $logfunc);
+}
+
+my $prune_mark = sub {
+    my ($prune_entries, $keep_count, $id_func) = @_;
+
+    return if !$keep_count;
+
+    my $already_included = {};
+    my $newly_included = {};
+
+    foreach my $prune_entry (@{$prune_entries}) {
+       my $mark = $prune_entry->{mark};
+       my $id = $id_func->($prune_entry->{ctime});
+       $already_included->{$id} = 1 if defined($mark) && $mark eq 'keep';
+    }
+
+    foreach my $prune_entry (@{$prune_entries}) {
+       my $mark = $prune_entry->{mark};
+       my $id = $id_func->($prune_entry->{ctime});
+
+       next if defined($mark) || $already_included->{$id};
+
+       if (!$newly_included->{$id}) {
+           last if scalar(keys %{$newly_included}) >= $keep_count;
+           $newly_included->{$id} = 1;
+           $prune_entry->{mark} = 'keep';
+       } else {
+           $prune_entry->{mark} = 'remove';
+       }
+    }
+};
+
+sub prune_mark_backup_group {
+    my ($backup_group, $keep) = @_;
+
+    my @positive_opts = grep { $_ ne 'keep-all' && $keep->{$_} > 0 } keys $keep->%*;
+
+    if ($keep->{'keep-all'} || scalar(@positive_opts) == 0) {
+       foreach my $prune_entry (@{$backup_group}) {
+           # preserve additional information like 'protected'
+           next if $prune_entry->{mark} && $prune_entry->{mark} ne 'remove';
+           $prune_entry->{mark} = 'keep';
+       }
+       return;
+    }
+
+    my $prune_list = [ sort { $b->{ctime} <=> $a->{ctime} } @{$backup_group} ];
+
+    $prune_mark->($prune_list, $keep->{'keep-last'}, sub {
+       my ($ctime) = @_;
+       return $ctime;
+    });
+    $prune_mark->($prune_list, $keep->{'keep-hourly'}, sub {
+       my ($ctime) = @_;
+       my (undef, undef, $hour, $day, $month, $year) = localtime($ctime);
+       return "$hour/$day/$month/$year";
+    });
+    $prune_mark->($prune_list, $keep->{'keep-daily'}, sub {
+       my ($ctime) = @_;
+       my (undef, undef, undef, $day, $month, $year) = localtime($ctime);
+       return "$day/$month/$year";
+    });
+    $prune_mark->($prune_list, $keep->{'keep-weekly'}, sub {
+       my ($ctime) = @_;
+       my ($sec, $min, $hour, $day, $month, $year) = localtime($ctime);
+       my $iso_week = int(strftime("%V", $sec, $min, $hour, $day, $month, $year));
+       my $iso_week_year = int(strftime("%G", $sec, $min, $hour, $day, $month, $year));
+       return "$iso_week/$iso_week_year";
+    });
+    $prune_mark->($prune_list, $keep->{'keep-monthly'}, sub {
+       my ($ctime) = @_;
+       my (undef, undef, undef, undef, $month, $year) = localtime($ctime);
+       return "$month/$year";
+    });
+    $prune_mark->($prune_list, $keep->{'keep-yearly'}, sub {
+       my ($ctime) = @_;
+       my $year = (localtime($ctime))[5];
+       return "$year";
+    });
+
+    foreach my $prune_entry (@{$prune_list}) {
+       $prune_entry->{mark} //= 'remove';
+    }
+}
+
+sub volume_export : prototype($$$$$$$) {
+    my ($cfg, $fh, $volid, $format, $snapshot, $base_snapshot, $with_snapshots) = @_;
+
+    my ($storeid, $volname) = parse_volume_id($volid, 1);
+    die "cannot export volume '$volid'\n" if !$storeid;
+    my $scfg = storage_config($cfg, $storeid);
+    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+    return $plugin->volume_export($scfg, $storeid, $fh, $volname, $format,
+                                  $snapshot, $base_snapshot, $with_snapshots);
+}
+
+sub volume_import : prototype($$$$$$$$) {
+    my ($cfg, $fh, $volid, $format, $snapshot, $base_snapshot, $with_snapshots, $allow_rename) = @_;
+
+    my ($storeid, $volname) = parse_volume_id($volid, 1);
+    die "cannot import into volume '$volid'\n" if !$storeid;
+    my $scfg = storage_config($cfg, $storeid);
+    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+    return $plugin->volume_import(
+       $scfg,
+       $storeid,
+       $fh,
+       $volname,
+       $format,
+       $snapshot,
+       $base_snapshot,
+       $with_snapshots,
+       $allow_rename,
+    ) // $volid;
+}
+
+sub volume_export_formats : prototype($$$$$) {
+    my ($cfg, $volid, $snapshot, $base_snapshot, $with_snapshots) = @_;
+
+    my ($storeid, $volname) = parse_volume_id($volid, 1);
+    return if !$storeid;
+    my $scfg = storage_config($cfg, $storeid);
+    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+    return $plugin->volume_export_formats($scfg, $storeid, $volname,
+                                          $snapshot, $base_snapshot,
+                                          $with_snapshots);
+}
+
+sub volume_import_formats : prototype($$$$$) {
+    my ($cfg, $volid, $snapshot, $base_snapshot, $with_snapshots) = @_;
+
+    my ($storeid, $volname) = parse_volume_id($volid, 1);
+    return if !$storeid;
+    my $scfg = storage_config($cfg, $storeid);
+    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+    return $plugin->volume_import_formats(
+       $scfg,
+       $storeid,
+       $volname,
+       $snapshot,
+       $base_snapshot,
+       $with_snapshots,
+    );
+}
+
+sub volume_transfer_formats {
+    my ($cfg, $src_volid, $dst_volid, $snapshot, $base_snapshot, $with_snapshots) = @_;
+    my @export_formats = volume_export_formats($cfg, $src_volid, $snapshot, $base_snapshot, $with_snapshots);
+    my @import_formats = volume_import_formats($cfg, $dst_volid, $snapshot, $base_snapshot, $with_snapshots);
+    my %import_hash = map { $_ => 1 } @import_formats;
+    my @common = grep { $import_hash{$_} } @export_formats;
+    return @common;
+}
+
+sub volume_imported_message {
+    my ($volid, $want_pattern) = @_;
+
+    if ($want_pattern) {
+       return qr/successfully imported '([^']*)'$/;
+    } else {
+       return "successfully imported '$volid'\n";
+    }
+}
+
+# $format and $volname are requests and might be overruled depending on $opts
+# $opts:
+# - with_snapshots: passed to `pvesm import` and used to select import format
+# - allow_rename: passed to `pvesm import`
+# - export_formats: used to select common transport format
+# - unix: unix socket path
+sub volume_import_start {
+    my ($cfg, $storeid, $volname, $format, $vmid, $opts) = @_;
+
+    my $with_snapshots = $opts->{'with_snapshots'} ? 1 : 0;
+
+    $volname = $volname_for_storage->($cfg, $storeid, $volname, $vmid, $format);
+
+    my $volid = "$storeid:$volname";
+
+    # find common import/export format, like volume_transfer_formats
+    my @import_formats = PVE::Storage::volume_import_formats($cfg, $volid, $opts->{snapshot}, undef, $with_snapshots);
+    my @export_formats = PVE::Tools::split_list($opts->{export_formats});
+    my %import_hash = map { $_ => 1 } @import_formats;
+    my @common = grep { $import_hash{$_} } @export_formats;
+    die "no matching import/export format found for storage '$storeid'\n"
+       if !@common;
+    $format = $common[0];
+
+    my $input = IO::File->new();
+    my $info = IO::File->new();
+
+    my $unix = $opts->{unix} // "/run/pve/storage-migrate-$vmid.$$.unix";
+    my $import = $volume_import_prepare->($volid, $format, "unix://$unix", APIVER, $opts);
+
+    unlink $unix;
+    my $cpid = open3($input, $info, $info, @$import)
+       or die "failed to spawn disk-import child - $!\n";
+
+    my $ready;
+    eval {
+       PVE::Tools::run_with_timeout(5, sub { $ready = <$info>; });
+    };
+
+    die "failed to read readyness from disk import child: $@\n" if $@;
+
+    print "$ready\n";
+
+    return {
+       fh => $info,
+       pid => $cpid,
+       socket => $unix,
+       format => $format,
+    };
+}
+
+sub volume_export_start {
+    my ($cfg, $volid, $format, $log, $opts) = @_;
+
+    my $known_format = [ grep { $_ eq $format } $KNOWN_EXPORT_FORMATS->@* ];
+    if (!$known_format->@*) {
+       die "Cannot export '$volid' using unknown export format '$format'\n";
+    }
+    $format = $known_format->[0];
+
+    my $run_command_params = delete $opts->{cmd} // {};
+
+    my $cmds = $volume_export_prepare->($cfg, $volid, $format, $log, $opts);
+
+    PVE::Tools::run_command($cmds, %$run_command_params);
+}
+
+# bash completion helper
+
+sub complete_storage {
+    my ($cmdname, $pname, $cvalue) = @_;
+
+    my $cfg = PVE::Storage::config();
+
+    return  $cmdname eq 'add' ? [] : [ PVE::Storage::storage_ids($cfg) ];
+}
+
+sub complete_storage_enabled {
+    my ($cmdname, $pname, $cvalue) = @_;
+
+    my $res = [];
+
+    my $cfg = PVE::Storage::config();
+    foreach my $sid (keys %{$cfg->{ids}}) {
+       next if !storage_check_enabled($cfg, $sid, undef, 1);
+       push @$res, $sid;
+    }
+    return $res;
+}
+
+sub complete_content_type {
+    my ($cmdname, $pname, $cvalue) = @_;
+
+    return [qw(rootdir images vztmpl iso backup snippets)];
+}
+
+sub complete_volume {
+    my ($cmdname, $pname, $cvalue) = @_;
+
+    my $cfg = config();
+
+    my $storage_list = complete_storage_enabled();
+
+    if ($cvalue =~ m/^([^:]+):/) {
+       $storage_list = [ $1 ];
+    } else {
+       if (scalar(@$storage_list) > 1) {
+           # only list storage IDs to avoid large listings
+           my $res = [];
+           foreach my $storeid (@$storage_list) {
+               # Hack: simply return 2 artificial values, so that
+               # completions does not finish
+               push @$res, "$storeid:volname", "$storeid:...";
+           }
+           return $res;
+       }
+    }
+
+    my $res = [];
+    foreach my $storeid (@$storage_list) {
+       my $vollist = PVE::Storage::volume_list($cfg, $storeid);
+
+       foreach my $item (@$vollist) {
+           push @$res, $item->{volid};
+       }
+    }
+
+    return $res;
+}
+
+sub rename_volume {
+    my ($cfg, $source_volid, $target_vmid, $target_volname) = @_;
+
+    die "no source volid provided\n" if !$source_volid;
+    die "no target VMID or target volname provided\n" if !$target_vmid && !$target_volname;
+
+    my ($storeid, $source_volname) = parse_volume_id($source_volid);
+
+    activate_storage($cfg, $storeid);
+
+    my $scfg = storage_config($cfg, $storeid);
+    my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
+
+    $target_vmid = ($plugin->parse_volname($source_volname))[3] if !$target_vmid;
+
+    return $plugin->cluster_lock_storage($storeid, $scfg->{shared}, undef, sub {
+       return $plugin->rename_volume($scfg, $storeid, $source_volname, $target_vmid, $target_volname);
+    });
+}
+
+# Various io-heavy operations require io/bandwidth limits which can be
+# configured on multiple levels: The global defaults in datacenter.cfg, and
+# per-storage overrides. When we want to do a restore from storage A to storage
+# B, we should take the smaller limit defined for storages A and B, and if no
+# such limit was specified, use the one from datacenter.cfg.
+sub get_bandwidth_limit {
+    my ($operation, $storage_list, $override) = @_;
+
+    # called for each limit (global, per-storage) with the 'default' and the
+    # $operation limit and should update $override for every limit affecting
+    # us.
+    my $use_global_limits = 0;
+    my $apply_limit = sub {
+       my ($bwlimit) = @_;
+       if (defined($bwlimit)) {
+           my $limits = PVE::JSONSchema::parse_property_string('bwlimit', $bwlimit);
+           my $limit = $limits->{$operation} // $limits->{default};
+           if (defined($limit)) {
+               if (!$override || $limit < $override) {
+                   $override = $limit;
+               }
+               return;
+           }
+       }
+       # If there was no applicable limit, try to apply the global ones.
+       $use_global_limits = 1;
+    };
+
+    my ($rpcenv, $authuser);
+    if (defined($override)) {
+       $rpcenv = PVE::RPCEnvironment->get();
+       $authuser = $rpcenv->get_user();
+    }
+
+    # Apply per-storage limits - if there are storages involved.
+    if (defined($storage_list) && grep { defined($_) } $storage_list->@*) {
+       my $config = config();
+
+       # The Datastore.Allocate permission allows us to modify the per-storage
+       # limits, therefore it also allows us to override them.
+       # Since we have most likely multiple storages to check, do a quick check on
+       # the general '/storage' path to see if we can skip the checks entirely:
+       return $override if $rpcenv && $rpcenv->check($authuser, '/storage', ['Datastore.Allocate'], 1);
+
+       my %done;
+       foreach my $storage (@$storage_list) {
+           next if !defined($storage);
+           # Avoid duplicate checks:
+           next if $done{$storage};
+           $done{$storage} = 1;
+
+           # Otherwise we may still have individual /storage/$ID permissions:
+           if (!$rpcenv || !$rpcenv->check($authuser, "/storage/$storage", ['Datastore.Allocate'], 1)) {
+               # And if not: apply the limits.
+               my $storecfg = storage_config($config, $storage);
+               $apply_limit->($storecfg->{bwlimit});
+           }
+       }
+
+       # Storage limits take precedence over the datacenter defaults, so if
+       # a limit was applied:
+       return $override if !$use_global_limits;
+    }
+
+    # Sys.Modify on '/' means we can change datacenter.cfg which contains the
+    # global default limits.
+    if (!$rpcenv || !$rpcenv->check($authuser, '/', ['Sys.Modify'], 1)) {
+       # So if we cannot modify global limits, apply them to our currently
+       # requested override.
+       my $dc = cfs_read_file('datacenter.cfg');
+       $apply_limit->($dc->{bwlimit});
+    }
+
+    return $override;
+}
+
+# checks if the storage id is available and dies if not
+sub assert_sid_unused {
+    my ($sid) = @_;
+
+    my $cfg = config();
+    if (my $scfg = storage_config($cfg, $sid, 1)) {
+       die "storage ID '$sid' already defined\n";
+    }
+
+    return undef;
+}
+
+# removes leading/trailing spaces and (back)slashes completely
+# substitutes every non-ASCII-alphanumerical char with '_', except '_.-'
+sub normalize_content_filename {
+    my ($filename) = @_;
+
+    chomp $filename;
+    $filename =~ s/^.*[\/\\]//;
+    $filename =~ s/[^a-zA-Z0-9_.-]/_/g;
+
+    return $filename;
+}
+
+1;
diff --git a/src/PVE/Storage/BTRFSPlugin.pm b/src/PVE/Storage/BTRFSPlugin.pm
new file mode 100644 (file)
index 0000000..1db4e4f
--- /dev/null
@@ -0,0 +1,933 @@
+package PVE::Storage::BTRFSPlugin;
+
+use strict;
+use warnings;
+
+use base qw(PVE::Storage::Plugin);
+
+use Fcntl qw(S_ISDIR O_WRONLY O_CREAT O_EXCL);
+use File::Basename qw(basename dirname);
+use File::Path qw(mkpath);
+use IO::Dir;
+use POSIX qw(EEXIST);
+
+use PVE::Tools qw(run_command dir_glob_foreach);
+
+use PVE::Storage::DirPlugin;
+
+use constant {
+    BTRFS_FIRST_FREE_OBJECTID => 256,
+    FS_NOCOW_FL => 0x00800000,
+    FS_IOC_GETFLAGS => 0x40086602,
+    FS_IOC_SETFLAGS => 0x80086601,
+    BTRFS_MAGIC => 0x9123683e,
+};
+
+# Configuration (similar to DirPlugin)
+
+sub type {
+    return 'btrfs';
+}
+
+sub plugindata {
+    return {
+       content => [
+           {
+               images => 1,
+               rootdir => 1,
+               vztmpl => 1,
+               iso => 1,
+               backup => 1,
+               snippets => 1,
+               none => 1,
+           },
+           { images => 1, rootdir => 1 },
+       ],
+       format => [ { raw => 1, subvol => 1 }, 'raw', ],
+    };
+}
+
+sub properties {
+    return {
+       nocow => {
+           description => "Set the NOCOW flag on files."
+               . " Disables data checksumming and causes data errors to be unrecoverable from"
+               . " while allowing direct I/O. Only use this if data does not need to be any more"
+               . " safe than on a single ext4 formatted disk with no underlying raid system.",
+           type => 'boolean',
+           default => 0,
+       },
+    };
+}
+
+sub options {
+    return {
+       path => { fixed => 1 },
+       nodes => { optional => 1 },
+       shared => { optional => 1 },
+       disable => { optional => 1 },
+       maxfiles => { optional => 1 },
+       'prune-backups' => { optional => 1 },
+       'max-protected-backups' => { optional => 1 },
+       content => { optional => 1 },
+       format => { optional => 1 },
+       is_mountpoint => { optional => 1 },
+       nocow => { optional => 1 },
+       mkdir => { optional => 1 },
+       preallocation => { optional => 1 },
+       # TODO: The new variant of mkdir with  `populate` vs `create`...
+    };
+}
+
+# Storage implementation
+#
+# We use the same volume names are directory plugins, but map *raw* disk image file names into a
+# subdirectory.
+#
+# `vm-VMID-disk-ID.raw`
+#   -> `images/VMID/vm-VMID-disk-ID/disk.raw`
+#   where the `vm-VMID-disk-ID/` subdirectory is a btrfs subvolume
+
+# Reuse `DirPlugin`'s `check_config`. This simply checks for invalid paths.
+sub check_config {
+    my ($self, $sectionId, $config, $create, $skipSchemaCheck) = @_;
+    return PVE::Storage::DirPlugin::check_config($self, $sectionId, $config, $create, $skipSchemaCheck);
+}
+
+my sub getfsmagic($) {
+    my ($path) = @_;
+    # The field type sizes in `struct statfs` are defined in a rather annoying way, and we only
+    # need the first field, which is a `long` for our supported platforms.
+    # Should be moved to pve-rs, so this can be the problem of the `libc` crate ;-)
+    # Just round up and extract what we need:
+    my $buf = pack('x160');
+    if (0 != syscall(&PVE::Syscall::SYS_statfs, $path, $buf)) {
+       die "statfs on '$path' failed - $!\n";
+    }
+
+    return unpack('L!', $buf);
+}
+
+my sub assert_btrfs($) {
+    my ($path) = @_;
+    die "'$path' is not a btrfs file system\n"
+       if getfsmagic($path) != BTRFS_MAGIC;
+}
+
+sub activate_storage {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    my $path = $scfg->{path};
+    if (!defined($scfg->{mkdir}) || $scfg->{mkdir}) {
+       mkpath $path;
+    }
+
+    my $mp = PVE::Storage::DirPlugin::parse_is_mountpoint($scfg);
+    if (defined($mp) && !PVE::Storage::DirPlugin::path_is_mounted($mp, $cache->{mountdata})) {
+       die "unable to activate storage '$storeid' - directory is expected to be a mount point but"
+       ." is not mounted: '$mp'\n";
+    }
+
+    assert_btrfs($path); # only assert this stuff now, ensures $path is there and better UX
+
+    $class->SUPER::activate_storage($storeid, $scfg, $cache);
+}
+
+sub status {
+    my ($class, $storeid, $scfg, $cache) = @_;
+    return PVE::Storage::DirPlugin::status($class, $storeid, $scfg, $cache);
+}
+
+sub get_volume_attribute {
+    my ($class, $scfg, $storeid, $volname, $attribute) = @_;
+    return PVE::Storage::DirPlugin::get_volume_attribute($class, $scfg, $storeid, $volname, $attribute);
+}
+
+sub update_volume_attribute {
+    my ($class, $scfg, $storeid, $volname, $attribute, $value) = @_;
+    return PVE::Storage::DirPlugin::update_volume_attribute(
+       $class,
+       $scfg,
+       $storeid,
+       $volname,
+       $attribute,
+       $value,
+    );
+}
+
+# croak would not include the caller from within this module
+sub __error {
+    my ($msg) = @_;
+    my (undef, $f, $n) = caller(1);
+    die "$msg at $f: $n\n";
+}
+
+# Given a name (eg. `vm-VMID-disk-ID.raw`), take the part up to the format suffix as the name of
+# the subdirectory (subvolume).
+sub raw_name_to_dir($) {
+    my ($raw) = @_;
+
+    # For the subvolume directory Strip the `.<format>` suffix:
+    if ($raw =~ /^(.*)\.raw$/) {
+       return $1;
+    }
+
+    __error "internal error: bad disk name: $raw";
+}
+
+sub raw_file_to_subvol($) {
+    my ($file) = @_;
+
+    if ($file =~ m|^(.*)/disk\.raw$|) {
+       return "$1";
+    }
+
+    __error "internal error: bad raw path: $file";
+}
+
+sub filesystem_path {
+    my ($class, $scfg, $volname, $snapname) = @_;
+
+    my ($vtype, $name, $vmid, undef, undef, $isBase, $format) =
+       $class->parse_volname($volname);
+
+    my $path = $class->get_subdir($scfg, $vtype);
+
+    $path .= "/$vmid" if $vtype eq 'images';
+
+    if (defined($format) && $format eq 'raw') {
+       my $dir = raw_name_to_dir($name);
+       if ($snapname) {
+           $dir .= "\@$snapname";
+       }
+       $path .= "/$dir/disk.raw";
+    } elsif (defined($format) && $format eq 'subvol') {
+       $path .= "/$name";
+       if ($snapname) {
+           $path .= "\@$snapname";
+       }
+    } else {
+       $path .= "/$name";
+    }
+
+    return wantarray ? ($path, $vmid, $vtype) : $path;
+}
+
+sub btrfs_cmd {
+    my ($class, $cmd, $outfunc) = @_;
+
+    my $msg = '';
+    my $func;
+    if (defined($outfunc)) {
+       $func = sub {
+           my $part = &$outfunc(@_);
+           $msg .= $part if defined($part);
+       };
+    } else {
+       $func = sub { $msg .= "$_[0]\n" };
+    }
+    run_command(['btrfs', '-q', @$cmd], errmsg => 'btrfs error', outfunc => $func);
+
+    return $msg;
+}
+
+sub btrfs_get_subvol_id {
+    my ($class, $path) = @_;
+    my $info = $class->btrfs_cmd(['subvolume', 'show', '--', $path]);
+    if ($info !~ /^\s*(?:Object|Subvolume) ID:\s*(\d+)$/m) {
+       die "failed to get btrfs subvolume ID from: $info\n";
+    }
+    return $1;
+}
+
+my sub chattr : prototype($$$) {
+    my ($fh, $mask, $xor) = @_;
+
+    my $flags = pack('L!', 0);
+    ioctl($fh, FS_IOC_GETFLAGS, $flags) or die "FS_IOC_GETFLAGS failed - $!\n";
+    $flags = pack('L!', (unpack('L!', $flags) & $mask) ^ $xor);
+    ioctl($fh, FS_IOC_SETFLAGS, $flags) or die "FS_IOC_SETFLAGS failed - $!\n";
+    return 1;
+}
+
+sub create_base {
+    my ($class, $storeid, $scfg, $volname) = @_;
+
+    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) =
+       $class->parse_volname($volname);
+
+    my $newname = $name;
+    $newname =~ s/^vm-/base-/;
+
+    # If we're not working with a 'raw' file, which is the only thing that's "different" for btrfs,
+    # or a subvolume, we forward to the DirPlugin
+    if ($format ne 'raw' && $format ne 'subvol') {
+       return PVE::Storage::DirPlugin::create_base(@_);
+    }
+
+    my $path = $class->filesystem_path($scfg, $volname);
+    my $newvolname = $basename ? "$basevmid/$basename/$vmid/$newname" : "$vmid/$newname";
+    my $newpath = $class->filesystem_path($scfg, $newvolname);
+
+    my $subvol = $path;
+    my $newsubvol = $newpath;
+    if ($format eq 'raw') {
+       $subvol = raw_file_to_subvol($subvol);
+       $newsubvol = raw_file_to_subvol($newsubvol);
+    }
+
+    rename($subvol, $newsubvol)
+       || die "rename '$subvol' to '$newsubvol' failed - $!\n";
+    eval { $class->btrfs_cmd(['property', 'set', $newsubvol, 'ro', 'true']) };
+    warn $@ if $@;
+
+    return $newvolname;
+}
+
+sub clone_image {
+    my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
+
+    my ($vtype, $basename, $basevmid, undef, undef, $isBase, $format) =
+       $class->parse_volname($volname);
+
+    # If we're not working with a 'raw' file, which is the only thing that's "different" for btrfs,
+    # or a subvolume, we forward to the DirPlugin
+    if ($format ne 'raw' && $format ne 'subvol') {
+       return PVE::Storage::DirPlugin::clone_image(@_);
+    }
+
+    my $imagedir = $class->get_subdir($scfg, 'images');
+    $imagedir .= "/$vmid";
+    mkpath $imagedir;
+
+    my $path = $class->filesystem_path($scfg, $volname);
+    my $newname = $class->find_free_diskname($storeid, $scfg, $vmid, $format, 1);
+
+    # For btrfs subvolumes we don't actually need the "link":
+    #my $newvolname = "$basevmid/$basename/$vmid/$newname";
+    my $newvolname = "$vmid/$newname";
+    my $newpath = $class->filesystem_path($scfg, $newvolname);
+
+    my $subvol = $path;
+    my $newsubvol = $newpath;
+    if ($format eq 'raw') {
+       $subvol = raw_file_to_subvol($subvol);
+       $newsubvol = raw_file_to_subvol($newsubvol);
+    }
+
+    $class->btrfs_cmd(['subvolume', 'snapshot', '--', $subvol, $newsubvol]);
+
+    return $newvolname;
+}
+
+sub alloc_image {
+    my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
+
+    if ($fmt ne 'raw' && $fmt ne 'subvol') {
+       return $class->SUPER::alloc_image($storeid, $scfg, $vmid, $fmt, $name, $size);
+    }
+
+    # From Plugin.pm:
+
+    my $imagedir = $class->get_subdir($scfg, 'images') . "/$vmid";
+
+    mkpath $imagedir;
+
+    $name = $class->find_free_diskname($storeid, $scfg, $vmid, $fmt, 1) if !$name;
+
+    my (undef, $tmpfmt) = PVE::Storage::Plugin::parse_name_dir($name);
+
+    die "illegal name '$name' - wrong extension for format ('$tmpfmt != '$fmt')\n"
+       if $tmpfmt ne $fmt;
+
+    # End copy from Plugin.pm
+
+    my $subvol = "$imagedir/$name";
+    # .raw is not part of the directory name
+    $subvol =~ s/\.raw$//;
+
+    die "disk image '$subvol' already exists\n" if -e $subvol;
+
+    my $path;
+    if ($fmt eq 'raw') {
+       $path = "$subvol/disk.raw";
+    }
+
+    if ($fmt eq 'subvol' && !!$size) {
+       # NOTE: `btrfs send/recv` actually drops quota information so supporting subvolumes with
+       # quotas doesn't play nice with send/recv.
+       die "btrfs quotas are currently not supported, use an unsized subvolume or a raw file\n";
+    }
+
+    $class->btrfs_cmd(['subvolume', 'create', '--', $subvol]);
+
+    eval {
+       if ($fmt eq 'subvol') {
+           # Nothing to do for now...
+
+           # This is how we *would* do it:
+           # # Use the subvol's default 0/$id qgroup
+           # eval {
+           #     # This call should happen at storage creation instead and therefore governed by a
+           #     # configuration option!
+           #     # $class->btrfs_cmd(['quota', 'enable', $subvol]);
+           #     my $id = $class->btrfs_get_subvol_id($subvol);
+           #     $class->btrfs_cmd(['qgroup', 'limit', "${size}k", "0/$id", $subvol]);
+           # };
+       } elsif ($fmt eq 'raw') {
+           sysopen my $fh, $path, O_WRONLY | O_CREAT | O_EXCL
+               or die "failed to create raw file '$path' - $!\n";
+           chattr($fh, ~FS_NOCOW_FL, FS_NOCOW_FL) if $scfg->{nocow};
+           truncate($fh, $size * 1024)
+               or die "failed to set file size for '$path' - $!\n";
+           close($fh);
+       } else {
+           die "internal format error (format = $fmt)\n";
+       }
+    };
+
+    if (my $err = $@) {
+       eval { $class->btrfs_cmd(['subvolume', 'delete', '--', $subvol]); };
+       warn $@ if $@;
+       die $err;
+    }
+
+    return "$vmid/$name";
+}
+
+# Same as btrfsprogs does:
+my sub path_is_subvolume : prototype($) {
+    my ($path) = @_;
+    my @stat = stat($path)
+       or die "stat failed on '$path' - $!\n";
+    my ($ino, $mode) = @stat[1, 2];
+    return S_ISDIR($mode) && $ino == BTRFS_FIRST_FREE_OBJECTID;
+}
+
+my $BTRFS_VOL_REGEX = qr/((?:vm|base|subvol)-\d+-disk-\d+(?:\.subvol)?)(?:\@(\S+))$/;
+
+# Calls `$code->($volume, $name, $snapshot)` for each subvol in a directory matching our volume
+# regex.
+my sub foreach_subvol : prototype($$) {
+    my ($dir, $code) = @_;
+
+    dir_glob_foreach($dir, $BTRFS_VOL_REGEX, sub {
+       my ($volume, $name, $snapshot) = ($1, $2, $3);
+       return if !path_is_subvolume("$dir/$volume");
+       $code->($volume, $name, $snapshot);
+    })
+}
+
+sub free_image {
+    my ($class, $storeid, $scfg, $volname, $isBase, $_format) = @_;
+
+    my (undef, undef, $vmid, undef, undef, undef, $format) =
+       $class->parse_volname($volname);
+
+    if (!defined($format) || ($format ne 'subvol' && $format ne 'raw')) {
+       return $class->SUPER::free_image($storeid, $scfg, $volname, $isBase, $_format);
+    }
+
+    my $path = $class->filesystem_path($scfg, $volname);
+
+    my $subvol = $path;
+    if ($format eq 'raw') {
+       $subvol = raw_file_to_subvol($path);
+    }
+
+    my $dir = dirname($subvol);
+    my $basename = basename($subvol);
+    my @snapshot_vols;
+    foreach_subvol($dir, sub {
+       my ($volume, $name, $snapshot) = @_;
+       return if $name ne $basename;
+       return if !defined $snapshot;
+       push @snapshot_vols, "$dir/$volume";
+    });
+
+    $class->btrfs_cmd(['subvolume', 'delete', '--', @snapshot_vols, $subvol]);
+    # try to cleanup directory to not clutter storage with empty $vmid dirs if
+    # all images from a guest got deleted
+    rmdir($dir);
+
+    return undef;
+}
+
+# Currently not used because quotas clash with send/recv.
+# my sub btrfs_subvol_quota {
+#     my ($class, $path) = @_;
+#     my $id = '0/' . $class->btrfs_get_subvol_id($path);
+#     my $search = qr/^\Q$id\E\s+(\d)+\s+\d+\s+(\d+)\s*$/;
+#     my ($used, $size);
+#     $class->btrfs_cmd(['qgroup', 'show', '--raw', '-rf', '--', $path], sub {
+#      return if defined($size);
+#      if ($_[0] =~ $search) {
+#          ($used, $size) = ($1, $2);
+#      }
+#     });
+#     if (!defined($size)) {
+#      # syslog should include more information:
+#      syslog('err', "failed to get subvolume size for: $path (id $id)");
+#      # UI should only see the last path component:
+#      $path =~ s|^.*/||;
+#      die "failed to get subvolume size for $path\n";
+#     }
+#     return wantarray ? ($used, $size) : $size;
+# }
+
+sub volume_size_info {
+    my ($class, $scfg, $storeid, $volname, $timeout) = @_;
+
+    my $path = $class->filesystem_path($scfg, $volname);
+
+    my $format = ($class->parse_volname($volname))[6];
+
+    if (defined($format) && $format eq 'subvol') {
+       my $ctime = (stat($path))[10];
+       my ($used, $size) = (0, 0);
+       #my ($used, $size) = btrfs_subvol_quota($class, $path); # uses wantarray
+       return wantarray ? ($size, 'subvol', $used, undef, $ctime) : 1;
+    }
+
+    return PVE::Storage::Plugin::file_size_info($path, $timeout);
+}
+
+sub volume_resize {
+    my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
+
+    my $format = ($class->parse_volname($volname))[6];
+    if ($format eq 'subvol') {
+       my $path = $class->filesystem_path($scfg, $volname);
+       my $id = '0/' . $class->btrfs_get_subvol_id($path);
+       $class->btrfs_cmd(['qgroup', 'limit', '--', "${size}k", "0/$id", $path]);
+       return undef;
+    }
+
+    return PVE::Storage::Plugin::volume_resize(@_);
+}
+
+sub volume_snapshot {
+    my ($class, $scfg, $storeid, $volname, $snap) = @_;
+
+    my ($name, $vmid, $format) = ($class->parse_volname($volname))[1,2,6];
+    if ($format ne 'subvol' && $format ne 'raw') {
+       return PVE::Storage::Plugin::volume_snapshot(@_);
+    }
+
+    my $path = $class->filesystem_path($scfg, $volname);
+    my $snap_path = $class->filesystem_path($scfg, $volname, $snap);
+
+    if ($format eq 'raw') {
+       $path = raw_file_to_subvol($path);
+       $snap_path = raw_file_to_subvol($snap_path);
+    }
+
+    my $snapshot_dir = $class->get_subdir($scfg, 'images') . "/$vmid";
+    mkpath $snapshot_dir;
+
+    $class->btrfs_cmd(['subvolume', 'snapshot', '-r', '--', $path, $snap_path]);
+    return undef;
+}
+
+sub volume_rollback_is_possible {
+    my ($class, $scfg, $storeid, $volname, $snap, $blockers) = @_;
+
+    return 1; 
+}
+
+sub volume_snapshot_rollback {
+    my ($class, $scfg, $storeid, $volname, $snap) = @_;
+
+    my ($name, $format) = ($class->parse_volname($volname))[1,6];
+
+    if ($format ne 'subvol' && $format ne 'raw') {
+       return PVE::Storage::Plugin::volume_snapshot_rollback(@_);
+    }
+
+    my $path = $class->filesystem_path($scfg, $volname);
+    my $snap_path = $class->filesystem_path($scfg, $volname, $snap);
+
+    if ($format eq 'raw') {
+       $path = raw_file_to_subvol($path);
+       $snap_path = raw_file_to_subvol($snap_path);
+    }
+
+    # Simple version would be:
+    #   rename old to temp
+    #   create new
+    #   on error rename temp back
+    # But for atomicity in case the rename after create-failure *also* fails, we create the new
+    # subvol first, then use RENAME_EXCHANGE, 
+    my $tmp_path = "$path.tmp.$$";
+    $class->btrfs_cmd(['subvolume', 'snapshot', '--', $snap_path, $tmp_path]);
+    # The paths are absolute, so pass -1 as file descriptors.
+    my $ok = PVE::Tools::renameat2(-1, $tmp_path, -1, $path, &PVE::Tools::RENAME_EXCHANGE);
+
+    eval { $class->btrfs_cmd(['subvolume', 'delete', '--', $tmp_path]) };
+    warn "failed to remove '$tmp_path' subvolume: $@" if $@;
+
+    if (!$ok) {
+       die "failed to rotate '$tmp_path' into place at '$path' - $!\n";
+    }
+
+    return undef;
+}
+
+sub volume_snapshot_delete {
+    my ($class, $scfg, $storeid, $volname, $snap, $running) = @_;
+
+    my ($name, $vmid, $format) = ($class->parse_volname($volname))[1,2,6];
+
+    if ($format ne 'subvol' && $format ne 'raw') {
+       return PVE::Storage::Plugin::volume_snapshot_delete(@_);
+    }
+
+    my $path = $class->filesystem_path($scfg, $volname, $snap);
+
+    if ($format eq 'raw') {
+       $path = raw_file_to_subvol($path);
+    }
+
+    $class->btrfs_cmd(['subvolume', 'delete', '--', $path]);
+
+    return undef;
+}
+
+sub volume_has_feature {
+    my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
+
+    my $features = {
+       snapshot => {
+           current => { qcow2 => 1, raw => 1, subvol => 1 },
+           snap => { qcow2 => 1, raw => 1, subvol => 1 }
+       },
+       clone => {
+           base => { qcow2 => 1, raw => 1, subvol => 1, vmdk => 1 },
+           current => { raw => 1 },
+           snap => { raw => 1 },
+       },
+       template => {
+           current => { qcow2 => 1, raw => 1, vmdk => 1, subvol => 1 },
+       },
+       copy => {
+           base => { qcow2 => 1, raw => 1, subvol => 1, vmdk => 1 },
+           current => { qcow2 => 1, raw => 1, subvol => 1, vmdk => 1 },
+           snap => { qcow2 => 1, raw => 1, subvol => 1 },
+       },
+       sparseinit => {
+           base => { qcow2 => 1, raw => 1, vmdk => 1 },
+           current => { qcow2 => 1, raw => 1, vmdk => 1 },
+       },
+    };
+
+    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) = $class->parse_volname($volname);
+
+    my $key = undef;
+    if ($snapname) {
+        $key = 'snap';
+    } else {
+        $key =  $isBase ? 'base' : 'current';
+    }
+
+    return 1 if defined($features->{$feature}->{$key}->{$format});
+
+    return undef;
+}
+
+sub list_images {
+    my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
+    my $imagedir = $class->get_subdir($scfg, 'images');
+
+    my $res = [];
+
+    # Copied from Plugin.pm, with file_size_info calls adapted:
+    foreach my $fn (<$imagedir/[0-9][0-9]*/*>) {
+       # different to in Plugin.pm the regex below also excludes '@' as valid file name
+       next if $fn !~ m@^(/.+/(\d+)/([^/\@.]+(?:\.(qcow2|vmdk|subvol))?))$@;
+       $fn = $1; # untaint
+
+       my $owner = $2;
+       my $name = $3;
+       my $ext = $4;
+
+       next if !$vollist && defined($vmid) && ($owner ne $vmid);
+
+       my $volid = "$storeid:$owner/$name";
+       my ($size, $format, $used, $parent, $ctime);
+
+       if (!$ext) { # raw
+           $volid .= '.raw';
+           ($size, $format, $used, $parent, $ctime) = PVE::Storage::Plugin::file_size_info("$fn/disk.raw");
+       } elsif ($ext eq 'subvol') {
+           ($used, $size) = (0, 0);
+           #($used, $size) = btrfs_subvol_quota($class, $fn);
+           $format = 'subvol';
+       } else {
+           ($size, $format, $used, $parent, $ctime) = PVE::Storage::Plugin::file_size_info($fn);
+       }
+       next if !($format && defined($size));
+
+       if ($vollist) {
+           next if ! grep { $_ eq $volid } @$vollist;
+       }
+
+       my $info = {
+           volid => $volid, format => $format,
+           size => $size, vmid => $owner, used => $used, parent => $parent,
+       };
+
+        $info->{ctime} = $ctime if $ctime;
+
+        push @$res, $info;
+    }
+
+    return $res;
+}
+
+sub volume_export_formats {
+    my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
+
+    # We can do whatever `DirPlugin` can do.
+    my @result = PVE::Storage::Plugin::volume_export_formats(@_);
+
+    # `btrfs send` only works on snapshots:
+    return @result if !defined $snapshot;
+
+    # Incremental stream with snapshots is only supported if the snapshots are listed (new api):
+    return @result if defined($base_snapshot) && $with_snapshots && ref($with_snapshots) ne 'ARRAY';
+
+    # Otherwise we do also support `with_snapshots`.
+
+    # Finally, `btrfs send` only works on formats where we actually use btrfs subvolumes:
+    my $format = ($class->parse_volname($volname))[6];
+    return @result if $format ne 'raw' && $format ne 'subvol';
+
+    return ('btrfs', @result);
+}
+
+sub volume_import_formats {
+    my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
+
+    # Same as export-formats, beware the parameter order:
+    return volume_export_formats(
+       $class,
+       $scfg,
+       $storeid,
+       $volname,
+       $snapshot,
+       $base_snapshot,
+       $with_snapshots,
+    );
+}
+
+sub volume_export {
+    my (
+       $class,
+       $scfg,
+       $storeid,
+       $fh,
+       $volname,
+       $format,
+       $snapshot,
+       $base_snapshot,
+       $with_snapshots,
+    ) = @_;
+
+    if ($format ne 'btrfs') {
+       return PVE::Storage::Plugin::volume_export(@_);
+    }
+
+    die "format 'btrfs' only works on snapshots\n"
+       if !defined $snapshot;
+
+    die "'btrfs' format in incremental mode requires snapshots to be listed explicitly\n"
+       if defined($base_snapshot) && $with_snapshots && ref($with_snapshots) ne 'ARRAY';
+
+    my $volume_format = ($class->parse_volname($volname))[6];
+
+    die "btrfs-sending volumes of type $volume_format ('$volname') is not supported\n"
+       if $volume_format ne 'raw' && $volume_format ne 'subvol';
+
+    my $path = $class->path($scfg, $volname, $storeid);
+
+    if ($volume_format eq 'raw') {
+       $path = raw_file_to_subvol($path);
+    }
+
+    my $cmd = ['btrfs', '-q', 'send', '-e'];
+    if ($base_snapshot) {
+       my $base = $class->path($scfg, $volname, $storeid, $base_snapshot);
+       if ($volume_format eq 'raw') {
+           $base = raw_file_to_subvol($base);
+       }
+       push @$cmd, '-p', $base;
+    }
+    push @$cmd, '--';
+    if (ref($with_snapshots) eq 'ARRAY') {
+       push @$cmd, (map { "$path\@$_" } ($with_snapshots // [])->@*), $path;
+    } else {
+       dir_glob_foreach(dirname($path), $BTRFS_VOL_REGEX, sub {
+           push @$cmd, "$path\@$_[2]" if !(defined($snapshot) && $_[2] eq $snapshot);
+       });
+    }
+    $path .= "\@$snapshot" if defined($snapshot);
+    push @$cmd, $path;
+
+    run_command($cmd, output => '>&'.fileno($fh));
+    return;
+}
+
+sub volume_import {
+    my (
+       $class,
+       $scfg,
+       $storeid,
+       $fh,
+       $volname,
+       $format,
+       $snapshot,
+       $base_snapshot,
+       $with_snapshots,
+       $allow_rename,
+    ) = @_;
+
+    if ($format ne 'btrfs') {
+       return PVE::Storage::Plugin::volume_import(@_);
+    }
+
+    die "format 'btrfs' only works on snapshots\n"
+       if !defined $snapshot;
+
+    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $volume_format) =
+       $class->parse_volname($volname);
+
+    die "btrfs-receiving volumes of type $volume_format ('$volname') is not supported\n"
+       if $volume_format ne 'raw' && $volume_format ne 'subvol';
+
+    if (defined($base_snapshot)) {
+       my $path = $class->path($scfg, $volname, $storeid, $base_snapshot);
+       die "base snapshot '$base_snapshot' not found - no such directory '$path'\n"
+           if !path_is_subvolume($path);
+    }
+
+    my $destination = $class->filesystem_path($scfg, $volname);
+    if ($volume_format eq 'raw') {
+       $destination = raw_file_to_subvol($destination);
+    }
+
+    if (!defined($base_snapshot) && -e $destination) {
+       die "volume $volname already exists\n" if !$allow_rename;
+       $volname = $class->find_free_diskname($storeid, $scfg, $vmid, $volume_format, 1);
+    }
+
+    my $imagedir = $class->get_subdir($scfg, $vtype);
+    $imagedir .= "/$vmid" if $vtype eq 'images';
+
+    my $tmppath = "$imagedir/recv.$vmid.tmp";
+    mkdir($imagedir); # FIXME: if $scfg->{mkdir};
+    if (!mkdir($tmppath)) {
+       die "temp receive directory already exists at '$tmppath', incomplete concurrent import?\n"
+           if $! == EEXIST;
+       die "failed to create temporary receive directory at '$tmppath' - $!\n";
+    }
+
+    my $dh = IO::Dir->new($tmppath)
+       or die "failed to open temporary receive directory '$tmppath' - $!\n";
+    eval {
+       run_command(['btrfs', '-q', 'receive', '-e', '--', $tmppath], input => '<&'.fileno($fh));
+
+       # Analyze the received subvolumes;
+       my ($diskname, $found_snapshot, @snapshots);
+       $dh->rewind;
+       while (defined(my $entry = $dh->read)) {
+           next if $entry eq '.' || $entry eq '..';
+           next if $entry !~ /^$BTRFS_VOL_REGEX$/;
+           my ($cur_diskname, $cur_snapshot) = ($1, $2);
+
+           die "send stream included a non-snapshot subvolume\n"
+               if !defined($cur_snapshot);
+
+           if (!defined($diskname)) {
+               $diskname = $cur_diskname;
+           } else {
+               die "multiple disks contained in stream ('$diskname' vs '$cur_diskname')\n"
+                   if $diskname ne $cur_diskname;
+           }
+
+           if ($cur_snapshot eq $snapshot) {
+               $found_snapshot = 1;
+           } else {
+               push @snapshots, $cur_snapshot;
+           }
+       }
+
+       die "send stream did not contain the expected current snapshot '$snapshot'\n"
+           if !$found_snapshot;
+
+       # Rotate the disk into place, first the current state:
+       # Note that read-only subvolumes cannot be moved into different directories, but for the
+       # "current" state we also want a writable copy, so start with that:
+       $class->btrfs_cmd(['property', 'set', "$tmppath/$diskname\@$snapshot", 'ro', 'false']);
+       PVE::Tools::renameat2(
+           -1,
+           "$tmppath/$diskname\@$snapshot",
+           -1,
+           $destination,
+           &PVE::Tools::RENAME_NOREPLACE,
+       ) or die "failed to move received snapshot '$tmppath/$diskname\@$snapshot'"
+           . " into place at '$destination' - $!\n";
+
+       # Now recreate the actual snapshot:
+       $class->btrfs_cmd([
+           'subvolume',
+           'snapshot',
+           '-r',
+           '--',
+           $destination,
+           "$destination\@$snapshot",
+       ]);
+
+       # Now go through the remaining snapshots (if any)
+       foreach my $snap (@snapshots) {
+           $class->btrfs_cmd(['property', 'set', "$tmppath/$diskname\@$snap", 'ro', 'false']);
+           PVE::Tools::renameat2(
+               -1,
+               "$tmppath/$diskname\@$snap",
+               -1,
+               "$destination\@$snap",
+               &PVE::Tools::RENAME_NOREPLACE,
+           ) or die "failed to move received snapshot '$tmppath/$diskname\@$snap'"
+               . " into place at '$destination\@$snap' - $!\n";
+           eval { $class->btrfs_cmd(['property', 'set', "$destination\@$snap", 'ro', 'true']) };
+           warn "failed to make $destination\@$snap read-only - $!\n" if $@;
+       }
+    };
+    my $err = $@;
+
+    eval {
+       # Cleanup all the received snapshots we did not move into place, so we can remove the temp
+       # directory.
+       if ($dh) {
+           $dh->rewind;
+           while (defined(my $entry = $dh->read)) {
+               next if $entry eq '.' || $entry eq '..';
+               eval { $class->btrfs_cmd(['subvolume', 'delete', '--', "$tmppath/$entry"]) };
+               warn $@ if $@;
+           }
+           $dh->close; undef $dh;
+       }
+       if (!rmdir($tmppath)) {
+           warn "failed to remove temporary directory '$tmppath' - $!\n"
+       }
+    };
+    warn $@ if $@;
+    if ($err) {
+       # clean up if the directory ended up being empty after an error
+       rmdir($tmppath);
+       die $err;
+    }
+
+    return "$storeid:$volname";
+}
+
+1
diff --git a/src/PVE/Storage/CIFSPlugin.pm b/src/PVE/Storage/CIFSPlugin.pm
new file mode 100644 (file)
index 0000000..e03226d
--- /dev/null
@@ -0,0 +1,313 @@
+package PVE::Storage::CIFSPlugin;
+
+use strict;
+use warnings;
+use Net::IP;
+use PVE::Tools qw(run_command);
+use PVE::ProcFSTools;
+use File::Path;
+use PVE::Storage::Plugin;
+use PVE::JSONSchema qw(get_standard_option);
+
+use base qw(PVE::Storage::Plugin);
+
+# CIFS helper functions
+
+sub cifs_is_mounted : prototype($$) {
+    my ($scfg, $mountdata) = @_;
+
+    my ($mountpoint, $server, $share) = $scfg->@{'path', 'server', 'share'};
+    my $subdir = $scfg->{subdir} // '';
+
+    $server = "[$server]" if Net::IP::ip_is_ipv6($server);
+    my $source = "//${server}/$share$subdir";
+    $mountdata = PVE::ProcFSTools::parse_proc_mounts() if !$mountdata;
+
+    return $mountpoint if grep {
+       $_->[2] =~ /^cifs/ &&
+       $_->[0] =~ m|^\Q$source\E/?$| &&
+       $_->[1] eq $mountpoint
+    } @$mountdata;
+    return undef;
+}
+
+sub cifs_cred_file_name {
+    my ($storeid) = @_;
+    return "/etc/pve/priv/storage/${storeid}.pw";
+}
+
+sub cifs_delete_credentials {
+    my ($storeid) = @_;
+
+    if (my $cred_file = get_cred_file($storeid)) {
+       unlink($cred_file) or warn "removing cifs credientials '$cred_file' failed: $!\n";
+    }
+}
+
+sub cifs_set_credentials {
+    my ($password, $storeid) = @_;
+
+    my $cred_file = cifs_cred_file_name($storeid);
+    mkdir "/etc/pve/priv/storage";
+
+    PVE::Tools::file_set_contents($cred_file, "password=$password\n");
+
+    return $cred_file;
+}
+
+sub get_cred_file {
+    my ($storeid) = @_;
+
+    my $cred_file = cifs_cred_file_name($storeid);
+
+    if (-e $cred_file) {
+       return $cred_file;
+    }
+    return undef;
+}
+
+sub cifs_mount : prototype($$$$$) {
+    my ($scfg, $storeid, $smbver, $user, $domain) = @_;
+
+    my ($mountpoint, $server, $share) = $scfg->@{'path', 'server', 'share'};
+    my $subdir = $scfg->{subdir} // '';
+
+    $server = "[$server]" if Net::IP::ip_is_ipv6($server);
+    my $source = "//${server}/$share$subdir";
+
+    my $cmd = ['/bin/mount', '-t', 'cifs', $source, $mountpoint, '-o', 'soft', '-o'];
+
+    if (my $cred_file = get_cred_file($storeid)) {
+       push @$cmd, "username=$user", '-o', "credentials=$cred_file";
+       push @$cmd, '-o', "domain=$domain" if defined($domain);
+    } else {
+       push @$cmd, 'guest,username=guest';
+    }
+
+    push @$cmd, '-o', defined($smbver) ? "vers=$smbver" : "vers=default";
+
+    run_command($cmd, errmsg => "mount error");
+}
+
+# Configuration
+
+sub type {
+    return 'cifs';
+}
+
+sub plugindata {
+    return {
+       content => [ { images => 1, rootdir => 1, vztmpl => 1, iso => 1,
+                  backup => 1, snippets => 1}, { images => 1 }],
+       format => [ { raw => 1, qcow2 => 1, vmdk => 1 } , 'raw' ],
+    };
+}
+
+sub properties {
+    return {
+       share => {
+           description => "CIFS share.",
+           type => 'string',
+       },
+       password => {
+           description => "Password for accessing the share/datastore.",
+           type => 'string',
+           maxLength => 256,
+       },
+       domain => {
+           description => "CIFS domain.",
+           type => 'string',
+           optional => 1,
+           maxLength => 256,
+       },
+       smbversion => {
+           description => "SMB protocol version. 'default' if not set, negotiates the highest SMB2+"
+               ." version supported by both the client and server.",
+           type => 'string',
+           default => 'default',
+           enum => ['default', '2.0', '2.1', '3', '3.0', '3.11'],
+           optional => 1,
+       },
+    };
+}
+
+sub options {
+    return {
+       path => { fixed => 1 },
+       'content-dirs' => { optional => 1 },
+       server => { fixed => 1 },
+       share => { fixed => 1 },
+       subdir => { optional => 1 },
+       nodes => { optional => 1 },
+       disable => { optional => 1 },
+       maxfiles => { optional => 1 },
+       'prune-backups' => { optional => 1 },
+       'max-protected-backups' => { optional => 1 },
+       content => { optional => 1 },
+       format => { optional => 1 },
+       username => { optional => 1 },
+       password => { optional => 1},
+       domain => { optional => 1},
+       smbversion => { optional => 1},
+       mkdir => { optional => 1 },
+       bwlimit => { optional => 1 },
+       preallocation => { optional => 1 },
+    };
+}
+
+
+sub check_config {
+    my ($class, $sectionId, $config, $create, $skipSchemaCheck) = @_;
+
+    $config->{path} = "/mnt/pve/$sectionId" if $create && !$config->{path};
+
+    return $class->SUPER::check_config($sectionId, $config, $create, $skipSchemaCheck);
+}
+
+# Storage implementation
+
+sub on_add_hook {
+    my ($class, $storeid, $scfg, %sensitive) = @_;
+
+    if (defined($sensitive{password})) {
+       cifs_set_credentials($sensitive{password}, $storeid);
+       if (!exists($scfg->{username})) {
+           warn "storage $storeid: ignoring password parameter, no user set\n";
+       }
+    } else {
+       cifs_delete_credentials($storeid);
+    }
+
+    return;
+}
+
+sub on_update_hook {
+    my ($class, $storeid, $scfg, %sensitive) = @_;
+
+    return if !exists($sensitive{password});
+
+    if (defined($sensitive{password})) {
+       cifs_set_credentials($sensitive{password}, $storeid);
+       if (!exists($scfg->{username})) {
+           warn "storage $storeid: ignoring password parameter, no user set\n";
+       }
+    } else {
+       cifs_delete_credentials($storeid);
+    }
+
+    return;
+}
+
+sub on_delete_hook {
+    my ($class, $storeid, $scfg) = @_;
+
+    cifs_delete_credentials($storeid);
+
+    return;
+}
+
+sub status {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    $cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts()
+       if !$cache->{mountdata};
+
+    return undef
+       if !cifs_is_mounted($scfg, $cache->{mountdata});
+
+    return $class->SUPER::status($storeid, $scfg, $cache);
+}
+
+sub activate_storage {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    $cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts()
+       if !$cache->{mountdata};
+
+    my $path = $scfg->{path};
+
+    if (!cifs_is_mounted($scfg, $cache->{mountdata})) {
+
+       mkpath $path if !(defined($scfg->{mkdir}) && !$scfg->{mkdir});
+
+       die "unable to activate storage '$storeid' - " .
+           "directory '$path' does not exist\n" if ! -d $path;
+
+       cifs_mount($scfg, $storeid, $scfg->{smbversion},
+           $scfg->{username}, $scfg->{domain});
+    }
+
+    $class->SUPER::activate_storage($storeid, $scfg, $cache);
+}
+
+sub deactivate_storage {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    $cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts()
+       if !$cache->{mountdata};
+
+    my $path = $scfg->{path};
+
+    if (cifs_is_mounted($scfg, $cache->{mountdata})) {
+       my $cmd = ['/bin/umount', $path];
+       run_command($cmd, errmsg => 'umount error');
+    }
+}
+
+sub check_connection {
+    my ($class, $storeid, $scfg) = @_;
+
+    my $servicename = '//'.$scfg->{server}.'/'.$scfg->{share};
+
+    my $cmd = ['/usr/bin/smbclient', $servicename, '-d', '0'];
+
+    if (defined($scfg->{smbversion}) && $scfg->{smbversion} ne 'default') {
+       # max-protocol version, so basically only relevant for smb2 vs smb3
+       push @$cmd, '-m', "smb" . int($scfg->{smbversion});
+    }
+
+    if (my $cred_file = get_cred_file($storeid)) {
+       push @$cmd, '-U', $scfg->{username}, '-A', $cred_file;
+       push @$cmd, '-W', $scfg->{domain} if defined($scfg->{domain});
+    } else {
+       push @$cmd, '-U', 'Guest','-N';
+    }
+    push @$cmd, '-c', 'echo 1 0';
+
+    my $out_str;
+    my $out = sub { $out_str .= shift };
+
+    eval { run_command($cmd, timeout => 10, outfunc => $out, errfunc => sub {}) };
+
+    if (my $err = $@) {
+       die "$out_str\n" if defined($out_str) &&
+           ($out_str =~ m/NT_STATUS_(ACCESS_DENIED|LOGON_FAILURE)/);
+       return 0;
+    }
+
+    return 1;
+}
+
+# FIXME remove on the next APIAGE reset.
+# Deprecated, use get_volume_attribute instead.
+sub get_volume_notes {
+    my $class = shift;
+    PVE::Storage::DirPlugin::get_volume_notes($class, @_);
+}
+
+# FIXME remove on the next APIAGE reset.
+# Deprecated, use update_volume_attribute instead.
+sub update_volume_notes {
+    my $class = shift;
+    PVE::Storage::DirPlugin::update_volume_notes($class, @_);
+}
+
+sub get_volume_attribute {
+    return PVE::Storage::DirPlugin::get_volume_attribute(@_);
+}
+
+sub update_volume_attribute {
+    return PVE::Storage::DirPlugin::update_volume_attribute(@_);
+}
+
+1;
diff --git a/src/PVE/Storage/CephFSPlugin.pm b/src/PVE/Storage/CephFSPlugin.pm
new file mode 100644 (file)
index 0000000..db0c2f6
--- /dev/null
@@ -0,0 +1,262 @@
+package PVE::Storage::CephFSPlugin;
+
+use strict;
+use warnings;
+
+use IO::File;
+use Net::IP;
+use File::Path;
+
+use PVE::CephConfig;
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::ProcFSTools;
+use PVE::Storage::Plugin;
+use PVE::Systemd;
+use PVE::Tools qw(run_command file_set_contents);
+
+use base qw(PVE::Storage::Plugin);
+
+sub cephfs_is_mounted {
+    my ($scfg, $storeid, $mountdata) = @_;
+
+    my $cmd_option = PVE::CephConfig::ceph_connect_option($scfg, $storeid);
+    my $configfile = $cmd_option->{ceph_conf};
+
+    my $subdir = $scfg->{subdir} // '/';
+    my $mountpoint = $scfg->{path};
+
+    $mountdata = PVE::ProcFSTools::parse_proc_mounts() if !$mountdata;
+    return $mountpoint if grep {
+       $_->[2] =~ m#^ceph|fuse\.ceph-fuse# &&
+       $_->[0] =~ m#\Q:$subdir\E$|^ceph-fuse$# &&
+       $_->[1] eq $mountpoint
+    } @$mountdata;
+
+    warn "A filesystem is already mounted on $mountpoint\n"
+       if grep { $_->[1] eq $mountpoint } @$mountdata;
+
+    return undef;
+}
+
+# FIXME: remove once it's possible to specify _netdev for fuse.ceph mounts
+sub systemd_netmount {
+    my ($where, $type, $what, $opts) = @_;
+
+# don't do default deps, systemd v241 generator produces ordering deps on both
+# local-fs(-pre) and remote-fs(-pre) targets if we use the required _netdev
+# option. Over three corners this gets us an ordering cycle on shutdown, which
+# may make shutdown hang if the random cycle breaking hits the "wrong" unit to
+# delete.
+    my $unit =  <<"EOF";
+[Unit]
+Description=${where}
+DefaultDependencies=no
+Requires=system.slice
+Wants=network-online.target
+Before=umount.target remote-fs.target
+After=systemd-journald.socket system.slice network.target -.mount remote-fs-pre.target network-online.target
+Conflicts=umount.target
+
+[Mount]
+Where=${where}
+What=${what}
+Type=${type}
+Options=${opts}
+EOF
+
+    my $unit_fn = PVE::Systemd::escape_unit($where, 1) . ".mount";
+    my $unit_path = "/run/systemd/system/$unit_fn";
+    my $daemon_needs_reload = -e $unit_path;
+
+    file_set_contents($unit_path, $unit);
+
+    run_command(['systemctl', 'daemon-reload'], errmsg => "daemon-reload error")
+       if $daemon_needs_reload;
+    run_command(['systemctl', 'start', $unit_fn], errmsg => "mount error");
+
+}
+
+sub cephfs_mount {
+    my ($scfg, $storeid) = @_;
+
+    my $mountpoint = $scfg->{path};
+    my $subdir = $scfg->{subdir} // '/';
+
+    my $cmd_option = PVE::CephConfig::ceph_connect_option($scfg, $storeid);
+    my $configfile = $cmd_option->{ceph_conf};
+    my $secretfile = $cmd_option->{keyring};
+    my $server = $cmd_option->{mon_host} // PVE::CephConfig::get_monaddr_list($configfile);
+    my $type = 'ceph';
+    my $fs_name = $scfg->{'fs-name'};
+
+    my @opts = ();
+    if ($scfg->{fuse}) {
+       $type = 'fuse.ceph';
+       push @opts, "ceph.id=$cmd_option->{userid}";
+       push @opts, "ceph.keyfile=$secretfile" if defined($secretfile);
+       push @opts, "ceph.conf=$configfile" if defined($configfile);
+       push @opts, "ceph.client_fs=$fs_name" if defined($fs_name);
+    } else {
+       push @opts, "name=$cmd_option->{userid}";
+       push @opts, "secretfile=$secretfile" if defined($secretfile);
+       push @opts, "conf=$configfile" if defined($configfile);
+       push @opts, "fs=$fs_name" if defined($fs_name);
+    }
+
+    push @opts, $scfg->{options} if $scfg->{options};
+
+    systemd_netmount($mountpoint, $type, "$server:$subdir", join(',', @opts));
+}
+
+# Configuration
+
+sub type {
+    return 'cephfs';
+}
+
+sub plugindata {
+    return {
+       content => [ { vztmpl => 1, iso => 1, backup => 1, snippets => 1},
+                    { backup => 1 }],
+    };
+}
+
+sub properties {
+    return {
+       fuse => {
+           description => "Mount CephFS through FUSE.",
+           type => 'boolean',
+       },
+       'fs-name' => {
+           description => "The Ceph filesystem name.",
+           type => 'string', format => 'pve-configid',
+       },
+    };
+}
+
+sub options {
+    return {
+       path => { fixed => 1 },
+       'content-dirs' => { optional => 1 },
+       monhost => { optional => 1},
+       nodes => { optional => 1 },
+       subdir => { optional => 1 },
+       disable => { optional => 1 },
+       options => { optional => 1 },
+       username => { optional => 1 },
+       content => { optional => 1 },
+       format => { optional => 1 },
+       mkdir => { optional => 1 },
+       fuse => { optional => 1 },
+       bwlimit => { optional => 1 },
+       maxfiles => { optional => 1 },
+       keyring => { optional => 1 },
+       'prune-backups' => { optional => 1 },
+       'max-protected-backups' => { optional => 1 },
+       'fs-name' => { optional => 1 },
+    };
+}
+
+sub check_config {
+    my ($class, $sectionId, $config, $create, $skipSchemaCheck) = @_;
+
+    $config->{path} = "/mnt/pve/$sectionId" if $create && !$config->{path};
+
+    return $class->SUPER::check_config($sectionId, $config, $create, $skipSchemaCheck);
+}
+
+# Storage implementation
+
+sub on_add_hook {
+    my ($class, $storeid, $scfg, %param) = @_;
+
+    PVE::CephConfig::ceph_create_keyfile($scfg->{type}, $storeid, $param{keyring});
+
+    return;
+}
+
+sub on_update_hook {
+    my ($class, $storeid, $scfg, %param) = @_;
+
+    if (exists($param{keyring})) {
+       if (defined($param{keyring})) {
+           PVE::CephConfig::ceph_create_keyfile($scfg->{type}, $storeid, $param{keyring});
+       } else {
+           PVE::CephConfig::ceph_remove_keyfile($scfg->{type}, $storeid);
+       }
+    }
+
+    return;
+}
+
+sub on_delete_hook {
+    my ($class, $storeid, $scfg) = @_;
+    PVE::CephConfig::ceph_remove_keyfile($scfg->{type}, $storeid);
+    return;
+}
+
+sub status {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    $cache->{mountdata} //= PVE::ProcFSTools::parse_proc_mounts();
+
+    return undef if !cephfs_is_mounted($scfg, $storeid, $cache->{mountdata});
+
+    return $class->SUPER::status($storeid, $scfg, $cache);
+}
+
+sub activate_storage {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    $cache->{mountdata} //= PVE::ProcFSTools::parse_proc_mounts();
+
+    # NOTE: mkpath may hang if storage is mounted but not reachable
+    if (!cephfs_is_mounted($scfg, $storeid, $cache->{mountdata})) {
+       my $path = $scfg->{path};
+
+       mkpath $path if !(defined($scfg->{mkdir}) && !$scfg->{mkdir});
+
+       die "unable to activate storage '$storeid' - " .
+           "directory '$path' does not exist\n" if ! -d $path;
+
+       cephfs_mount($scfg, $storeid);
+    }
+
+    $class->SUPER::activate_storage($storeid, $scfg, $cache);
+}
+
+sub deactivate_storage {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    $cache->{mountdata} //= PVE::ProcFSTools::parse_proc_mounts();
+
+    my $path = $scfg->{path};
+
+    if (cephfs_is_mounted($scfg, $storeid, $cache->{mountdata})) {
+       run_command(['/bin/umount', $path], errmsg => 'umount error');
+    }
+}
+
+# FIXME remove on the next APIAGE reset.
+# Deprecated, use get_volume_attribute instead.
+sub get_volume_notes {
+    my $class = shift;
+    PVE::Storage::DirPlugin::get_volume_notes($class, @_);
+}
+
+# FIXME remove on the next APIAGE reset.
+# Deprecated, use update_volume_attribute instead.
+sub update_volume_notes {
+    my $class = shift;
+    PVE::Storage::DirPlugin::update_volume_notes($class, @_);
+}
+
+sub get_volume_attribute {
+    return PVE::Storage::DirPlugin::get_volume_attribute(@_);
+}
+
+sub update_volume_attribute {
+    return PVE::Storage::DirPlugin::update_volume_attribute(@_);
+}
+
+1;
diff --git a/src/PVE/Storage/DirPlugin.pm b/src/PVE/Storage/DirPlugin.pm
new file mode 100644 (file)
index 0000000..9e305f5
--- /dev/null
@@ -0,0 +1,239 @@
+package PVE::Storage::DirPlugin;
+
+use strict;
+use warnings;
+
+use Cwd;
+use Encode qw(decode encode);
+use File::Path;
+use IO::File;
+use POSIX;
+
+use PVE::Storage::Plugin;
+use PVE::JSONSchema qw(get_standard_option);
+
+use base qw(PVE::Storage::Plugin);
+
+# Configuration
+
+sub type {
+    return 'dir';
+}
+
+sub plugindata {
+    return {
+       content => [ { images => 1, rootdir => 1, vztmpl => 1, iso => 1, backup => 1, snippets => 1, none => 1 },
+                    { images => 1,  rootdir => 1 }],
+       format => [ { raw => 1, qcow2 => 1, vmdk => 1, subvol => 1 } , 'raw' ],
+    };
+}
+
+sub properties {
+    return {
+       path => {
+           description => "File system path.",
+           type => 'string', format => 'pve-storage-path',
+       },
+       mkdir => {
+           description => "Create the directory if it doesn't exist.",
+           type => 'boolean',
+           default => 'yes',
+       },
+       is_mountpoint => {
+           description =>
+               "Assume the given path is an externally managed mountpoint " .
+               "and consider the storage offline if it is not mounted. ".
+               "Using a boolean (yes/no) value serves as a shortcut to using the target path in this field.",
+           type => 'string',
+           default => 'no',
+       },
+       bwlimit => get_standard_option('bwlimit'),
+    };
+}
+
+sub options {
+    return {
+       path => { fixed => 1 },
+       'content-dirs' => { optional => 1 },
+       nodes => { optional => 1 },
+       shared => { optional => 1 },
+       disable => { optional => 1 },
+       maxfiles => { optional => 1 },
+       'prune-backups' => { optional => 1 },
+       'max-protected-backups' => { optional => 1 },
+       content => { optional => 1 },
+       format => { optional => 1 },
+       mkdir => { optional => 1 },
+       is_mountpoint => { optional => 1 },
+       bwlimit => { optional => 1 },
+       preallocation => { optional => 1 },
+   };
+}
+
+# Storage implementation
+#
+
+# NOTE: should ProcFSTools::is_mounted accept an optional cache like this?
+sub path_is_mounted {
+    my ($mountpoint, $mountdata) = @_;
+
+    $mountpoint = Cwd::realpath($mountpoint); # symlinks
+    return 0 if !defined($mountpoint); # path does not exist
+
+    $mountdata = PVE::ProcFSTools::parse_proc_mounts() if !$mountdata;
+    return 1 if grep { $_->[1] eq $mountpoint } @$mountdata;
+    return undef;
+}
+
+sub parse_is_mountpoint {
+    my ($scfg) = @_;
+    my $is_mp = $scfg->{is_mountpoint};
+    return undef if !defined $is_mp;
+    if (defined(my $bool = PVE::JSONSchema::parse_boolean($is_mp))) {
+       return $bool ? $scfg->{path} : undef;
+    }
+    return $is_mp; # contains a path
+}
+
+# FIXME move into 'get_volume_attribute' when removing 'get_volume_notes'
+my $get_volume_notes_impl = sub {
+    my ($class, $scfg, $storeid, $volname, $timeout) = @_;
+
+    my ($vtype) = $class->parse_volname($volname);
+    return if $vtype ne 'backup';
+
+    my $path = $class->filesystem_path($scfg, $volname);
+    $path .= $class->SUPER::NOTES_EXT;
+
+    if (-f $path) {
+       my $data = PVE::Tools::file_get_contents($path);
+       return eval { decode('UTF-8', $data, 1) } // $data;
+    }
+
+    return '';
+};
+
+# FIXME remove on the next APIAGE reset.
+# Deprecated, use get_volume_attribute instead.
+sub get_volume_notes {
+    my ($class, $scfg, $storeid, $volname, $timeout) = @_;
+    return $get_volume_notes_impl->($class, $scfg, $storeid, $volname, $timeout);
+}
+
+# FIXME move into 'update_volume_attribute' when removing 'update_volume_notes'
+my $update_volume_notes_impl = sub {
+    my ($class, $scfg, $storeid, $volname, $notes, $timeout) = @_;
+
+    my ($vtype) = $class->parse_volname($volname);
+    die "only backups can have notes\n" if $vtype ne 'backup';
+
+    my $path = $class->filesystem_path($scfg, $volname);
+    $path .= $class->SUPER::NOTES_EXT;
+
+    if (defined($notes) && $notes ne '') {
+       my $encoded = encode('UTF-8', $notes);
+       PVE::Tools::file_set_contents($path, $encoded);
+    } else {
+       unlink $path or $! == ENOENT or die "could not delete notes - $!\n";
+    }
+    return;
+};
+
+# FIXME remove on the next APIAGE reset.
+# Deprecated, use update_volume_attribute instead.
+sub update_volume_notes {
+    my ($class, $scfg, $storeid, $volname, $notes, $timeout) = @_;
+    return $update_volume_notes_impl->($class, $scfg, $storeid, $volname, $notes, $timeout);
+}
+
+sub get_volume_attribute {
+    my ($class, $scfg, $storeid, $volname, $attribute) = @_;
+
+    if ($attribute eq 'notes') {
+       return $get_volume_notes_impl->($class, $scfg, $storeid, $volname);
+    }
+
+    my ($vtype) = $class->parse_volname($volname);
+    return if $vtype ne 'backup';
+
+    if ($attribute eq 'protected') {
+       my $path = $class->filesystem_path($scfg, $volname);
+       return -e PVE::Storage::protection_file_path($path) ? 1 : 0;
+    }
+
+    return;
+}
+
+sub update_volume_attribute {
+    my ($class, $scfg, $storeid, $volname, $attribute, $value) = @_;
+
+    if ($attribute eq 'notes') {
+       return $update_volume_notes_impl->($class, $scfg, $storeid, $volname, $value);
+    }
+
+    my ($vtype) = $class->parse_volname($volname);
+    die "only backups support attribute '$attribute'\n" if $vtype ne 'backup';
+
+    if ($attribute eq 'protected') {
+       my $path = $class->filesystem_path($scfg, $volname);
+       my $protection_path = PVE::Storage::protection_file_path($path);
+
+       return if !((-e $protection_path) xor $value); # protection status already correct
+
+       if ($value) {
+           my $fh = IO::File->new($protection_path, O_CREAT, 0644)
+               or die "unable to create protection file '$protection_path' - $!\n";
+           close($fh);
+       } else {
+           unlink $protection_path or $! == ENOENT
+               or die "could not delete protection file '$protection_path' - $!\n";
+       }
+
+       return;
+    }
+
+    die "attribute '$attribute' is not supported for storage type '$scfg->{type}'\n";
+}
+
+sub status {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    if (defined(my $mp = parse_is_mountpoint($scfg))) {
+       $cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts()
+           if !$cache->{mountdata};
+
+       return undef if !path_is_mounted($mp, $cache->{mountdata});
+    }
+
+    return $class->SUPER::status($storeid, $scfg, $cache);
+}
+
+
+sub activate_storage {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    my $path = $scfg->{path};
+    if (!defined($scfg->{mkdir}) || $scfg->{mkdir}) {
+       mkpath $path;
+    }
+
+    my $mp = parse_is_mountpoint($scfg);
+    if (defined($mp) && !path_is_mounted($mp, $cache->{mountdata})) {
+       die "unable to activate storage '$storeid' - " .
+           "directory is expected to be a mount point but is not mounted: '$mp'\n";
+    }
+
+    $class->SUPER::activate_storage($storeid, $scfg, $cache);
+}
+
+sub check_config {
+    my ($self, $sectionId, $config, $create, $skipSchemaCheck) = @_;
+    my $opts = PVE::SectionConfig::check_config($self, $sectionId, $config, $create, $skipSchemaCheck);
+    return $opts if !$create;
+    if ($opts->{path} !~ m@^/[-/a-zA-Z0-9_.]+$@) {
+       die "illegal path for directory storage: $opts->{path}\n";
+    }
+    return $opts;
+}
+
+1;
diff --git a/src/PVE/Storage/GlusterfsPlugin.pm b/src/PVE/Storage/GlusterfsPlugin.pm
new file mode 100644 (file)
index 0000000..ad386d2
--- /dev/null
@@ -0,0 +1,354 @@
+package PVE::Storage::GlusterfsPlugin;
+
+use strict;
+use warnings;
+use IO::File;
+use File::Path;
+use PVE::Tools qw(run_command);
+use PVE::ProcFSTools;
+use PVE::Network;
+use PVE::Storage::Plugin;
+use PVE::JSONSchema qw(get_standard_option);
+
+use base qw(PVE::Storage::Plugin);
+
+# Glusterfs helper functions
+
+my $server_test_results = {};
+
+my $get_active_server = sub {
+    my ($scfg, $return_default_if_offline) = @_;
+
+    my $defaultserver = $scfg->{server} ? $scfg->{server} : 'localhost';
+
+    if ($return_default_if_offline && !defined($scfg->{server2})) {
+       # avoid delays (there is no backup server anyways)
+       return $defaultserver;
+    }
+
+    my $serverlist = [ $defaultserver ];
+    push @$serverlist, $scfg->{server2} if $scfg->{server2};
+
+    my $ctime = time();
+    foreach my $server (@$serverlist) {
+       my $stat = $server_test_results->{$server};
+       return $server if $stat && $stat->{active} && (($ctime - $stat->{time}) <= 2); 
+    }
+
+    foreach my $server (@$serverlist) {
+       my $status = 0;
+
+       if ($server && $server ne 'localhost' && $server ne '127.0.0.1' && $server ne '::1') {
+           # ping the gluster daemon default port (24007) as heuristic
+           $status = PVE::Network::tcp_ping($server, 24007, 2);
+
+       } else {
+
+           my $parser = sub {
+               my $line = shift;
+
+               if ($line =~ m/Status: Started$/) {
+                   $status = 1;
+               }
+           };
+
+           my $cmd = ['/usr/sbin/gluster', 'volume', 'info', $scfg->{volume}];
+
+           run_command($cmd, errmsg => "glusterfs error", errfunc => sub {}, outfunc => $parser);
+       }
+
+       $server_test_results->{$server} = { time => time(), active => $status };
+       return $server if $status;
+    }
+
+    return $defaultserver if $return_default_if_offline;
+
+    return undef;
+};
+
+sub glusterfs_is_mounted {
+    my ($volume, $mountpoint, $mountdata) = @_;
+
+    $mountdata = PVE::ProcFSTools::parse_proc_mounts() if !$mountdata;
+
+    return $mountpoint if grep {
+       $_->[2] eq 'fuse.glusterfs' &&
+       $_->[0] =~ /^\S+:\Q$volume\E$/ &&
+       $_->[1] eq $mountpoint
+    } @$mountdata;
+    return undef;
+}
+
+sub glusterfs_mount {
+    my ($server, $volume, $mountpoint) = @_;
+
+    my $source = "$server:$volume";
+
+    my $cmd = ['/bin/mount', '-t', 'glusterfs', $source, $mountpoint];
+
+    run_command($cmd, errmsg => "mount error");
+}
+
+# Configuration
+
+sub type {
+    return 'glusterfs';
+}
+
+sub plugindata {
+    return {
+       content => [ { images => 1, vztmpl => 1, iso => 1, backup => 1, snippets => 1},
+                    { images => 1 }],
+       format => [ { raw => 1, qcow2 => 1, vmdk => 1 } , 'raw' ],
+    };
+}
+
+sub properties {
+    return {
+       volume => {
+           description => "Glusterfs Volume.",
+           type => 'string',
+       },
+       server2 => {
+           description => "Backup volfile server IP or DNS name.",
+           type => 'string', format => 'pve-storage-server',
+           requires => 'server',
+       },
+       transport => {
+           description => "Gluster transport: tcp or rdma",
+           type => 'string',
+           enum => ['tcp', 'rdma', 'unix'],
+       },
+    };
+}
+
+sub options {
+    return {
+       path => { fixed => 1 },
+       server => { optional => 1 },
+       server2 => { optional => 1 },
+       volume => { fixed => 1 },
+       transport => { optional => 1 },
+       nodes => { optional => 1 },
+       disable => { optional => 1 },
+       maxfiles => { optional => 1 },
+       'prune-backups' => { optional => 1 },
+       'max-protected-backups' => { optional => 1 },
+       content => { optional => 1 },
+       format => { optional => 1 },
+       mkdir => { optional => 1 },
+       bwlimit => { optional => 1 },
+       preallocation => { optional => 1 },
+    };
+}
+
+
+sub check_config {
+    my ($class, $sectionId, $config, $create, $skipSchemaCheck) = @_;
+
+    $config->{path} = "/mnt/pve/$sectionId" if $create && !$config->{path};
+
+    return $class->SUPER::check_config($sectionId, $config, $create, $skipSchemaCheck);
+}
+
+# Storage implementation
+
+sub parse_name_dir {
+    my $name = shift;
+
+    if ($name =~ m!^((base-)?[^/\s]+\.(raw|qcow2|vmdk))$!) {
+        return ($1, $3, $2);
+    }
+
+    die "unable to parse volume filename '$name'\n";
+}
+
+sub path {
+    my ($class, $scfg, $volname, $storeid, $snapname) = @_;
+
+    my ($vtype, $name, $vmid, undef, undef, $isBase, $format) =
+       $class->parse_volname($volname);
+
+    # Note: qcow2/qed has internal snapshot, so path is always
+    # the same (with or without snapshot => same file).
+    die "can't snapshot this image format\n" 
+       if defined($snapname) && $format !~ m/^(qcow2|qed)$/;
+
+    my $path = undef;
+    if ($vtype eq 'images') {
+
+       my $server = &$get_active_server($scfg, 1);
+       my $glustervolume = $scfg->{volume};
+       my $transport = $scfg->{transport};
+       my $protocol = "gluster";
+
+       if ($transport) {
+           $protocol = "gluster+$transport";
+       }
+
+       $path = "$protocol://$server/$glustervolume/images/$vmid/$name";
+
+    } else {
+       my $dir = $class->get_subdir($scfg, $vtype);
+       $path = "$dir/$name";
+    }
+
+    return wantarray ? ($path, $vmid, $vtype) : $path;
+}
+
+sub clone_image {
+    my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
+
+    die "storage definition has no path\n" if !$scfg->{path};
+
+    my ($vtype, $basename, $basevmid, undef, undef, $isBase, $format) =
+       $class->parse_volname($volname);
+
+    die "clone_image on wrong vtype '$vtype'\n" if $vtype ne 'images';
+
+    die "this storage type does not support clone_image on snapshot\n" if $snap;
+
+    die "this storage type does not support clone_image on subvolumes\n" if $format eq 'subvol';
+
+    die "clone_image only works on base images\n" if !$isBase;
+
+    my $imagedir = $class->get_subdir($scfg, 'images');
+    $imagedir .= "/$vmid";
+
+    mkpath $imagedir;
+
+    my $name = $class->find_free_diskname($storeid, $scfg, $vmid, "qcow2", 1);
+
+    warn "clone $volname: $vtype, $name, $vmid to $name (base=../$basevmid/$basename)\n";
+
+    my $path = "$imagedir/$name";
+
+    die "disk image '$path' already exists\n" if -e $path;
+
+    my $server = &$get_active_server($scfg, 1);
+    my $glustervolume = $scfg->{volume};
+    my $volumepath = "gluster://$server/$glustervolume/images/$vmid/$name";
+
+    my $cmd = ['/usr/bin/qemu-img', 'create', '-b', "../$basevmid/$basename",
+              '-F', $format, '-f', 'qcow2', $volumepath];
+
+    run_command($cmd, errmsg => "unable to create image");
+
+    return "$basevmid/$basename/$vmid/$name";
+}
+
+sub alloc_image {
+    my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
+
+    my $imagedir = $class->get_subdir($scfg, 'images');
+    $imagedir .= "/$vmid";
+
+    mkpath $imagedir;
+
+    $name = $class->find_free_diskname($storeid, $scfg, $vmid, $fmt, 1) if !$name;
+
+    my (undef, $tmpfmt) = parse_name_dir($name);
+
+    die "illegal name '$name' - wrong extension for format ('$tmpfmt != '$fmt')\n"
+        if $tmpfmt ne $fmt;
+
+    my $path = "$imagedir/$name";
+
+    die "disk image '$path' already exists\n" if -e $path;
+
+    my $server = &$get_active_server($scfg, 1);
+    my $glustervolume = $scfg->{volume};
+    my $volumepath = "gluster://$server/$glustervolume/images/$vmid/$name";
+
+    my $cmd = ['/usr/bin/qemu-img', 'create'];
+
+    my $prealloc_opt = PVE::Storage::Plugin::preallocation_cmd_option($scfg, $fmt);
+    push @$cmd, '-o', $prealloc_opt if defined($prealloc_opt);
+
+    push @$cmd, '-f', $fmt, $volumepath, "${size}K";
+
+    eval { run_command($cmd, errmsg => "unable to create image"); };
+    if ($@) {
+       unlink $path;
+       rmdir $imagedir;
+       die "$@";
+    }
+
+    return "$vmid/$name";
+}
+
+sub status {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    $cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts()
+       if !$cache->{mountdata};
+
+    my $path = $scfg->{path};
+
+    my $volume = $scfg->{volume};
+
+    return undef if !glusterfs_is_mounted($volume, $path, $cache->{mountdata});
+
+    return $class->SUPER::status($storeid, $scfg, $cache);
+}
+
+sub activate_storage {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    $cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts()
+       if !$cache->{mountdata};
+
+    my $path = $scfg->{path};
+    my $volume = $scfg->{volume};
+
+    if (!glusterfs_is_mounted($volume, $path, $cache->{mountdata})) {
+       
+       mkpath $path if !(defined($scfg->{mkdir}) && !$scfg->{mkdir});
+
+       die "unable to activate storage '$storeid' - " .
+           "directory '$path' does not exist\n" if ! -d $path;
+
+       my $server = &$get_active_server($scfg, 1);
+
+       glusterfs_mount($server, $volume, $path);
+    }
+
+    $class->SUPER::activate_storage($storeid, $scfg, $cache);
+}
+
+sub deactivate_storage {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    $cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts()
+       if !$cache->{mountdata};
+
+    my $path = $scfg->{path};
+    my $volume = $scfg->{volume};
+
+    if (glusterfs_is_mounted($volume, $path, $cache->{mountdata})) {
+       my $cmd = ['/bin/umount', $path];
+       run_command($cmd, errmsg => 'umount error');
+    }
+}
+
+sub activate_volume {
+    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
+
+    # do nothing by default
+}
+
+sub deactivate_volume {
+    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
+
+    # do nothing by default
+}
+
+sub check_connection {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    my $server = &$get_active_server($scfg);
+
+    return defined($server) ? 1 : 0;
+}
+
+1;
diff --git a/src/PVE/Storage/ISCSIDirectPlugin.pm b/src/PVE/Storage/ISCSIDirectPlugin.pm
new file mode 100644 (file)
index 0000000..eb329d4
--- /dev/null
@@ -0,0 +1,255 @@
+package PVE::Storage::ISCSIDirectPlugin;
+
+use strict;
+use warnings;
+use IO::File;
+use HTTP::Request;
+use LWP::UserAgent;
+use PVE::Tools qw(run_command file_read_firstline trim dir_glob_regex dir_glob_foreach);
+use PVE::Storage::Plugin;
+use PVE::JSONSchema qw(get_standard_option);
+
+use base qw(PVE::Storage::Plugin);
+
+sub iscsi_ls {
+    my ($scfg, $storeid) = @_;
+
+    my $portal = $scfg->{portal};
+    my $cmd = ['/usr/bin/iscsi-ls', '-s', 'iscsi://'.$portal ];
+    my $list = {};
+    my %unittobytes = (
+       "k"  => 1024,
+       "M" => 1024*1024,
+       "G" => 1024*1024*1024,
+       "T"   => 1024*1024*1024*1024
+    );
+    eval {
+
+           run_command($cmd, errmsg => "iscsi error", errfunc => sub {}, outfunc => sub {
+               my $line = shift;
+               $line = trim($line);
+               if( $line =~ /Lun:(\d+)\s+([A-Za-z0-9\-\_\.\:]*)\s+\(Size:([0-9\.]*)(k|M|G|T)\)/ ) {
+                   my $image = "lun".$1;
+                   my $size = $3;
+                   my $unit = $4;
+                               
+                   $list->{$storeid}->{$image} = {
+                       name => $image,
+                       size => $size * $unittobytes{$unit},
+                       format => 'raw',
+                   };
+               }
+           });
+    };
+
+    my $err = $@;
+    die $err if $err && $err !~ m/TESTUNITREADY failed with SENSE KEY/ ;
+
+    return $list;
+
+}
+
+# Configuration
+
+sub type {
+    return 'iscsidirect';
+}
+
+sub plugindata {
+    return {
+       content => [ {images => 1, none => 1}, { images => 1 }],
+       select_existing => 1,
+    };
+}
+
+sub options {
+    return {
+        portal => { fixed => 1 },
+        target => { fixed => 1 },
+        nodes => { optional => 1},
+        disable => { optional => 1},
+        content => { optional => 1},
+        bwlimit => { optional => 1 },
+    };
+}
+
+# Storage implementation
+
+sub parse_volname {
+    my ($class, $volname) = @_;
+
+
+    if ($volname =~ m/^lun(\d+)$/) {
+       return ('images', $1, undef, undef, undef, undef, 'raw');
+    }
+
+    die "unable to parse iscsi volume name '$volname'\n";
+
+}
+
+sub path {
+    my ($class, $scfg, $volname, $storeid, $snapname) = @_;
+
+    die "volume snapshot is not possible on iscsi device"
+       if defined($snapname);
+
+    my ($vtype, $lun, $vmid) = $class->parse_volname($volname);
+
+    my $target = $scfg->{target};
+    my $portal = $scfg->{portal};
+
+    my $path = "iscsi://$portal/$target/$lun";
+
+    return ($path, $vmid, $vtype);
+}
+
+sub create_base {
+    my ($class, $storeid, $scfg, $volname) = @_;
+
+    die "can't create base images in iscsi storage\n";
+}
+
+sub clone_image {
+    my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
+
+    die "can't clone images in iscsi storage\n";
+}
+
+sub alloc_image {
+    my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
+
+    die "can't allocate space in iscsi storage\n";
+}
+
+sub free_image {
+    my ($class, $storeid, $scfg, $volname, $isBase) = @_;
+
+    die "can't free space in iscsi storage\n";
+}
+
+
+sub list_images {
+    my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
+
+    my $res = [];
+
+    $cache->{directiscsi} = iscsi_ls($scfg,$storeid) if !$cache->{directiscsi};
+
+    # we have no owner for iscsi devices
+
+    my $target = $scfg->{target};
+
+    if (my $dat = $cache->{directiscsi}->{$storeid}) {
+
+        foreach my $volname (keys %$dat) {
+
+            my $volid = "$storeid:$volname";
+
+            if ($vollist) {
+                my $found = grep { $_ eq $volid } @$vollist;
+                next if !$found;
+            } else {
+                # we have no owner for iscsi devices
+                next if defined($vmid);
+            }
+
+            my $info = $dat->{$volname};
+            $info->{volid} = $volid;
+
+            push @$res, $info;
+        }
+    }
+
+    return $res;
+}
+
+
+sub status {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    my $total = 0;
+    my $free = 0;
+    my $used = 0;
+    my $active = 1;
+    return ($total,$free,$used,$active);
+
+    return undef;
+}
+
+sub activate_storage {
+    my ($class, $storeid, $scfg, $cache) = @_;
+    return 1;
+}
+
+sub deactivate_storage {
+    my ($class, $storeid, $scfg, $cache) = @_;
+    return 1;
+}
+
+sub activate_volume {
+    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
+
+    die "volume snapshot is not possible on iscsi device" if $snapname;
+
+    return 1;
+}
+
+sub deactivate_volume {
+    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
+
+    die "volume snapshot is not possible on iscsi device" if $snapname;
+
+    return 1;
+}
+
+sub volume_size_info {
+    my ($class, $scfg, $storeid, $volname, $timeout) = @_;
+
+    my $vollist = iscsi_ls($scfg,$storeid);
+    my $info = $vollist->{$storeid}->{$volname};
+
+    return wantarray ? ($info->{size}, 'raw', 0, undef) : $info->{size};
+}
+
+sub volume_resize {
+    my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
+    die "volume resize is not possible on iscsi device";
+}
+
+sub volume_snapshot {
+    my ($class, $scfg, $storeid, $volname, $snap) = @_;
+    die "volume snapshot is not possible on iscsi device";
+}
+
+sub volume_snapshot_rollback {
+    my ($class, $scfg, $storeid, $volname, $snap) = @_;
+    die "volume snapshot rollback is not possible on iscsi device";
+}
+
+sub volume_snapshot_delete {
+    my ($class, $scfg, $storeid, $volname, $snap) = @_;
+    die "volume snapshot delete is not possible on iscsi device";
+}
+
+sub volume_has_feature {
+    my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
+    
+    my $features = {
+       copy => { current => 1},
+    };
+
+    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
+       $class->parse_volname($volname);
+
+    my $key = undef;
+    if($snapname){
+       $key = 'snap';
+    }else{
+       $key =  $isBase ? 'base' : 'current';
+    }
+    return 1 if $features->{$feature}->{$key};
+
+    return undef;
+}
+
+1;
diff --git a/src/PVE/Storage/ISCSIPlugin.pm b/src/PVE/Storage/ISCSIPlugin.pm
new file mode 100644 (file)
index 0000000..a79fcb0
--- /dev/null
@@ -0,0 +1,437 @@
+package PVE::Storage::ISCSIPlugin;
+
+use strict;
+use warnings;
+
+use File::stat;
+use IO::Dir;
+use IO::File;
+
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::Storage::Plugin;
+use PVE::Tools qw(run_command file_read_firstline trim dir_glob_regex dir_glob_foreach $IPV4RE $IPV6RE);
+
+use base qw(PVE::Storage::Plugin);
+
+# iscsi helper function
+
+my $ISCSIADM = '/usr/bin/iscsiadm';
+$ISCSIADM = undef if ! -X $ISCSIADM;
+
+sub check_iscsi_support {
+    my $noerr = shift;
+
+    if (!$ISCSIADM) {
+       my $msg = "no iscsi support - please install open-iscsi";
+       if ($noerr) {
+           warn "warning: $msg\n";
+           return 0;
+       }
+
+       die "error: $msg\n";
+    }
+
+    return 1;
+}
+
+sub iscsi_session_list {
+
+    check_iscsi_support ();
+
+    my $cmd = [$ISCSIADM, '--mode', 'session'];
+
+    my $res = {};
+
+    eval {
+       run_command($cmd, errmsg => 'iscsi session scan failed', outfunc => sub {
+           my $line = shift;
+
+           if ($line =~ m/^tcp:\s+\[(\S+)\]\s+\S+\s+(\S+)(\s+\S+)?\s*$/) {
+               my ($session, $target) = ($1, $2);
+               # there can be several sessions per target (multipath)
+               push @{$res->{$target}}, $session;
+           }
+       });
+    };
+    if (my $err = $@) {
+       die $err if $err !~ m/: No active sessions.$/i;
+    }
+
+    return $res;
+}
+
+sub iscsi_test_portal {
+    my ($portal) = @_;
+
+    my ($server, $port) = PVE::Tools::parse_host_and_port($portal);
+    return 0 if !$server;
+    return PVE::Network::tcp_ping($server, $port || 3260, 2);
+}
+
+sub iscsi_discovery {
+    my ($portal) = @_;
+
+    check_iscsi_support ();
+
+    my $res = {};
+    return $res if !iscsi_test_portal($portal); # fixme: raise exception here?
+
+    my $cmd = [$ISCSIADM, '--mode', 'discovery', '--type', 'sendtargets', '--portal', $portal];
+    run_command($cmd, outfunc => sub {
+       my $line = shift;
+
+       if ($line =~ m/^((?:$IPV4RE|\[$IPV6RE\]):\d+)\,\S+\s+(\S+)\s*$/) {
+           my $portal = $1;
+           my $target = $2;
+           # one target can have more than one portal (multipath).
+           push @{$res->{$target}}, $portal;
+       }
+    });
+
+    return $res;
+}
+
+sub iscsi_login {
+    my ($target, $portal_in) = @_;
+
+    check_iscsi_support();
+
+    eval { iscsi_discovery($portal_in); };
+    warn $@ if $@;
+
+    run_command([$ISCSIADM, '--mode', 'node', '--targetname',  $target, '--login']);
+}
+
+sub iscsi_logout {
+    my ($target, $portal) = @_;
+
+    check_iscsi_support();
+
+    run_command([$ISCSIADM, '--mode', 'node', '--targetname', $target, '--logout']);
+}
+
+my $rescan_filename = "/var/run/pve-iscsi-rescan.lock";
+
+sub iscsi_session_rescan {
+    my $session_list = shift;
+
+    check_iscsi_support();
+
+    my $rstat = stat($rescan_filename);
+
+    if (!$rstat) {
+       if (my $fh = IO::File->new($rescan_filename, "a")) {
+           utime undef, undef, $fh;
+           close($fh);
+       }
+    } else {
+       my $atime = $rstat->atime;
+       my $tdiff = time() - $atime;
+       # avoid frequent rescans
+       return if !($tdiff < 0 || $tdiff > 10);
+       utime undef, undef, $rescan_filename;
+    }
+
+    foreach my $session (@$session_list) {
+       my $cmd = [$ISCSIADM, '--mode', 'session', '--sid', $session, '--rescan'];
+       eval { run_command($cmd, outfunc => sub {}); };
+       warn $@ if $@;
+    }
+}
+
+sub load_stable_scsi_paths {
+
+    my $stable_paths = {};
+
+    my $stabledir = "/dev/disk/by-id";
+
+    if (my $dh = IO::Dir->new($stabledir)) {
+       foreach my $tmp (sort $dh->read) {
+           # exclude filenames with part in name (same disk but partitions)
+           # use only filenames with scsi(with multipath i have the same device
+          # with dm-uuid-mpath , dm-name and scsi in name)
+           if($tmp !~ m/-part\d+$/ && ($tmp =~ m/^scsi-/ || $tmp =~ m/^dm-uuid-mpath-/)) {
+                 my $path = "$stabledir/$tmp";
+                 my $bdevdest = readlink($path);
+                if ($bdevdest && $bdevdest =~ m|^../../([^/]+)|) {
+                    $stable_paths->{$1}=$tmp;
+                }
+          }
+       }
+       $dh->close;
+    }
+    return $stable_paths;
+}
+
+sub iscsi_device_list {
+
+    my $res = {};
+
+    my $dirname = '/sys/class/iscsi_session';
+
+    my $stable_paths = load_stable_scsi_paths();
+
+    dir_glob_foreach($dirname, 'session(\d+)', sub {
+       my ($ent, $session) = @_;
+
+       my $target = file_read_firstline("$dirname/$ent/targetname");
+       return if !$target;
+
+       my (undef, $host) = dir_glob_regex("$dirname/$ent/device", 'target(\d+):.*');
+       return if !defined($host);
+
+       dir_glob_foreach("/sys/bus/scsi/devices", "$host:" . '(\d+):(\d+):(\d+)', sub {
+           my ($tmp, $channel, $id, $lun) = @_;
+
+           my $type = file_read_firstline("/sys/bus/scsi/devices/$tmp/type");
+           return if !defined($type) || $type ne '0'; # list disks only
+
+           my $bdev;
+           if (-d "/sys/bus/scsi/devices/$tmp/block") { # newer kernels
+               (undef, $bdev) = dir_glob_regex("/sys/bus/scsi/devices/$tmp/block/", '([A-Za-z]\S*)');
+           } else {
+               (undef, $bdev) = dir_glob_regex("/sys/bus/scsi/devices/$tmp", 'block:(\S+)');
+           }
+           return if !$bdev;
+
+           #check multipath
+           if (-d "/sys/block/$bdev/holders") {
+               my $multipathdev = dir_glob_regex("/sys/block/$bdev/holders", '[A-Za-z]\S*');
+               $bdev = $multipathdev if $multipathdev;
+           }
+
+           my $blockdev = $stable_paths->{$bdev};
+           return if !$blockdev;
+
+           my $size = file_read_firstline("/sys/block/$bdev/size");
+           return if !$size;
+
+           my $volid = "$channel.$id.$lun.$blockdev";
+
+           $res->{$target}->{$volid} = {
+               'format' => 'raw',
+               'size' => int($size * 512),
+               'vmid' => 0, # not assigned to any vm
+               'channel' => int($channel),
+               'id' => int($id),
+               'lun' => int($lun),
+           };
+
+           #print "TEST: $target $session $host,$bus,$tg,$lun $blockdev\n";
+       });
+
+    });
+
+    return $res;
+}
+
+# Configuration
+
+sub type {
+    return 'iscsi';
+}
+
+sub plugindata {
+    return {
+       content => [ {images => 1, none => 1}, { images => 1 }],
+       select_existing => 1,
+    };
+}
+
+sub properties {
+    return {
+       target => {
+           description => "iSCSI target.",
+           type => 'string',
+       },
+       portal => {
+           description => "iSCSI portal (IP or DNS name with optional port).",
+           type => 'string', format => 'pve-storage-portal-dns',
+       },
+    };
+}
+
+sub options {
+    return {
+        portal => { fixed => 1 },
+        target => { fixed => 1 },
+        nodes => { optional => 1},
+       disable => { optional => 1},
+       content => { optional => 1},
+       bwlimit => { optional => 1 },
+    };
+}
+
+# Storage implementation
+
+sub parse_volname {
+    my ($class, $volname) = @_;
+
+    if ($volname =~ m!^\d+\.\d+\.\d+\.(\S+)$!) {
+       return ('images', $1, undef, undef, undef, undef, 'raw');
+    }
+
+    die "unable to parse iscsi volume name '$volname'\n";
+}
+
+sub filesystem_path {
+    my ($class, $scfg, $volname, $snapname) = @_;
+
+    die "snapshot is not possible on iscsi storage\n" if defined($snapname);
+
+    my ($vtype, $name, $vmid) = $class->parse_volname($volname);
+
+    my $path = "/dev/disk/by-id/$name";
+
+    return wantarray ? ($path, $vmid, $vtype) : $path;
+}
+
+sub create_base {
+    my ($class, $storeid, $scfg, $volname) = @_;
+
+    die "can't create base images in iscsi storage\n";
+}
+
+sub clone_image {
+    my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
+
+    die "can't clone images in iscsi storage\n";
+}
+
+sub alloc_image {
+    my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
+
+    die "can't allocate space in iscsi storage\n";
+}
+
+sub free_image {
+    my ($class, $storeid, $scfg, $volname, $isBase) = @_;
+
+    die "can't free space in iscsi storage\n";
+}
+
+# list all luns regardless of set content_types, since we need it for
+# listing in the gui and we can only have images anyway
+sub list_volumes {
+    my ($class, $storeid, $scfg, $vmid, $content_types) = @_;
+
+    my $res = $class->list_images($storeid, $scfg, $vmid);
+
+    for my $item (@$res) {
+       $item->{content} = 'images'; # we only have images
+    }
+
+    return $res;
+}
+
+sub list_images {
+    my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
+
+    my $res = [];
+
+    $cache->{iscsi_devices} = iscsi_device_list() if !$cache->{iscsi_devices};
+
+    # we have no owner for iscsi devices
+
+    my $target = $scfg->{target};
+
+    if (my $dat = $cache->{iscsi_devices}->{$target}) {
+
+       foreach my $volname (keys %$dat) {
+
+           my $volid = "$storeid:$volname";
+
+           if ($vollist) {
+               my $found = grep { $_ eq $volid } @$vollist;
+               next if !$found;
+           } else {
+               # we have no owner for iscsi devices
+               next if defined($vmid);
+           }
+
+           my $info = $dat->{$volname};
+           $info->{volid} = $volid;
+
+           push @$res, $info;
+       }
+    }
+
+    return $res;
+}
+
+sub iscsi_session {
+    my ($cache, $target) = @_;
+    $cache->{iscsi_sessions} = iscsi_session_list() if !$cache->{iscsi_sessions};
+    return $cache->{iscsi_sessions}->{$target};
+}
+
+sub status {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    my $session = iscsi_session($cache, $scfg->{target});
+    my $active = defined($session) ? 1 : 0;
+
+    return (0, 0, 0, $active);
+}
+
+sub activate_storage {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    return if !check_iscsi_support(1);
+
+    my $session = iscsi_session($cache, $scfg->{target});
+
+    if (!defined ($session)) {
+       eval { iscsi_login($scfg->{target}, $scfg->{portal}); };
+       warn $@ if $@;
+    } else {
+       # make sure we get all devices
+       iscsi_session_rescan($session);
+    }
+}
+
+sub deactivate_storage {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    return if !check_iscsi_support(1);
+
+    if (defined(iscsi_session($cache, $scfg->{target}))) {
+       iscsi_logout($scfg->{target}, $scfg->{portal});
+    }
+}
+
+sub check_connection {
+    my ($class, $storeid, $scfg) = @_;
+
+    my $portal = $scfg->{portal};
+    return iscsi_test_portal($portal);
+}
+
+sub volume_resize {
+    my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
+    die "volume resize is not possible on iscsi device";
+}
+
+sub volume_has_feature {
+    my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
+
+    my $features = {
+       copy => { current => 1},
+    };
+
+    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
+       $class->parse_volname($volname);
+
+    my $key = undef;
+    if($snapname){
+       $key = 'snap';
+    }else{
+       $key =  $isBase ? 'base' : 'current';
+    }
+    return 1 if $features->{$feature}->{$key};
+
+    return undef;
+}
+
+
+1;
diff --git a/src/PVE/Storage/LVMPlugin.pm b/src/PVE/Storage/LVMPlugin.pm
new file mode 100644 (file)
index 0000000..a706e0c
--- /dev/null
@@ -0,0 +1,741 @@
+package PVE::Storage::LVMPlugin;
+
+use strict;
+use warnings;
+
+use IO::File;
+
+use PVE::Tools qw(run_command trim);
+use PVE::Storage::Plugin;
+use PVE::JSONSchema qw(get_standard_option);
+
+use base qw(PVE::Storage::Plugin);
+
+# lvm helper functions
+
+my $ignore_no_medium_warnings = sub {
+    my $line = shift;
+    # ignore those, most of the time they're from (virtual) IPMI/iKVM devices
+    # and just spam the log..
+    if ($line !~ /open failed: No medium found/) {
+       print STDERR "$line\n";
+    }
+};
+
+sub lvm_pv_info {
+    my ($device) = @_;
+
+    die "no device specified" if !$device;
+
+    my $has_label = 0;
+
+    my $cmd = ['/usr/bin/file', '-L', '-s', $device];
+    run_command($cmd, outfunc => sub {
+       my $line = shift;
+       $has_label = 1 if $line =~ m/LVM2/;
+    });
+
+    return undef if !$has_label;
+
+    $cmd = ['/sbin/pvs', '--separator', ':', '--noheadings', '--units', 'k',
+           '--unbuffered', '--nosuffix', '--options',
+           'pv_name,pv_size,vg_name,pv_uuid', $device];
+
+    my $pvinfo;
+    run_command($cmd, outfunc => sub {
+       my $line = shift;
+
+       $line = trim($line);
+
+       my ($pvname, $size, $vgname, $uuid) = split(':', $line);
+
+       die "found multiple pvs entries for device '$device'\n"
+           if $pvinfo;
+
+       $pvinfo = {
+           pvname => $pvname,
+           size => int($size),
+           vgname => $vgname,
+           uuid => $uuid,
+       };
+    });
+
+    return $pvinfo;
+}
+
+sub clear_first_sector {
+    my ($dev) = shift;
+
+    if (my $fh = IO::File->new($dev, "w")) {
+       my $buf = 0 x 512;
+       syswrite $fh, $buf;
+       $fh->close();
+    }
+}
+
+sub lvm_create_volume_group {
+    my ($device, $vgname, $shared) = @_;
+
+    my $res = lvm_pv_info($device);
+
+    if ($res->{vgname}) {
+       return if $res->{vgname} eq $vgname; # already created
+       die "device '$device' is already used by volume group '$res->{vgname}'\n";
+    }
+
+    clear_first_sector($device); # else pvcreate fails
+
+    # we use --metadatasize 250k, which reseults in "pe_start = 512"
+    # so pe_start is aligned on a 128k boundary (advantage for SSDs)
+    my $cmd = ['/sbin/pvcreate', '--metadatasize', '250k', $device];
+
+    run_command($cmd, errmsg => "pvcreate '$device' error");
+
+    $cmd = ['/sbin/vgcreate', $vgname, $device];
+    # push @$cmd, '-c', 'y' if $shared; # we do not use this yet
+
+    run_command($cmd, errmsg => "vgcreate $vgname $device error", errfunc => $ignore_no_medium_warnings, outfunc => $ignore_no_medium_warnings);
+}
+
+sub lvm_destroy_volume_group {
+    my ($vgname) = @_;
+
+    run_command(
+       ['vgremove', '-y', $vgname],
+       errmsg => "unable to remove volume group $vgname",
+       errfunc => $ignore_no_medium_warnings,
+       outfunc => $ignore_no_medium_warnings,
+    );
+}
+
+sub lvm_vgs {
+    my ($includepvs) = @_;
+
+    my $cmd = ['/sbin/vgs', '--separator', ':', '--noheadings', '--units', 'b',
+              '--unbuffered', '--nosuffix', '--options'];
+
+    my $cols = [qw(vg_name vg_size vg_free lv_count)];
+
+    if ($includepvs) {
+       push @$cols, qw(pv_name pv_size pv_free);
+    }
+
+    push @$cmd, join(',', @$cols);
+
+    my $vgs = {};
+    eval {
+       run_command($cmd, outfunc => sub {
+           my $line = shift;
+           $line = trim($line);
+
+           my ($name, $size, $free, $lvcount, $pvname, $pvsize, $pvfree) = split (':', $line);
+
+           $vgs->{$name} //= {
+               size => int ($size),
+               free => int ($free),
+               lvcount => int($lvcount)
+           };
+
+           if (defined($pvname) && defined($pvsize) && defined($pvfree)) {
+               push @{$vgs->{$name}->{pvs}}, {
+                   name => $pvname,
+                   size => int($pvsize),
+                   free => int($pvfree),
+               };
+           }
+       },
+       errfunc => $ignore_no_medium_warnings,
+       );
+    };
+    my $err = $@;
+
+    # just warn (vgs return error code 5 if clvmd does not run)
+    # but output is still OK (list without clustered VGs)
+    warn $err if $err;
+
+    return $vgs;
+}
+
+sub lvm_list_volumes {
+    my ($vgname) = @_;
+
+    my $option_list = 'vg_name,lv_name,lv_size,lv_attr,pool_lv,data_percent,metadata_percent,snap_percent,uuid,tags,metadata_size,time';
+
+    my $cmd = [
+       '/sbin/lvs', '--separator', ':', '--noheadings', '--units', 'b',
+       '--unbuffered', '--nosuffix',
+       '--config', 'report/time_format="%s"',
+       '--options', $option_list,
+    ];
+
+    push @$cmd, $vgname if $vgname;
+
+    my $lvs = {};
+    run_command($cmd, outfunc => sub {
+       my $line = shift;
+
+       $line = trim($line);
+
+       my ($vg_name, $lv_name, $lv_size, $lv_attr, $pool_lv, $data_percent, $meta_percent, $snap_percent, $uuid, $tags, $meta_size, $ctime) = split(':', $line);
+       return if !$vg_name;
+       return if !$lv_name;
+
+       my $lv_type = substr($lv_attr, 0, 1);
+
+       my $d = {
+           lv_size => int($lv_size),
+           lv_state => substr($lv_attr, 4, 1),
+           lv_type => $lv_type,
+       };
+       $d->{pool_lv} = $pool_lv if $pool_lv;
+       $d->{tags} = $tags if $tags;
+       $d->{ctime} = $ctime;
+
+       if ($lv_type eq 't') {
+           $data_percent ||= 0;
+           $meta_percent ||= 0;
+           $snap_percent ||= 0;
+           $d->{metadata_size} = int($meta_size);
+           $d->{metadata_used} = int(($meta_percent * $meta_size)/100);
+           $d->{used} = int(($data_percent * $lv_size)/100);
+       }
+       $lvs->{$vg_name}->{$lv_name} = $d;
+    },
+    errfunc => $ignore_no_medium_warnings,
+    );
+
+    return $lvs;
+}
+
+# Configuration
+
+sub type {
+    return 'lvm';
+}
+
+sub plugindata {
+    return {
+       content => [ {images => 1, rootdir => 1}, { images => 1 }],
+    };
+}
+
+sub properties {
+    return {
+       vgname => {
+           description => "Volume group name.",
+           type => 'string', format => 'pve-storage-vgname',
+       },
+       base => {
+           description => "Base volume. This volume is automatically activated.",
+           type => 'string', format => 'pve-volume-id',
+       },
+       saferemove => {
+           description => "Zero-out data when removing LVs.",
+           type => 'boolean',
+       },
+       saferemove_throughput => {
+           description => "Wipe throughput (cstream -t parameter value).",
+           type => 'string',
+       },
+       tagged_only => {
+           description => "Only use logical volumes tagged with 'pve-vm-ID'.",
+           type => 'boolean',
+       }
+    };
+}
+
+sub options {
+    return {
+       vgname => { fixed => 1 },
+       nodes => { optional => 1 },
+       shared => { optional => 1 },
+       disable => { optional => 1 },
+       saferemove => { optional => 1 },
+       saferemove_throughput => { optional => 1 },
+       content => { optional => 1 },
+       base => { fixed => 1, optional => 1 },
+       tagged_only => { optional => 1 },
+       bwlimit => { optional => 1 },
+    };
+}
+
+# Storage implementation
+
+sub on_add_hook {
+    my ($class, $storeid, $scfg, %param) = @_;
+
+    if (my $base = $scfg->{base}) {
+       my ($baseid, $volname) = PVE::Storage::parse_volume_id($base);
+
+       my $cfg = PVE::Storage::config();
+       my $basecfg = PVE::Storage::storage_config ($cfg, $baseid, 1);
+       die "base storage ID '$baseid' does not exist\n" if !$basecfg;
+
+       # we only support iscsi for now
+       die "unsupported base type '$basecfg->{type}'"
+           if $basecfg->{type} ne 'iscsi';
+
+       my $path = PVE::Storage::path($cfg, $base);
+
+       PVE::Storage::activate_storage($cfg, $baseid);
+
+       lvm_create_volume_group($path, $scfg->{vgname}, $scfg->{shared});
+    }
+
+    return;
+}
+
+sub parse_volname {
+    my ($class, $volname) = @_;
+
+    PVE::Storage::Plugin::parse_lvm_name($volname);
+
+    if ($volname =~ m/^(vm-(\d+)-\S+)$/) {
+       return ('images', $1, $2, undef, undef, undef, 'raw');
+    }
+
+    die "unable to parse lvm volume name '$volname'\n";
+}
+
+sub filesystem_path {
+    my ($class, $scfg, $volname, $snapname) = @_;
+
+    die "lvm snapshot is not implemented"if defined($snapname);
+
+    my ($vtype, $name, $vmid) = $class->parse_volname($volname);
+
+    my $vg = $scfg->{vgname};
+
+    my $path = "/dev/$vg/$name";
+
+    return wantarray ? ($path, $vmid, $vtype) : $path;
+}
+
+sub create_base {
+    my ($class, $storeid, $scfg, $volname) = @_;
+
+    die "can't create base images in lvm storage\n";
+}
+
+sub clone_image {
+    my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
+
+    die "can't clone images in lvm storage\n";
+}
+
+sub find_free_diskname {
+    my ($class, $storeid, $scfg, $vmid, $fmt, $add_fmt_suffix) = @_;
+
+    my $vg = $scfg->{vgname};
+
+    my $lvs = lvm_list_volumes($vg);
+
+    my $disk_list = [ keys %{$lvs->{$vg}} ];
+
+    return PVE::Storage::Plugin::get_next_vm_diskname($disk_list, $storeid, $vmid, undef, $scfg);
+}
+
+sub lvcreate {
+    my ($vg, $name, $size, $tags) = @_;
+
+    if ($size =~ m/\d$/) { # no unit is given
+       $size .= "k"; # default to kilobytes
+    }
+
+    my $cmd = ['/sbin/lvcreate', '-aly', '-Wy', '--yes', '--size', $size, '--name', $name];
+    for my $tag (@$tags) {
+       push @$cmd, '--addtag', $tag;
+    }
+    push @$cmd, $vg;
+
+    run_command($cmd, errmsg => "lvcreate '$vg/$name' error");
+}
+
+sub lvrename {
+    my ($vg, $oldname, $newname) = @_;
+
+    run_command(
+       ['/sbin/lvrename', $vg, $oldname, $newname],
+       errmsg => "lvrename '${vg}/${oldname}' to '${newname}' error",
+    );
+}
+
+sub alloc_image {
+    my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
+
+    die "unsupported format '$fmt'" if $fmt ne 'raw';
+
+    die "illegal name '$name' - should be 'vm-$vmid-*'\n"
+       if  $name && $name !~ m/^vm-$vmid-/;
+
+    my $vgs = lvm_vgs();
+
+    my $vg = $scfg->{vgname};
+
+    die "no such volume group '$vg'\n" if !defined ($vgs->{$vg});
+
+    my $free = int($vgs->{$vg}->{free});
+
+    die "not enough free space ($free < $size)\n" if $free < $size;
+
+    $name = $class->find_free_diskname($storeid, $scfg, $vmid)
+       if !$name;
+
+    lvcreate($vg, $name, $size, ["pve-vm-$vmid"]);
+
+    return $name;
+}
+
+sub free_image {
+    my ($class, $storeid, $scfg, $volname, $isBase) = @_;
+
+    my $vg = $scfg->{vgname};
+
+    # we need to zero out LVM data for security reasons
+    # and to allow thin provisioning
+
+    my $zero_out_worker = sub {
+       print "zero-out data on image $volname (/dev/$vg/del-$volname)\n";
+
+       # wipe throughput up to 10MB/s by default; may be overwritten with saferemove_throughput
+       my $throughput = '-10485760';
+       if ($scfg->{saferemove_throughput}) {
+               $throughput = $scfg->{saferemove_throughput};
+       }
+
+       my $cmd = [
+               '/usr/bin/cstream',
+               '-i', '/dev/zero',
+               '-o', "/dev/$vg/del-$volname",
+               '-T', '10',
+               '-v', '1',
+               '-b', '1048576',
+               '-t', "$throughput"
+       ];
+       eval { run_command($cmd, errmsg => "zero out finished (note: 'No space left on device' is ok here)"); };
+       warn $@ if $@;
+
+       $class->cluster_lock_storage($storeid, $scfg->{shared}, undef, sub {
+           my $cmd = ['/sbin/lvremove', '-f', "$vg/del-$volname"];
+           run_command($cmd, errmsg => "lvremove '$vg/del-$volname' error");
+       });
+       print "successfully removed volume $volname ($vg/del-$volname)\n";
+    };
+
+    my $cmd = ['/sbin/lvchange', '-aly', "$vg/$volname"];
+    run_command($cmd, errmsg => "can't activate LV '$vg/$volname' to zero-out its data");
+    $cmd = ['/sbin/lvchange', '--refresh', "$vg/$volname"];
+    run_command($cmd, errmsg => "can't refresh LV '$vg/$volname' to zero-out its data");
+
+    if ($scfg->{saferemove}) {
+       # avoid long running task, so we only rename here
+       $cmd = ['/sbin/lvrename', $vg, $volname, "del-$volname"];
+       run_command($cmd, errmsg => "lvrename '$vg/$volname' error");
+       return $zero_out_worker;
+    } else {
+       my $tmpvg = $scfg->{vgname};
+       $cmd = ['/sbin/lvremove', '-f', "$tmpvg/$volname"];
+       run_command($cmd, errmsg => "lvremove '$tmpvg/$volname' error");
+    }
+
+    return undef;
+}
+
+my $check_tags = sub {
+    my ($tags) = @_;
+
+    return defined($tags) && $tags =~ /(^|,)pve-vm-\d+(,|$)/;
+};
+
+sub list_images {
+    my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
+
+    my $vgname = $scfg->{vgname};
+
+    $cache->{lvs} = lvm_list_volumes() if !$cache->{lvs};
+
+    my $res = [];
+
+    if (my $dat = $cache->{lvs}->{$vgname}) {
+
+       foreach my $volname (keys %$dat) {
+
+           next if $volname !~ m/^vm-(\d+)-/;
+           my $owner = $1;
+
+           my $info = $dat->{$volname};
+
+           next if $scfg->{tagged_only} && !&$check_tags($info->{tags});
+
+           # Allow mirrored and RAID LVs
+           next if $info->{lv_type} !~ m/^[-mMrR]$/;
+
+           my $volid = "$storeid:$volname";
+
+           if ($vollist) {
+               my $found = grep { $_ eq $volid } @$vollist;
+               next if !$found;
+           } else {
+               next if defined($vmid) && ($owner ne $vmid);
+           }
+
+           push @$res, {
+               volid => $volid, format => 'raw', size => $info->{lv_size}, vmid => $owner,
+               ctime => $info->{ctime},
+           };
+       }
+    }
+
+    return $res;
+}
+
+sub status {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    $cache->{vgs} = lvm_vgs() if !$cache->{vgs};
+
+    my $vgname = $scfg->{vgname};
+
+     if (my $info = $cache->{vgs}->{$vgname}) {
+       return ($info->{size}, $info->{free}, $info->{size} - $info->{free}, 1);
+    }
+
+    return undef;
+}
+
+sub activate_storage {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    $cache->{vgs} = lvm_vgs() if !$cache->{vgs};
+
+    # In LVM2, vgscans take place automatically;
+    # this is just to be sure
+    if ($cache->{vgs} && !$cache->{vgscaned} &&
+       !$cache->{vgs}->{$scfg->{vgname}}) {
+       $cache->{vgscaned} = 1;
+       my $cmd = ['/sbin/vgscan', '--ignorelockingfailure', '--mknodes'];
+       eval { run_command($cmd, outfunc => sub {}); };
+       warn $@ if $@;
+    }
+
+    # we do not acticate any volumes here ('vgchange -aly')
+    # instead, volumes are activate individually later
+}
+
+sub deactivate_storage {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    my $cmd = ['/sbin/vgchange', '-aln', $scfg->{vgname}];
+    run_command($cmd, errmsg => "can't deactivate VG '$scfg->{vgname}'");
+}
+
+sub activate_volume {
+    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
+    #fix me lvmchange is not provided on
+    my $path = $class->path($scfg, $volname, $snapname);
+
+    my $lvm_activate_mode = 'ey';
+
+    my $cmd = ['/sbin/lvchange', "-a$lvm_activate_mode", $path];
+    run_command($cmd, errmsg => "can't activate LV '$path'");
+    $cmd = ['/sbin/lvchange', '--refresh', $path];
+    run_command($cmd, errmsg => "can't refresh LV '$path' for activation");
+}
+
+sub deactivate_volume {
+    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
+
+    my $path = $class->path($scfg, $volname, $snapname);
+    return if ! -b $path;
+
+    my $cmd = ['/sbin/lvchange', '-aln', $path];
+    run_command($cmd, errmsg => "can't deactivate LV '$path'");
+}
+
+sub volume_resize {
+    my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
+
+    $size = ($size/1024/1024) . "M";
+
+    my $path = $class->path($scfg, $volname);
+    my $cmd = ['/sbin/lvextend', '-L', $size, $path];
+
+    $class->cluster_lock_storage($storeid, $scfg->{shared}, undef, sub {
+       run_command($cmd, errmsg => "error resizing volume '$path'");
+    });
+
+    return 1;
+}
+
+sub volume_size_info {
+    my ($class, $scfg, $storeid, $volname, $timeout) = @_;
+    my $path = $class->filesystem_path($scfg, $volname);
+
+    my $cmd = ['/sbin/lvs', '--separator', ':', '--noheadings', '--units', 'b',
+              '--unbuffered', '--nosuffix', '--options', 'lv_size', $path];
+
+    my $size;
+    run_command($cmd, timeout => $timeout, errmsg => "can't get size of '$path'",
+       outfunc => sub {
+           $size = int(shift);
+    });
+    return wantarray ? ($size, 'raw', 0, undef) : $size;
+}
+
+sub volume_snapshot {
+    my ($class, $scfg, $storeid, $volname, $snap) = @_;
+
+    die "lvm snapshot is not implemented";
+}
+
+sub volume_snapshot_rollback {
+    my ($class, $scfg, $storeid, $volname, $snap) = @_;
+
+    die "lvm snapshot rollback is not implemented";
+}
+
+sub volume_snapshot_delete {
+    my ($class, $scfg, $storeid, $volname, $snap) = @_;
+
+    die "lvm snapshot delete is not implemented";
+}
+
+sub volume_has_feature {
+    my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
+
+    my $features = {
+       copy => { base => 1, current => 1},
+       rename => {current => 1},
+    };
+
+    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
+       $class->parse_volname($volname);
+
+    my $key = undef;
+    if($snapname){
+       $key = 'snap';
+    }else{
+       $key =  $isBase ? 'base' : 'current';
+    }
+    return 1 if $features->{$feature}->{$key};
+
+    return undef;
+}
+
+sub volume_export_formats {
+    my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
+    return () if defined($snapshot); # lvm-thin only
+    return volume_import_formats($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots);
+}
+
+sub volume_export {
+    my ($class, $scfg, $storeid, $fh, $volname, $format, $snapshot, $base_snapshot, $with_snapshots) = @_;
+    die "volume export format $format not available for $class\n"
+       if $format ne 'raw+size';
+    die "cannot export volumes together with their snapshots in $class\n"
+       if $with_snapshots;
+    die "cannot export a snapshot in $class\n" if defined($snapshot);
+    die "cannot export an incremental stream in $class\n" if defined($base_snapshot);
+    my $file = $class->path($scfg, $volname, $storeid);
+    my $size;
+    # should be faster than querying LVM, also checks for the device file's availability
+    run_command(['/sbin/blockdev', '--getsize64', $file], outfunc => sub {
+       my ($line) = @_;
+       die "unexpected output from /sbin/blockdev: $line\n" if $line !~ /^(\d+)$/;
+       $size = int($1);
+    });
+    PVE::Storage::Plugin::write_common_header($fh, $size);
+    run_command(['dd', "if=$file", "bs=64k"], output => '>&'.fileno($fh));
+}
+
+sub volume_import_formats {
+    my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
+    return () if $with_snapshots; # not supported
+    return () if defined($base_snapshot); # not supported
+    return ('raw+size');
+}
+
+sub volume_import {
+    my ($class, $scfg, $storeid, $fh, $volname, $format, $snapshot, $base_snapshot, $with_snapshots, $allow_rename) = @_;
+    die "volume import format $format not available for $class\n"
+       if $format ne 'raw+size';
+    die "cannot import volumes together with their snapshots in $class\n"
+       if $with_snapshots;
+    die "cannot import an incremental stream in $class\n" if defined($base_snapshot);
+
+    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $file_format) =
+       $class->parse_volname($volname);
+    die "cannot import format $format into a file of format $file_format\n"
+       if $file_format ne 'raw';
+
+    my $vg = $scfg->{vgname};
+    my $lvs = lvm_list_volumes($vg);
+    if ($lvs->{$vg}->{$volname}) {
+       die "volume $vg/$volname already exists\n" if !$allow_rename;
+       warn "volume $vg/$volname already exists - importing with a different name\n";
+       $name = undef;
+    }
+
+    my ($size) = PVE::Storage::Plugin::read_common_header($fh);
+    $size = int($size/1024);
+
+    eval {
+       my $allocname = $class->alloc_image($storeid, $scfg, $vmid, 'raw', $name, $size);
+       my $oldname = $volname;
+       $volname = $allocname;
+       if (defined($name) && $allocname ne $oldname) {
+           die "internal error: unexpected allocated name: '$allocname' != '$oldname'\n";
+       }
+       my $file = $class->path($scfg, $volname, $storeid)
+           or die "internal error: failed to get path to newly allocated volume $volname\n";
+
+       $class->volume_import_write($fh, $file);
+    };
+    if (my $err = $@) {
+       my $cleanup_worker = eval { $class->free_image($storeid, $scfg, $volname, 0) };
+       warn $@ if $@;
+
+       if ($cleanup_worker) {
+           my $rpcenv = PVE::RPCEnvironment::get();
+           my $authuser = $rpcenv->get_user();
+
+           $rpcenv->fork_worker('imgdel', undef, $authuser, $cleanup_worker);
+       }
+
+       die $err;
+    }
+
+    return "$storeid:$volname";
+}
+
+sub volume_import_write {
+    my ($class, $input_fh, $output_file) = @_;
+    run_command(['dd', "of=$output_file", 'bs=64k'],
+       input => '<&'.fileno($input_fh));
+}
+
+sub rename_volume {
+    my ($class, $scfg, $storeid, $source_volname, $target_vmid, $target_volname) = @_;
+
+    my (
+       undef,
+       $source_image,
+       $source_vmid,
+       $base_name,
+       $base_vmid,
+       undef,
+       $format
+    ) = $class->parse_volname($source_volname);
+    $target_volname = $class->find_free_diskname($storeid, $scfg, $target_vmid, $format)
+       if !$target_volname;
+
+    my $vg = $scfg->{vgname};
+    my $lvs = lvm_list_volumes($vg);
+    die "target volume '${target_volname}' already exists\n"
+       if ($lvs->{$vg}->{$target_volname});
+
+    lvrename($vg, $source_volname, $target_volname);
+    return "${storeid}:${target_volname}";
+}
+
+1;
diff --git a/src/PVE/Storage/LunCmd/Comstar.pm b/src/PVE/Storage/LunCmd/Comstar.pm
new file mode 100644 (file)
index 0000000..527e4ba
--- /dev/null
@@ -0,0 +1,115 @@
+package PVE::Storage::LunCmd::Comstar;
+
+use strict;
+use warnings;
+
+use Digest::MD5 qw(md5_hex);
+use PVE::Tools qw(run_command file_read_firstline trim dir_glob_regex dir_glob_foreach);
+
+my @ssh_opts = ('-o', 'BatchMode=yes');
+my @ssh_cmd = ('/usr/bin/ssh', @ssh_opts);
+my $id_rsa_path = '/etc/pve/priv/zfs';
+
+my $get_lun_cmd_map = sub {
+    my ($method) = @_;
+
+    my $stmfadmcmd = "/usr/sbin/stmfadm";
+    my $sbdadmcmd = "/usr/sbin/sbdadm";
+
+    my $cmdmap = {
+        create_lu   => { cmd => $stmfadmcmd, method => 'create-lu' },
+        delete_lu   => { cmd => $stmfadmcmd, method => 'delete-lu' },
+        import_lu   => { cmd => $stmfadmcmd, method => 'import-lu' },
+        modify_lu   => { cmd => $stmfadmcmd, method => 'modify-lu' },
+        add_view    => { cmd => $stmfadmcmd, method => 'add-view' },
+        list_view   => { cmd => $stmfadmcmd, method => 'list-view' },
+        list_lu => { cmd => $sbdadmcmd, method => 'list-lu' },
+    };
+
+    die "unknown command '$method'" unless exists $cmdmap->{$method};
+
+    return $cmdmap->{$method};
+};
+
+sub get_base {
+    return '/dev/zvol/rdsk';
+}
+
+sub run_lun_command {
+    my ($scfg, $timeout, $method, @params) = @_;
+
+    my $msg = '';
+    my $luncmd;
+    my $target;
+    my $guid;
+    $timeout = 10 if !$timeout;
+
+    my $output = sub {
+    my $line = shift;
+    $msg .= "$line\n";
+    };
+
+    if ($method eq 'create_lu') {
+        my $wcd = 'false'; 
+        if ($scfg->{nowritecache}) {
+          $wcd = 'true';
+       }
+        my $prefix = '600144f';
+        my $digest = md5_hex($params[0]);
+        $digest =~ /(\w{7}(.*))/;
+        $guid = "$prefix$2";
+        @params = ('-p', "wcd=$wcd", '-p', "guid=$guid", @params);
+    } elsif ($method eq 'modify_lu') {
+        @params = ('-s', @params);
+    } elsif ($method eq 'list_view') {
+        @params = ('-l', @params);
+    } elsif ($method eq 'list_lu') {
+        $guid = $params[0];
+        @params = undef;
+    } elsif ($method eq 'add_view') {
+        if ($scfg->{comstar_tg}) {
+          unshift @params, $scfg->{comstar_tg};
+          unshift @params, '--target-group';
+       }
+        if ($scfg->{comstar_hg}) {
+          unshift @params, $scfg->{comstar_hg};
+          unshift @params, '--host-group';
+       }
+    }
+
+    my $cmdmap = $get_lun_cmd_map->($method);
+    $luncmd = $cmdmap->{cmd};
+    my $lunmethod = $cmdmap->{method};
+
+    $target = 'root@' . $scfg->{portal};
+
+    my $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, $luncmd, $lunmethod, @params];
+
+    run_command($cmd, outfunc => $output, timeout => $timeout);
+
+    if ($method eq 'list_view') {
+        my @lines = split /\n/, $msg;
+        $msg = undef;
+        foreach my $line (@lines) {
+            if ($line =~ /^\s*LUN\s*:\s*(\d+)$/) {
+                $msg = $1;
+                last;
+            }
+        }
+    } elsif ($method eq 'list_lu') {
+        my $object = $guid;
+        my @lines = split /\n/, $msg;
+        $msg = undef;
+        foreach my $line (@lines) {
+            if ($line =~ /(\w+)\s+\d+\s+$object$/) {
+                $msg = $1;
+                last;
+            }
+        }
+    } elsif ($method eq 'create_lu') {
+        $msg = $guid;
+    }
+
+    return $msg;
+}
+
diff --git a/src/PVE/Storage/LunCmd/Iet.pm b/src/PVE/Storage/LunCmd/Iet.pm
new file mode 100644 (file)
index 0000000..5b09b88
--- /dev/null
@@ -0,0 +1,478 @@
+package PVE::Storage::LunCmd::Iet;
+
+# iscsi storage running Debian
+# 1) apt-get install iscsitarget iscsitarget-dkms
+# 2) Create target like (/etc/iet/ietd.conf):
+# Target iqn.2001-04.com.example:tank
+#   Alias           tank
+# 3) Activate daemon (/etc/default/iscsitarget)
+# ISCSITARGET_ENABLE=true
+# 4) service iscsitarget start
+#
+# On one of the proxmox nodes:
+# 1) Login as root
+# 2) ssh-copy-id <ip_of_iscsi_storage>
+
+use strict;
+use warnings;
+
+use PVE::Tools qw(run_command file_read_firstline trim dir_glob_regex dir_glob_foreach);
+
+sub get_base;
+
+# A logical unit can max have 16864 LUNs
+# http://manpages.ubuntu.com/manpages/precise/man5/ietd.conf.5.html
+my $MAX_LUNS = 16864;
+
+my $CONFIG_FILE = '/etc/iet/ietd.conf';
+my $DAEMON = '/usr/sbin/ietadm';
+my $SETTINGS = undef;
+my $CONFIG = undef;
+my $OLD_CONFIG = undef;
+
+my @ssh_opts = ('-o', 'BatchMode=yes');
+my @ssh_cmd = ('/usr/bin/ssh', @ssh_opts);
+my @scp_cmd = ('/usr/bin/scp', @ssh_opts);
+my $id_rsa_path = '/etc/pve/priv/zfs';
+my $ietadm = '/usr/sbin/ietadm';
+
+my $execute_command = sub {
+    my ($scfg, $exec, $timeout, $method, @params) = @_;
+
+    my $msg = '';
+    my $err = undef;
+    my $target;
+    my $cmd;
+    my $res = ();
+
+    $timeout = 10 if !$timeout;
+
+    my $output = sub {
+    my $line = shift;
+    $msg .= "$line\n";
+    };
+
+    my $errfunc = sub {
+    my $line = shift;
+    $err .= "$line";
+    };
+
+    if ($exec eq 'scp') {
+        $target = 'root@[' . $scfg->{portal} . ']';
+        $cmd = [@scp_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", '--', $method, "$target:$params[0]"];
+    } else {
+        $target = 'root@' . $scfg->{portal};
+        $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, '--', $method, @params];
+    }
+
+    eval {
+        run_command($cmd, outfunc => $output, errfunc => $errfunc, timeout => $timeout);
+    };
+    if ($@) {
+        $res = {
+            result => 0,
+            msg => $err,
+        }
+    } else {
+        $res = {
+            result => 1,
+            msg => $msg,
+        }
+    }
+
+    return $res;
+};
+
+my $read_config = sub {
+    my ($scfg, $timeout) = @_;
+
+    my $msg = '';
+    my $err = undef;
+    my $luncmd = 'cat';
+    my $target;
+    $timeout = 10 if !$timeout;
+
+    my $output = sub {
+        my $line = shift;
+        $msg .= "$line\n";
+    };
+
+    my $errfunc = sub {
+        my $line = shift;
+        $err .= "$line";
+    };
+
+    $target = 'root@' . $scfg->{portal};
+
+    my $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, $luncmd, $CONFIG_FILE];
+    eval {
+        run_command($cmd, outfunc => $output, errfunc => $errfunc, timeout => $timeout);
+    };
+    if ($@) {
+        die $err if ($err !~ /No such file or directory/);
+        die "No configuration found. Install iet on $scfg->{portal}" if $msg eq '';
+    }
+
+    return $msg;
+};
+
+my $get_config = sub {
+    my ($scfg) = @_;
+    my @conf = undef;
+
+    my $config = $read_config->($scfg, undef);
+    die "Missing config file" unless $config;
+
+    $OLD_CONFIG = $config;
+
+    return $config;
+};
+
+my $parser = sub {
+    my ($scfg) = @_;
+
+    my $line = 0;
+
+    my $base = get_base;
+    my $config = $get_config->($scfg);
+    my @cfgfile = split "\n", $config;
+
+    my $cfg_target = 0;
+    foreach (@cfgfile) {
+        $line++;
+        if ($_ =~ /^\s*Target\s*([\w\-\:\.]+)\s*$/) {
+            if ($1 eq $scfg->{target} && ! $cfg_target) {
+                # start colect info
+                die "$line: Parse error [$_]" if $SETTINGS;
+                $SETTINGS->{target} = $1;
+                $cfg_target = 1;
+            } elsif ($1 eq $scfg->{target} && $cfg_target) {
+                die "$line: Parse error [$_]";
+            } elsif ($cfg_target) {
+                $cfg_target = 0;
+                $CONFIG .= "$_\n";
+            } else {
+                $CONFIG .= "$_\n";
+            }
+        } else {
+            if ($cfg_target) {
+                $SETTINGS->{text} .= "$_\n";
+                next if ($_ =~ /^\s*#/ || ! $_);
+                my $option = $_;
+                if ($_ =~ /^(\w+)\s*#/) {
+                    $option = $1;
+                }
+                if ($option =~ /^\s*(\w+)\s+(\w+)\s*$/) {
+                    if ($1 eq 'Lun') {
+                        die "$line: Parse error [$_]";
+                    }
+                    $SETTINGS->{$1} = $2;
+                } elsif ($option =~ /^\s*(\w+)\s+(\d+)\s+([\w\-\/=,]+)\s*$/) {
+                    die "$line: Parse error [$option]" unless ($1 eq 'Lun');
+                    my $conf = undef;
+                    my $num = $2;
+                    my @lun = split ',', $3;
+                    die "$line: Parse error [$option]" unless (scalar(@lun) > 1);
+                    foreach (@lun) {
+                        my @lun_opt = split '=', $_;
+                        die "$line: Parse error [$option]" unless (scalar(@lun_opt) == 2);
+                        $conf->{$lun_opt[0]} = $lun_opt[1];
+                    }
+                    if ($conf->{Path} && $conf->{Path} =~ /^$base\/$scfg->{pool}\/([\w\-]+)$/) {
+                        $conf->{include} = 1;
+                    } else {
+                        $conf->{include} = 0;
+                    }
+                    $conf->{lun} = $num;
+                    push @{$SETTINGS->{luns}}, $conf;
+                } else {
+                    die "$line: Parse error [$option]";
+                }
+            } else {
+                $CONFIG .= "$_\n";
+            }
+        }
+    }
+    $CONFIG =~ s/^\s+|\s+$|"\s*//g;
+};
+
+my $update_config = sub {
+    my ($scfg) = @_;
+    my $file = "/tmp/config$$";
+    my $config = '';
+
+    while ((my $option, my $value) = each(%$SETTINGS)) {
+        next if ($option eq 'include' || $option eq 'luns' || $option eq 'Path' || $option eq 'text' || $option eq 'used');
+        if ($option eq 'target') {
+            $config = "\n\nTarget " . $SETTINGS->{target} . "\n" . $config;
+        } else {
+            $config .= "\t$option\t\t\t$value\n";
+        }
+    }
+    foreach my $lun (@{$SETTINGS->{luns}}) {
+        my $lun_opt = '';
+        while ((my $option, my $value) = each(%$lun)) {
+            next if ($option eq 'include' || $option eq 'lun' || $option eq 'Path');
+            if ($lun_opt eq '') {
+            $lun_opt = $option . '=' . $value;
+            } else {
+                $lun_opt .= ',' . $option . '=' . $value;
+            }
+        }
+        $config .= "\tLun $lun->{lun} Path=$lun->{Path},$lun_opt\n";
+    }
+    open(my $fh, '>', $file) or die "Could not open file '$file' $!";
+
+    print $fh $CONFIG;
+    print $fh $config;
+    close $fh;
+
+    my @params = ($CONFIG_FILE);
+    my $res = $execute_command->($scfg, 'scp', undef, $file, @params);
+    unlink $file;
+
+    die $res->{msg} unless $res->{result};
+};
+
+my $get_target_tid = sub {
+    my ($scfg) = @_;
+    my $proc = '/proc/net/iet/volume';
+    my $tid = undef;
+
+    my @params = ($proc);
+    my $res = $execute_command->($scfg, 'ssh', undef, 'cat', @params);
+    die $res->{msg} unless $res->{result};
+    my @cfg = split "\n", $res->{msg};
+
+    foreach (@cfg) {
+        if ($_ =~ /^\s*tid:(\d+)\s+name:([\w\-\:\.]+)\s*$/) {
+            if ($2 && $2 eq $scfg->{target}) {
+                $tid = $1;
+                last;
+            }
+        }
+    }
+
+    return $tid;
+};
+
+my $get_lu_name = sub {
+    my $used = ();
+    my $i;
+
+    if (! exists $SETTINGS->{used}) {
+        for ($i = 0; $i < $MAX_LUNS; $i++) {
+            $used->{$i} = 0;
+        }
+        foreach my $lun (@{$SETTINGS->{luns}}) {
+            $used->{$lun->{lun}} = 1;
+        }
+        $SETTINGS->{used} = $used;
+    }
+
+    $used = $SETTINGS->{used};
+    for ($i = 0; $i < $MAX_LUNS; $i++) {
+        last unless $used->{$i};
+    }
+    $SETTINGS->{used}->{$i} = 1;
+
+    return $i;
+};
+
+my $init_lu_name = sub {
+    my $used = ();
+
+    if (! exists($SETTINGS->{used})) {
+        for (my $i = 0; $i < $MAX_LUNS; $i++) {
+            $used->{$i} = 0;
+        }
+        $SETTINGS->{used} = $used;
+    }
+    foreach my $lun (@{$SETTINGS->{luns}}) {
+        $SETTINGS->{used}->{$lun->{lun}} = 1;
+    }
+};
+
+my $free_lu_name = sub {
+    my ($lu_name) = @_;
+    my $new;
+
+    foreach my $lun (@{$SETTINGS->{luns}}) {
+        if ($lun->{lun} != $lu_name) {
+            push @$new, $lun;
+        }
+    }
+
+    $SETTINGS->{luns} = $new;
+    $SETTINGS->{used}->{$lu_name} = 0;
+};
+
+my $make_lun = sub {
+    my ($scfg, $path) = @_;
+
+    die 'Maximum number of LUNs per target is 16384' if scalar @{$SETTINGS->{luns}} >= $MAX_LUNS;
+
+    my $lun = $get_lu_name->();
+    my $conf = {
+        lun => $lun,
+        Path => $path,
+        Type => 'blockio',
+        include => 1,
+    };
+    push @{$SETTINGS->{luns}}, $conf;
+
+    return $conf;
+};
+
+my $list_view = sub {
+    my ($scfg, $timeout, $method, @params) = @_;
+    my $lun = undef;
+
+    my $object = $params[0];
+    foreach my $lun (@{$SETTINGS->{luns}}) {
+        next unless $lun->{include} == 1;
+        if ($lun->{Path} =~ /^$object$/) {
+            return $lun->{lun} if (defined($lun->{lun}));
+            die "$lun->{Path}: Missing LUN";
+        }
+    }
+
+    return $lun;
+};
+
+my $list_lun = sub {
+    my ($scfg, $timeout, $method, @params) = @_;
+    my $name = undef;
+
+    my $object = $params[0];
+    foreach my $lun (@{$SETTINGS->{luns}}) {
+        next unless $lun->{include} == 1;
+        if ($lun->{Path} =~ /^$object$/) {
+            return $lun->{Path};
+        }
+    }
+
+    return $name;
+};
+
+my $create_lun = sub {
+    my ($scfg, $timeout, $method, @params) = @_;
+
+    if ($list_lun->($scfg, $timeout, $method, @params)) {
+        die "$params[0]: LUN exists";
+    }
+    my $lun = $params[0];
+    $lun = $make_lun->($scfg, $lun);
+    my $tid = $get_target_tid->($scfg);
+    $update_config->($scfg);
+
+    my $path = "Path=$lun->{Path},Type=$lun->{Type}";
+
+    @params = ('--op', 'new', "--tid=$tid", "--lun=$lun->{lun}", '--params', $path);
+    my $res = $execute_command->($scfg, 'ssh', $timeout, $ietadm, @params);
+    do {
+        $free_lu_name->($lun->{lun});
+        $update_config->($scfg);
+        die $res->{msg};
+    } unless $res->{result};
+
+    return $res->{msg};
+};
+
+my $delete_lun = sub {
+    my ($scfg, $timeout, $method, @params) = @_;
+    my $res = {msg => undef};
+
+    my $path = $params[0];
+    my $tid = $get_target_tid->($scfg);
+
+    foreach my $lun (@{$SETTINGS->{luns}}) {
+        if ($lun->{Path} eq $path) {
+            @params = ('--op', 'delete', "--tid=$tid", "--lun=$lun->{lun}");
+            $res = $execute_command->($scfg, 'ssh', $timeout, $ietadm, @params);
+            if ($res->{result}) {
+                $free_lu_name->($lun->{lun});
+                $update_config->($scfg);
+                last;
+            } else {
+                die $res->{msg};
+            }
+        }
+    }
+
+    return $res->{msg};
+};
+
+my $import_lun = sub {
+    my ($scfg, $timeout, $method, @params) = @_;
+
+    return $create_lun->($scfg, $timeout, $method, @params);
+};
+
+my $modify_lun = sub {
+    my ($scfg, $timeout, $method, @params) = @_;
+    my $lun;
+    my $res;
+
+    my $path = $params[1];
+    my $tid = $get_target_tid->($scfg);
+
+    foreach my $cfg (@{$SETTINGS->{luns}}) {
+        if ($cfg->{Path} eq $path) {
+            $lun = $cfg;
+            last;
+        }
+    }
+
+    @params = ('--op', 'delete', "--tid=$tid", "--lun=$lun->{lun}");
+    $res = $execute_command->($scfg, 'ssh', $timeout, $ietadm, @params);
+    die $res->{msg} unless $res->{result};
+
+    $path = "Path=$lun->{Path},Type=$lun->{Type}";
+    @params = ('--op', 'new', "--tid=$tid", "--lun=$lun->{lun}", '--params', $path);
+    $res = $execute_command->($scfg, 'ssh', $timeout, $ietadm, @params);
+    die $res->{msg} unless $res->{result};
+
+    return $res->{msg};
+};
+
+my $add_view = sub {
+    my ($scfg, $timeout, $method, @params) = @_;
+
+    return '';
+};
+
+my $get_lun_cmd_map = sub {
+    my ($method) = @_;
+
+    my $cmdmap = {
+        create_lu   =>  { cmd => $create_lun },
+        delete_lu   =>  { cmd => $delete_lun },
+        import_lu   =>  { cmd => $import_lun },
+        modify_lu   =>  { cmd => $modify_lun },
+        add_view    =>  { cmd => $add_view },
+        list_view   =>  { cmd => $list_view },
+        list_lu     =>  { cmd => $list_lun },
+    };
+
+    die "unknown command '$method'" unless exists $cmdmap->{$method};
+
+    return $cmdmap->{$method};
+};
+
+sub run_lun_command {
+    my ($scfg, $timeout, $method, @params) = @_;
+
+    $parser->($scfg) unless $SETTINGS;
+    my $cmdmap = $get_lun_cmd_map->($method);
+    my $msg = $cmdmap->{cmd}->($scfg, $timeout, $method, @params);
+
+    return $msg;
+}
+
+sub get_base {
+    return '/dev';
+}
+
+1;
+
diff --git a/src/PVE/Storage/LunCmd/Istgt.pm b/src/PVE/Storage/LunCmd/Istgt.pm
new file mode 100644 (file)
index 0000000..2f758f9
--- /dev/null
@@ -0,0 +1,601 @@
+package PVE::Storage::LunCmd::Istgt;
+
+# TODO
+# Create initial target and LUN if target is missing ?
+# Create and use list of free LUNs
+
+use strict;
+use warnings;
+
+use PVE::Tools qw(run_command file_read_firstline trim dir_glob_regex dir_glob_foreach);
+
+my @CONFIG_FILES = (
+    '/usr/local/etc/istgt/istgt.conf',  # FreeBSD, FreeNAS
+    '/var/etc/iscsi/istgt.conf'         # NAS4Free
+);
+my @DAEMONS = (
+    '/usr/local/etc/rc.d/istgt',        # FreeBSD, FreeNAS
+    '/var/etc/rc.d/istgt'               # NAS4Free
+);
+
+# A logical unit can max have 63 LUNs
+# https://code.google.com/p/istgt/source/browse/src/istgt_lu.h#39
+my $MAX_LUNS = 64;
+
+my $CONFIG_FILE = undef;
+my $DAEMON = undef;
+my $SETTINGS = undef;
+my $CONFIG = undef;
+my $OLD_CONFIG = undef;
+
+my @ssh_opts = ('-o', 'BatchMode=yes');
+my @ssh_cmd = ('/usr/bin/ssh', @ssh_opts);
+my @scp_cmd = ('/usr/bin/scp', @ssh_opts);
+my $id_rsa_path = '/etc/pve/priv/zfs';
+
+#Current SIGHUP reload limitations (http://www.peach.ne.jp/archives/istgt/):
+#
+#    The parameters other than PG, IG, and LU are not reloaded by SIGHUP.
+#    LU connected by the initiator can't be reloaded by SIGHUP.
+#    PG and IG mapped to LU can't be deleted by SIGHUP.
+#    If you delete an active LU, all connections of the LU are closed by SIGHUP.
+#    Updating IG is not affected until the next login.
+#
+# FreeBSD
+# 1. Alt-F2 to change to native shell (zfsguru)
+# 2. pw mod user root -w yes (change password for root to root)
+# 3. vi /etc/ssh/sshd_config
+# 4. uncomment PermitRootLogin yes
+# 5. change PasswordAuthentication no to PasswordAuthentication yes
+# 5. /etc/rc.d/sshd restart
+# 6. On one of the proxmox nodes login as root and run: ssh-copy-id ip_freebsd_host
+# 7. vi /etc/ssh/sshd_config
+# 8. comment PermitRootLogin yes
+# 9. change PasswordAuthentication yes to PasswordAuthentication no
+# 10. /etc/rc.d/sshd restart
+# 11. Reset passwd -> pw mod user root -w no
+# 12. Alt-Ctrl-F1 to return to zfsguru shell (zfsguru)
+
+sub get_base;
+sub run_lun_command;
+
+my $read_config = sub {
+    my ($scfg, $timeout, $method) = @_;
+
+    my $msg = '';
+    my $err = undef;
+    my $luncmd = 'cat';
+    my $target;
+    $timeout = 10 if !$timeout;
+
+    my $output = sub {
+    my $line = shift;
+    $msg .= "$line\n";
+    };
+
+    my $errfunc = sub {
+    my $line = shift;
+    $err .= "$line";
+    };
+
+    $target = 'root@' . $scfg->{portal};
+
+    my $daemon = 0;
+    foreach my $config (@CONFIG_FILES) {
+        $err = undef;
+        my $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, $luncmd, $config];
+        eval {
+            run_command($cmd, outfunc => $output, errfunc => $errfunc, timeout => $timeout);
+        };
+        do {
+            $err = undef;
+            $DAEMON = $DAEMONS[$daemon];
+            $CONFIG_FILE = $config;
+            last;
+        } unless $@;
+        $daemon++;
+    }
+    die $err if ($err && $err !~ /No such file or directory/);
+    die "No configuration found. Install istgt on $scfg->{portal}" if $msg eq '';
+
+    return $msg;
+};
+
+my $get_config = sub {
+    my ($scfg) = @_;
+    my @conf = undef;
+
+    my $config = $read_config->($scfg, undef, 'get_config');
+    die "Missing config file" unless $config;
+
+    $OLD_CONFIG = $config;
+
+    return $config;
+};
+
+my $parse_size = sub {
+    my ($text) = @_;
+
+    return 0 if !$text;
+
+    if ($text =~ m/^(\d+(\.\d+)?)([TGMK]B)?$/) {
+    my ($size, $reminder, $unit) = ($1, $2, $3);
+    return $size if !$unit;
+    if ($unit eq 'KB') {
+        $size *= 1024;
+    } elsif ($unit eq 'MB') {
+        $size *= 1024*1024;
+    } elsif ($unit eq 'GB') {
+        $size *= 1024*1024*1024;
+    } elsif ($unit eq 'TB') {
+        $size *= 1024*1024*1024*1024;
+    }
+        if ($reminder) {
+            $size = ceil($size);
+        }
+        return $size;
+    } elsif ($text =~ /^auto$/i) {
+        return 'AUTO';
+    } else {
+        return 0;
+    }
+};
+
+my $size_with_unit = sub {
+    my ($size, $n) = (shift, 0);
+
+    return '0KB' if !$size;
+
+    return $size if $size eq 'AUTO';
+
+    if ($size =~ m/^\d+$/) {
+        ++$n and $size /= 1024 until $size < 1024;
+        if ($size =~ /\./) {
+            return sprintf "%.2f%s", $size, ( qw[bytes KB MB GB TB] )[ $n ];
+        } else {
+            return sprintf "%d%s", $size, ( qw[bytes KB MB GB TB] )[ $n ];
+        }
+    }
+    die "$size: Not a number";
+};
+
+my $lun_dumper = sub {
+    my ($lun) = @_;
+    my $config = '';
+
+    $config .= "\n[$lun]\n";
+    $config .=  'TargetName ' . $SETTINGS->{$lun}->{TargetName} . "\n";
+    $config .=  'Mapping ' . $SETTINGS->{$lun}->{Mapping} . "\n";
+    $config .=  'AuthGroup ' . $SETTINGS->{$lun}->{AuthGroup} . "\n";
+    $config .=  'UnitType ' . $SETTINGS->{$lun}->{UnitType} . "\n";
+    $config .=  'QueueDepth ' . $SETTINGS->{$lun}->{QueueDepth} . "\n";
+
+    foreach my $conf (@{$SETTINGS->{$lun}->{luns}}) {
+        $config .=  "$conf->{lun} Storage " . $conf->{Storage};
+        $config .= ' ' . $size_with_unit->($conf->{Size}) . "\n";
+        foreach ($conf->{options}) {
+            if ($_) {
+                $config .=  "$conf->{lun} Option " . $_ . "\n";
+            }
+        }
+    }
+    $config .= "\n";
+
+    return $config;
+};
+
+my $get_lu_name = sub {
+    my ($target) = @_;
+    my $used = ();
+    my $i;
+
+    if (! exists $SETTINGS->{$target}->{used}) {
+        for ($i = 0; $i < $MAX_LUNS; $i++) {
+            $used->{$i} = 0;
+        }
+        foreach my $lun (@{$SETTINGS->{$target}->{luns}}) {
+            $lun->{lun} =~ /^LUN(\d+)$/;
+            $used->{$1} = 1;
+        }
+        $SETTINGS->{$target}->{used} = $used;
+    }
+
+    $used = $SETTINGS->{$target}->{used};
+    for ($i = 0; $i < $MAX_LUNS; $i++) {
+        last unless $used->{$i};
+    }
+    $SETTINGS->{$target}->{used}->{$i} = 1;
+
+    return "LUN$i";
+};
+
+my $init_lu_name = sub {
+    my ($target) = @_;
+    my $used = ();
+
+    if (! exists($SETTINGS->{$target}->{used})) {
+        for (my $i = 0; $i < $MAX_LUNS; $i++) {
+            $used->{$i} = 0;
+        }
+        $SETTINGS->{$target}->{used} = $used;
+    }
+    foreach my $lun (@{$SETTINGS->{$target}->{luns}}) {
+        $lun->{lun} =~ /^LUN(\d+)$/;
+        $SETTINGS->{$target}->{used}->{$1} = 1;
+    }
+};
+
+my $free_lu_name = sub {
+    my ($target, $lu_name) = @_;
+
+    $lu_name =~ /^LUN(\d+)$/;
+    $SETTINGS->{$target}->{used}->{$1} = 0;
+};
+
+my $make_lun = sub {
+    my ($scfg, $path) = @_;
+
+    my $target = $SETTINGS->{current};
+    die 'Maximum number of LUNs per target is 63' if scalar @{$SETTINGS->{$target}->{luns}} >= $MAX_LUNS;
+
+    my @options = ();
+    my $lun = $get_lu_name->($target);
+    if ($scfg->{nowritecache}) {
+        push @options, "WriteCache Disable";
+    }
+    my $conf = {
+        lun => $lun,
+        Storage => $path,
+        Size => 'AUTO',
+        options => @options,
+    };
+    push @{$SETTINGS->{$target}->{luns}}, $conf;
+
+    return $conf->{lun};
+};
+
+my $parser = sub {
+    my ($scfg) = @_;
+
+    my $lun = undef;
+    my $line = 0;
+
+    my $config = $get_config->($scfg);
+    my @cfgfile = split "\n", $config;
+
+    foreach (@cfgfile) {
+        $line++;
+        if ($_ =~ /^\s*\[(PortalGroup\d+)\]\s*/) {
+            $lun = undef;
+            $SETTINGS->{$1} = ();
+        } elsif ($_ =~ /^\s*\[(InitiatorGroup\d+)\]\s*/) {
+            $lun = undef;
+            $SETTINGS->{$1} = ();
+        } elsif ($_ =~ /^\s*PidFile\s+"?([\w\/\.]+)"?\s*/) {
+            $lun = undef;
+            $SETTINGS->{pidfile} = $1;
+        } elsif ($_ =~ /^\s*NodeBase\s+"?([\w\-\.]+)"?\s*/) {
+            $lun = undef;
+            $SETTINGS->{nodebase} = $1;
+        } elsif ($_ =~ /^\s*\[(LogicalUnit\d+)\]\s*/) {
+            $lun = $1;
+            $SETTINGS->{$lun} = ();
+            $SETTINGS->{targets}++;
+        } elsif ($lun) {
+            next if (($_ =~ /^\s*#/) || ($_ =~ /^\s*$/));
+            if ($_ =~ /^\s*(\w+)\s+(.+)\s*/) {
+                my $arg1 = $1;
+                my $arg2 = $2;
+                $arg2 =~ s/^\s+|\s+$|"\s*//g;
+                if ($arg2 =~ /^Storage\s*(.+)/i) {
+                    $SETTINGS->{$lun}->{$arg1}->{storage} = $1;
+                } elsif ($arg2 =~ /^Option\s*(.+)/i) {
+                    push @{$SETTINGS->{$lun}->{$arg1}->{options}}, $1;
+                } else {
+                    $SETTINGS->{$lun}->{$arg1} = $arg2;
+                }
+            } else {
+                die "$line: parse error [$_]";
+            }
+        }
+        $CONFIG .= "$_\n" unless $lun;
+    }
+
+    $CONFIG =~ s/\n$//;
+    die "$scfg->{target}: Target not found" unless $SETTINGS->{targets};
+    my $max = $SETTINGS->{targets};
+    my $base = get_base;
+
+    for (my $i = 1; $i <= $max; $i++) {
+        my $target = $SETTINGS->{nodebase}.':'.$SETTINGS->{"LogicalUnit$i"}->{TargetName};
+        if ($target eq $scfg->{target}) {
+            my $lu = ();
+            while ((my $key, my $val) = each(%{$SETTINGS->{"LogicalUnit$i"}})) {
+                if ($key =~ /^LUN\d+/) {
+                    $val->{storage} =~ /^([\w\/\-]+)\s+(\w+)/;
+                    my $storage = $1;
+                    my $size = $parse_size->($2);
+                    my $conf = undef;
+                    my @options = ();
+                    if ($val->{options}) {
+                        @options = @{$val->{options}};
+                    }
+                    if ($storage =~ /^$base\/$scfg->{pool}\/([\w\-]+)$/) {
+                        $conf = {
+                            lun => $key,
+                            Storage => $storage,
+                            Size => $size,
+                            options => @options,
+                        }
+                    }
+                    push @$lu, $conf if $conf;
+                    delete $SETTINGS->{"LogicalUnit$i"}->{$key};
+                }
+            }
+            $SETTINGS->{"LogicalUnit$i"}->{luns} = $lu;
+            $SETTINGS->{current} = "LogicalUnit$i";
+            $init_lu_name->("LogicalUnit$i");
+        } else {
+            $CONFIG .= $lun_dumper->("LogicalUnit$i");
+            delete $SETTINGS->{"LogicalUnit$i"};
+            $SETTINGS->{targets}--;
+        }
+    }
+    die "$scfg->{target}: Target not found" unless $SETTINGS->{targets} > 0;
+};
+
+my $list_lun = sub {
+    my ($scfg, $timeout, $method, @params) = @_;
+    my $name = undef;
+
+    my $object = $params[0];
+    for my $key (keys %$SETTINGS)  {
+        next unless $key =~ /^LogicalUnit\d+$/;
+        foreach my $lun (@{$SETTINGS->{$key}->{luns}}) {
+            if ($lun->{Storage} =~ /^$object$/) {
+                return $lun->{Storage};
+            }
+        }
+    }
+
+    return $name;
+};
+
+my $create_lun = sub {
+    my ($scfg, $timeout, $method, @params) = @_;
+    my $res = ();
+    my $file = "/tmp/config$$";
+
+    if ($list_lun->($scfg, $timeout, $method, @params)) {
+        die "$params[0]: LUN exists";
+    }
+    my $lun = $params[0];
+    $lun = $make_lun->($scfg, $lun);
+    my $config = $lun_dumper->($SETTINGS->{current});
+    open(my $fh, '>', $file) or die "Could not open file '$file' $!";
+
+    print $fh $CONFIG;
+    print $fh $config;
+    close $fh;
+    @params = ($CONFIG_FILE);
+    $res = {
+        cmd => 'scp',
+        method => $file,
+        params => \@params,
+        msg => $lun,
+        post_exe => sub {
+            unlink $file;
+        },
+    };
+
+    return $res;
+};
+
+my $delete_lun = sub {
+    my ($scfg, $timeout, $method, @params) = @_;
+    my $res = ();
+    my $file = "/tmp/config$$";
+
+    my $target = $SETTINGS->{current};
+    my $luns = ();
+
+    foreach my $conf (@{$SETTINGS->{$target}->{luns}}) {
+        if ($conf->{Storage} =~ /^$params[0]$/) {
+            $free_lu_name->($target, $conf->{lun});
+        } else {
+            push @$luns, $conf;
+        }
+    }
+    $SETTINGS->{$target}->{luns} = $luns;
+
+    my $config = $lun_dumper->($SETTINGS->{current});
+    open(my $fh, '>', $file) or die "Could not open file '$file' $!";
+
+    print $fh $CONFIG;
+    print $fh $config;
+    close $fh;
+    @params = ($CONFIG_FILE);
+    $res = {
+        cmd => 'scp',
+        method => $file,
+        params => \@params,
+        post_exe => sub {
+            unlink $file;
+            run_lun_command($scfg, undef, 'add_view', 'restart');
+        },
+    };
+
+    return $res;
+};
+
+my $import_lun = sub {
+    my ($scfg, $timeout, $method, @params) = @_;
+
+    my $res = $create_lun->($scfg, $timeout, $method, @params);
+
+    return $res;
+};
+
+my $add_view = sub {
+    my ($scfg, $timeout, $method, @params) = @_;
+    my $cmdmap;
+
+    if (@params && $params[0] eq 'restart') {
+        @params = ('onerestart', '>&', '/dev/null');
+        $cmdmap = {
+            cmd => 'ssh',
+            method => $DAEMON,
+            params => \@params,
+        };
+    } else {
+        @params = ('-HUP', '`cat '. "$SETTINGS->{pidfile}`");
+        $cmdmap = {
+            cmd => 'ssh',
+            method => 'kill',
+            params => \@params,
+        };
+    }
+
+    return $cmdmap;
+};
+
+my $modify_lun = sub {
+    my ($scfg, $timeout, $method, @params) = @_;
+
+    # Current SIGHUP reload limitations
+    # LU connected by the initiator can't be reloaded by SIGHUP.
+    # Until above limitation persists modifying a LUN will require
+    # a restart of the daemon breaking all current connections
+    #die 'Modify a connected LUN is not currently supported by istgt';
+    @params = ('restart', @params);
+
+    return $add_view->($scfg, $timeout, $method, @params);
+};
+
+my $list_view = sub {
+    my ($scfg, $timeout, $method, @params) = @_;
+    my $lun = undef;
+
+    my $object = $params[0];
+    for my $key (keys %$SETTINGS)  {
+        next unless $key =~ /^LogicalUnit\d+$/;
+        foreach my $lun (@{$SETTINGS->{$key}->{luns}}) {
+            if ($lun->{Storage} =~ /^$object$/) {
+                if ($lun->{lun} =~ /^LUN(\d+)/) {
+                    return $1;
+                }
+                die "$lun->{Storage}: Missing LUN";
+            }
+        }
+    }
+
+    return $lun;
+};
+
+my $get_lun_cmd_map = sub {
+    my ($method) = @_;
+
+    my $cmdmap = {
+        create_lu   => { cmd => $create_lun },
+        delete_lu   => { cmd => $delete_lun },
+        import_lu   => { cmd => $import_lun },
+        modify_lu   => { cmd => $modify_lun },
+        add_view    => { cmd => $add_view },
+        list_view   => { cmd => $list_view },
+        list_lu     => { cmd => $list_lun },
+    };
+
+    die "unknown command '$method'" unless exists $cmdmap->{$method};
+
+    return $cmdmap->{$method};
+};
+
+sub run_lun_command {
+    my ($scfg, $timeout, $method, @params) = @_;
+
+    my $msg = '';
+    my $luncmd;
+    my $target;
+    my $cmd;
+    my $res;
+    $timeout = 10 if !$timeout;
+    my $is_add_view = 0;
+
+    my $output = sub {
+    my $line = shift;
+    $msg .= "$line\n";
+    };
+
+    $target = 'root@' . $scfg->{portal};
+
+    $parser->($scfg) unless $SETTINGS;
+    my $cmdmap = $get_lun_cmd_map->($method);
+    if ($method eq 'add_view') {
+        $is_add_view = 1 ;
+        $timeout = 15;
+    }
+    if (ref $cmdmap->{cmd} eq 'CODE') {
+        $res = $cmdmap->{cmd}->($scfg, $timeout, $method, @params);
+        if (ref $res) {
+            $method = $res->{method};
+            @params = @{$res->{params}};
+            if ($res->{cmd} eq 'scp') {
+                $cmd = [@scp_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $method, "$target:$params[0]"];
+            } else {
+                $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, $method, @params];
+            }
+        } else {
+            return $res;
+        }
+    } else {
+        $luncmd = $cmdmap->{cmd};
+        $method = $cmdmap->{method};
+        $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, $luncmd, $method, @params];
+    }
+
+    eval {
+        run_command($cmd, outfunc => $output, timeout => $timeout);
+    };
+    if ($@ && $is_add_view) {
+        my $err = $@;
+        if ($OLD_CONFIG) {
+            my $err1 = undef;
+            my $file = "/tmp/config$$";
+            open(my $fh, '>', $file) or die "Could not open file '$file' $!";
+            print $fh $OLD_CONFIG;
+            close $fh;
+            $cmd = [@scp_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $file, $CONFIG_FILE];
+            eval {
+                run_command($cmd, outfunc => $output, timeout => $timeout);
+            };
+            $err1 = $@ if $@;
+            unlink $file;
+            die "$err\n$err1" if $err1;
+            eval {
+                run_lun_command($scfg, undef, 'add_view', 'restart');
+            };
+            die "$err\n$@" if ($@);
+        }
+        die $err;
+    } elsif ($@) {
+        die $@;
+    } elsif ($is_add_view) {
+        $OLD_CONFIG = undef;
+    }
+
+    if ($res->{post_exe} && ref $res->{post_exe} eq 'CODE') {
+        $res->{post_exe}->();
+    }
+
+    if ($res->{msg}) {
+        $msg = $res->{msg};
+    }
+
+    return $msg;
+}
+
+sub get_base {
+    return '/dev/zvol';
+}
+
+1;
diff --git a/src/PVE/Storage/LunCmd/LIO.pm b/src/PVE/Storage/LunCmd/LIO.pm
new file mode 100644 (file)
index 0000000..9264e46
--- /dev/null
@@ -0,0 +1,420 @@
+package PVE::Storage::LunCmd::LIO;
+
+# lightly based on code from Iet.pm
+#
+# additional changes:
+# -----------------------------------------------------------------
+# Copyright (c) 2018 BestSolution.at EDV Systemhaus GmbH
+# All Rights Reserved.
+#
+# This software is released under the terms of the
+#
+#            "GNU Affero General Public License"
+#
+# and may only be distributed and used under the terms of the
+# mentioned license. You should have received a copy of the license
+# along with this software product, if not you can download it from
+# https://www.gnu.org/licenses/agpl-3.0.en.html
+#
+# Author: udo.rader@bestsolution.at
+# -----------------------------------------------------------------
+
+use strict;
+use warnings;
+use PVE::Tools qw(run_command);
+use JSON;
+
+sub get_base;
+
+# targetcli constants
+# config file location differs from distro to distro
+my @CONFIG_FILES = (
+       '/etc/rtslib-fb-target/saveconfig.json',        # Debian 9.x et al
+       '/etc/target/saveconfig.json' ,                 # ArchLinux, CentOS
+);
+my $BACKSTORE = '/backstores/block';
+
+my $SETTINGS = undef;
+my $SETTINGS_TIMESTAMP = 0;
+my $SETTINGS_MAXAGE = 15; # in seconds
+
+my @ssh_opts = ('-o', 'BatchMode=yes');
+my @ssh_cmd = ('/usr/bin/ssh', @ssh_opts);
+my $id_rsa_path = '/etc/pve/priv/zfs';
+my $targetcli = '/usr/bin/targetcli';
+
+my $execute_remote_command = sub {
+    my ($scfg, $timeout, $remote_command, @params) = @_;
+
+    my $msg = '';
+    my $err = undef;
+    my $target;
+    my $cmd;
+    my $res = ();
+
+    $timeout = 10 if !$timeout;
+
+    my $output = sub { $msg .= "$_[0]\n" };
+    my $errfunc = sub { $err .= "$_[0]\n" };
+
+    $target = 'root@' . $scfg->{portal};
+    $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, '--', $remote_command, @params];
+
+    eval {
+       run_command($cmd, outfunc => $output, errfunc => $errfunc, timeout => $timeout);
+    };
+    if ($@) {
+       $res = {
+           result => 0,
+           msg => $err,
+       }
+    } else {
+       $res = {
+           result => 1,
+           msg => $msg,
+       }
+    }
+
+    return $res;
+};
+
+# fetch targetcli configuration from the portal
+my $read_config = sub {
+    my ($scfg, $timeout) = @_;
+
+    my $msg = '';
+    my $err = undef;
+    my $luncmd = 'cat';
+    my $target;
+    my $retry = 1;
+
+    $timeout = 10 if !$timeout;
+
+    my $output = sub { $msg .= "$_[0]\n" };
+    my $errfunc = sub { $err .= "$_[0]\n" };
+
+    $target = 'root@' . $scfg->{portal};
+
+    foreach my $oneFile (@CONFIG_FILES) {
+       my $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target, $luncmd, $oneFile];
+       eval {
+           run_command($cmd, outfunc => $output, errfunc => $errfunc, timeout => $timeout);
+       };
+       if ($@) {
+           die $err if ($err !~ /No such file or directory/);
+       }
+       return $msg if $msg ne '';
+    }
+
+    die "No configuration found. Install targetcli on $scfg->{portal}\n" if $msg eq '';
+
+    return $msg;
+};
+
+my $get_config = sub {
+    my ($scfg) = @_;
+    my @conf = undef;
+
+    my $config = $read_config->($scfg, undef);
+    die "Missing config file" unless $config;
+
+    return $config;
+};
+
+# Return settings of a specific target
+my $get_target_settings = sub {
+   my ($scfg) = @_;
+
+   my $id = "$scfg->{portal}.$scfg->{target}";
+   return undef if !$SETTINGS;
+   return $SETTINGS->{$id};
+};
+
+# fetches and parses targetcli config from the portal
+my $parser = sub {
+    my ($scfg) = @_;
+    my $tpg = $scfg->{lio_tpg} || die "Target Portal Group not set, aborting!\n";
+    my $tpg_tag;
+
+    if ($tpg =~ /^tpg(\d+)$/) {
+       $tpg_tag = $1;
+    } else {
+       die "Target Portal Group has invalid value, must contain string 'tpg' and a suffix number, eg 'tpg17'\n";
+    }
+
+    my $config = $get_config->($scfg);
+    my $jsonconfig = JSON->new->utf8->decode($config);
+
+    my $haveTarget = 0;
+    foreach my $target (@{$jsonconfig->{targets}}) {
+       # only interested in iSCSI targets
+       next if !($target->{fabric} eq 'iscsi' && $target->{wwn} eq $scfg->{target});
+       # find correct TPG
+       foreach my $tpg (@{$target->{tpgs}}) {
+           if ($tpg->{tag} == $tpg_tag) {
+               my $res = [];
+               foreach my $lun (@{$tpg->{luns}}) {
+                   my ($idx, $storage_object);
+                   if ($lun->{index} =~ /^(\d+)$/) {
+                       $idx = $1;
+                   }
+                   if ($lun->{storage_object} =~ m|^($BACKSTORE/.*)$|) {
+                       $storage_object = $1;
+                   }
+                   die "Invalid lun definition in config!\n"
+                       if !(defined($idx) && defined($storage_object));
+                   push @$res, { index => $idx, storage_object => $storage_object };
+               }
+
+               my $id = "$scfg->{portal}.$scfg->{target}";
+               $SETTINGS->{$id}->{luns} = $res;
+               $haveTarget = 1;
+               last;
+           }
+       }
+    }
+
+    # seriously unhappy if the target server lacks iSCSI target configuration ...
+    if (!$haveTarget) {
+       die "target portal group tpg$tpg_tag not found!\n";
+    }
+};
+
+# Get prefix for backstores
+my $get_backstore_prefix = sub {
+    my ($scfg) = @_;
+    my $pool = $scfg->{pool};
+    $pool =~ s/\//-/g;
+    return $pool . '-';
+};
+
+# removes the given lu_name from the local list of luns
+my $free_lu_name = sub {
+    my ($scfg, $lu_name) = @_;
+
+    my $new = [];
+    my $target = $get_target_settings->($scfg);
+    foreach my $lun (@{$target->{luns}}) {
+       if ($lun->{storage_object} ne "$BACKSTORE/$lu_name") {
+           push @$new, $lun;
+       }
+    }
+
+    $target->{luns} = $new;
+};
+
+# locally registers a new lun
+my $register_lun = sub {
+    my ($scfg, $idx, $volname) = @_;
+
+    my $conf = {
+       index => $idx,
+       storage_object => "$BACKSTORE/$volname",
+       is_new => 1,
+    };
+    my $target = $get_target_settings->($scfg);
+    push @{$target->{luns}}, $conf;
+
+    return $conf;
+};
+
+# extracts the ZFS volume name from a device path
+my $extract_volname = sub {
+    my ($scfg, $lunpath) = @_;
+    my $volname = undef;
+
+    my $base = get_base;
+    if ($lunpath =~ /^$base\/$scfg->{pool}\/([\w\-]+)$/) {
+       $volname = $1;
+       my $prefix = $get_backstore_prefix->($scfg);
+       my $target = $get_target_settings->($scfg);
+       foreach my $lun (@{$target->{luns}}) {
+           # If we have a lun with the pool prefix matching this vol, then return this one
+           # like pool-pve-vm-100-disk-0
+           # Else, just fallback to the old name scheme which is vm-100-disk-0
+           if ($lun->{storage_object} =~ /^$BACKSTORE\/($prefix$volname)$/) {
+               return $1;
+           }
+       }
+    }
+
+    return $volname;
+};
+
+# retrieves the LUN index for a particular object
+my $list_view = sub {
+    my ($scfg, $timeout, $method, @params) = @_;
+    my $lun = undef;
+
+    my $object = $params[0];
+    my $volname = $extract_volname->($scfg, $object);
+    my $target = $get_target_settings->($scfg);
+
+    return undef if !defined($volname); # nothing to search for..
+
+    foreach my $lun (@{$target->{luns}}) {
+       if ($lun->{storage_object} eq "$BACKSTORE/$volname") {
+           return $lun->{index};
+       }
+    }
+
+    return $lun;
+};
+
+# determines, if the given object exists on the portal
+my $list_lun = sub {
+    my ($scfg, $timeout, $method, @params) = @_;
+
+    my $object = $params[0];
+    my $volname = $extract_volname->($scfg, $object);
+    my $target = $get_target_settings->($scfg);
+
+    foreach my $lun (@{$target->{luns}}) {
+       if ($lun->{storage_object} eq "$BACKSTORE/$volname") {
+           return $object;
+       }
+    }
+
+    return undef;
+};
+
+# adds a new LUN to the target
+my $create_lun = sub {
+    my ($scfg, $timeout, $method, @params) = @_;
+
+    if ($list_lun->($scfg, $timeout, $method, @params)) {
+       die "$params[0]: LUN already exists!";
+    }
+
+    my $device = $params[0];
+    my $volname = $extract_volname->($scfg, $device);
+    # Here we create a new device, so we didn't get the volname prefixed with the pool name
+    # as extract_volname couldn't find a matching vol yet
+    $volname = $get_backstore_prefix->($scfg) . $volname;
+    my $tpg = $scfg->{lio_tpg} || die "Target Portal Group not set, aborting!\n";
+
+    # step 1: create backstore for device
+    my @cliparams = ($BACKSTORE, 'create', "name=$volname", "dev=$device" );
+    my $res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams);
+    die $res->{msg} if !$res->{result};
+
+    # step 2: enable unmap support on the backstore
+    @cliparams = ($BACKSTORE . '/' . $volname, 'set', 'attribute', 'emulate_tpu=1' );
+    $res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams);
+    die $res->{msg} if !$res->{result};
+
+    # step 3: register lun with target
+    # targetcli /iscsi/iqn.2018-04.at.bestsolution.somehost:target/tpg1/luns/ create /backstores/block/foobar
+    @cliparams = ("/iscsi/$scfg->{target}/$tpg/luns/", 'create', "$BACKSTORE/$volname" );
+    $res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams);
+    die $res->{msg} if !$res->{result};
+
+    # targetcli responds with "Created LUN 99"
+    # not calculating the index ourselves, because the index at the portal might have
+    # changed without our knowledge, so relying on the number that targetcli returns
+    my $lun_idx;
+    if ($res->{msg} =~ /LUN (\d+)/) {
+       $lun_idx = $1;
+    } else {
+       die "unable to determine new LUN index: $res->{msg}";
+    }
+
+    $register_lun->($scfg, $lun_idx, $volname);
+
+    # step 3: unfortunately, targetcli doesn't always save changes, no matter
+    #         if auto_save_on_exit is true or not. So saving to be safe ...
+    $execute_remote_command->($scfg, $timeout, $targetcli, 'saveconfig');
+
+    return $res->{msg};
+};
+
+my $delete_lun = sub {
+    my ($scfg, $timeout, $method, @params) = @_;
+    my $res = {msg => undef};
+
+    my $tpg = $scfg->{lio_tpg} || die "Target Portal Group not set, aborting!\n";
+
+    my $path = $params[0];
+    my $volname = $extract_volname->($scfg, $path);
+    my $target = $get_target_settings->($scfg);
+
+    foreach my $lun (@{$target->{luns}}) {
+       next if $lun->{storage_object} ne "$BACKSTORE/$volname";
+
+       # step 1: delete the lun
+       my @cliparams = ("/iscsi/$scfg->{target}/$tpg/luns/", 'delete', "lun$lun->{index}" );
+       my $res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams);
+       do {
+           die $res->{msg};
+       } unless $res->{result};
+
+       # step 2: delete the backstore
+       @cliparams = ($BACKSTORE, 'delete', $volname);
+       $res = $execute_remote_command->($scfg, $timeout, $targetcli, @cliparams);
+       do {
+           die $res->{msg};
+       } unless $res->{result};
+
+       # step 3: save to be safe ...
+       $execute_remote_command->($scfg, $timeout, $targetcli, 'saveconfig');
+
+       # update internal cache
+       $free_lu_name->($scfg, $volname);
+
+       last;
+    }
+
+    return $res->{msg};
+};
+
+my $import_lun = sub {
+    my ($scfg, $timeout, $method, @params) = @_;
+
+    return $create_lun->($scfg, $timeout, $method, @params);
+};
+
+# needed for example when the underlying ZFS volume has been resized
+my $modify_lun = sub {
+    my ($scfg, $timeout, $method, @params) = @_;
+    # Nothing to do on volume modification for LIO
+    return undef;
+};
+
+my $add_view = sub {
+    my ($scfg, $timeout, $method, @params) = @_;
+
+    return '';
+};
+
+my %lun_cmd_map = (
+    create_lu   =>  $create_lun,
+    delete_lu   =>  $delete_lun,
+    import_lu   =>  $import_lun,
+    modify_lu   =>  $modify_lun,
+    add_view    =>  $add_view,
+    list_view   =>  $list_view,
+    list_lu     =>  $list_lun,
+);
+
+sub run_lun_command {
+    my ($scfg, $timeout, $method, @params) = @_;
+
+    # fetch configuration from target if we haven't yet or if it is stale
+    my $timediff = time - $SETTINGS_TIMESTAMP;
+    my $target = $get_target_settings->($scfg);
+    if (!$target || $timediff > $SETTINGS_MAXAGE) {
+       $SETTINGS_TIMESTAMP = time;
+       $parser->($scfg);
+    }
+
+    die "unknown command '$method'" unless exists $lun_cmd_map{$method};
+    my $msg = $lun_cmd_map{$method}->($scfg, $timeout, $method, @params);
+
+    return $msg;
+}
+
+sub get_base {
+    return '/dev';
+}
+
+1;
diff --git a/src/PVE/Storage/LunCmd/Makefile b/src/PVE/Storage/LunCmd/Makefile
new file mode 100644 (file)
index 0000000..a7209d1
--- /dev/null
@@ -0,0 +1,5 @@
+SOURCES=Comstar.pm Istgt.pm Iet.pm LIO.pm
+
+.PHONY: install
+install:
+       for i in ${SOURCES}; do install -D -m 0644 $$i ${DESTDIR}${PERLDIR}/PVE/Storage/LunCmd/$$i; done
diff --git a/src/PVE/Storage/LvmThinPlugin.pm b/src/PVE/Storage/LvmThinPlugin.pm
new file mode 100644 (file)
index 0000000..1d2e37c
--- /dev/null
@@ -0,0 +1,393 @@
+package PVE::Storage::LvmThinPlugin;
+
+use strict;
+use warnings;
+
+use IO::File;
+
+use PVE::Tools qw(run_command trim);
+use PVE::Storage::Plugin;
+use PVE::Storage::LVMPlugin;
+use PVE::JSONSchema qw(get_standard_option);
+
+# see: man lvmthin
+# lvcreate -n ThinDataLV -L LargeSize VG
+# lvconvert --type thin-pool VG/ThinDataLV
+# lvcreate -n pvepool -L 20G pve
+# lvconvert --type thin-pool pve/pvepool
+
+# NOTE: volumes which were created as linked clones of another base volume
+# are currently not tracking this relationship in their volume IDs. this is
+# generally not a problem, as LVM thin allows deletion of such base volumes
+# without affecting the linked clones. this leads to increased disk usage
+# when migrating LVM-thin volumes, which is normally prevented for linked clones.
+
+use base qw(PVE::Storage::LVMPlugin);
+
+sub type {
+    return 'lvmthin';
+}
+
+sub plugindata {
+    return {
+       content => [ {images => 1, rootdir => 1}, { images => 1, rootdir => 1}],
+    };
+}
+
+sub properties {
+    return {
+       thinpool => {
+           description => "LVM thin pool LV name.",
+           type => 'string', format => 'pve-storage-vgname',
+       },
+    };
+}
+
+sub options {
+    return {
+       thinpool => { fixed => 1 },
+       vgname => { fixed => 1 },
+        nodes => { optional => 1 },
+       disable => { optional => 1 },
+       content => { optional => 1 },
+       bwlimit => { optional => 1 },
+    };
+}
+
+# NOTE: the fourth and fifth element of the returned array are always
+# undef, even if the volume is a linked clone of another volume. see note
+# at beginning of file.
+sub parse_volname {
+    my ($class, $volname) = @_;
+
+    PVE::Storage::Plugin::parse_lvm_name($volname);
+
+    if ($volname =~ m/^((vm|base)-(\d+)-\S+)$/) {
+       return ('images', $1, $3, undef, undef, $2 eq 'base', 'raw');
+    }
+
+    die "unable to parse lvm volume name '$volname'\n";
+}
+
+sub filesystem_path {
+    my ($class, $scfg, $volname, $snapname) = @_;
+
+    my ($vtype, $name, $vmid) = $class->parse_volname($volname);
+
+    my $vg = $scfg->{vgname};
+
+    my $path = defined($snapname) ? "/dev/$vg/snap_${name}_$snapname": "/dev/$vg/$name";
+
+    return wantarray ? ($path, $vmid, $vtype) : $path;
+}
+
+sub alloc_image {
+    my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
+
+    die "unsupported format '$fmt'" if $fmt ne 'raw';
+
+    die "illegal name '$name' - should be 'vm-$vmid-*'\n"
+       if  $name && $name !~ m/^vm-$vmid-/;
+
+    my $vgs = PVE::Storage::LVMPlugin::lvm_vgs();
+
+    my $vg = $scfg->{vgname};
+
+    die "no such volume group '$vg'\n" if !defined ($vgs->{$vg});
+
+    $name = $class->find_free_diskname($storeid, $scfg, $vmid)
+       if !$name;
+
+    my $cmd = ['/sbin/lvcreate', '-aly', '-V', "${size}k", '--name', $name,
+              '--thinpool', "$vg/$scfg->{thinpool}" ];
+
+    run_command($cmd, errmsg => "lvcreate '$vg/$name' error");
+
+    return $name;
+}
+
+sub free_image {
+    my ($class, $storeid, $scfg, $volname, $isBase) = @_;
+
+    my $vg = $scfg->{vgname};
+
+    my $lvs = PVE::Storage::LVMPlugin::lvm_list_volumes($vg);
+
+    if (my $dat = $lvs->{$scfg->{vgname}}) {
+
+       # remove all volume snapshots first
+       foreach my $lv (keys %$dat) {
+           next if $lv !~ m/^snap_${volname}_${PVE::JSONSchema::CONFIGID_RE}$/;
+           my $cmd = ['/sbin/lvremove', '-f', "$vg/$lv"];
+           run_command($cmd, errmsg => "lvremove snapshot '$vg/$lv' error");
+       }
+
+       # finally remove original (if exists)
+       if ($dat->{$volname}) {
+           my $cmd = ['/sbin/lvremove', '-f', "$vg/$volname"];
+           run_command($cmd, errmsg => "lvremove '$vg/$volname' error");
+       }
+    }
+
+    return undef;
+}
+
+sub list_images {
+    my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
+
+    my $vgname = $scfg->{vgname};
+
+    $cache->{lvs} = PVE::Storage::LVMPlugin::lvm_list_volumes() if !$cache->{lvs};
+
+    my $res = [];
+
+    if (my $dat = $cache->{lvs}->{$vgname}) {
+
+       foreach my $volname (keys %$dat) {
+
+           next if $volname !~ m/^(vm|base)-(\d+)-/;
+           my $owner = $2;
+
+           my $info = $dat->{$volname};
+
+           next if $info->{lv_type} ne 'V';
+
+           next if $info->{pool_lv} ne $scfg->{thinpool};
+
+           my $volid = "$storeid:$volname";
+
+           if ($vollist) {
+               my $found = grep { $_ eq $volid } @$vollist;
+               next if !$found;
+           } else {
+               next if defined($vmid) && ($owner ne $vmid);
+           }
+
+           push @$res, {
+               volid => $volid, format => 'raw', size => $info->{lv_size}, vmid => $owner,
+               ctime => $info->{ctime},
+           };
+       }
+    }
+
+    return $res;
+}
+
+sub list_thinpools {
+    my ($vg) = @_;
+
+    my $lvs = PVE::Storage::LVMPlugin::lvm_list_volumes($vg);
+    my $thinpools = [];
+
+    foreach my $vg (keys %$lvs) {
+       foreach my $lvname (keys %{$lvs->{$vg}}) {
+           next if $lvs->{$vg}->{$lvname}->{lv_type} ne 't';
+           my $lv = $lvs->{$vg}->{$lvname};
+           $lv->{lv} = $lvname;
+           $lv->{vg} = $vg;
+           push @$thinpools, $lv;
+       }
+    }
+
+    return $thinpools;
+}
+
+sub status {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    my $lvs = $cache->{lvs} ||= PVE::Storage::LVMPlugin::lvm_list_volumes();
+
+    return if !$lvs->{$scfg->{vgname}};
+
+    my $info = $lvs->{$scfg->{vgname}}->{$scfg->{thinpool}};
+
+    return if !$info || $info->{lv_type} ne 't' || !$info->{lv_size};
+
+    return (
+       $info->{lv_size},
+       $info->{lv_size} - $info->{used},
+       $info->{used},
+       $info->{lv_state} eq 'a' ? 1 : 0,
+    );
+}
+
+my $activate_lv = sub {
+    my ($vg, $lv, $cache) = @_;
+
+    my $lvs = $cache->{lvs} ||= PVE::Storage::LVMPlugin::lvm_list_volumes();
+
+    die "no such logical volume $vg/$lv\n" if !$lvs->{$vg} || !$lvs->{$vg}->{$lv};
+
+    return if $lvs->{$vg}->{$lv}->{lv_state} eq 'a';
+
+    run_command(['lvchange', '-ay', '-K', "$vg/$lv"], errmsg => "activating LV '$vg/$lv' failed");
+
+    $lvs->{$vg}->{$lv}->{lv_state} = 'a'; # update cache
+
+    return;
+};
+
+sub activate_storage {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    $class->SUPER::activate_storage($storeid, $scfg, $cache);
+
+    $activate_lv->($scfg->{vgname}, $scfg->{thinpool}, $cache);
+}
+
+sub activate_volume {
+    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
+
+    my $vg = $scfg->{vgname};
+    my $lv = $snapname ? "snap_${volname}_$snapname" : $volname;
+
+    $activate_lv->($vg, $lv, $cache);
+}
+
+sub deactivate_volume {
+    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
+
+    return if !$snapname && $volname !~ /^base-/; # other volumes are kept active
+
+    my $vg = $scfg->{vgname};
+    my $lv = $snapname ? "snap_${volname}_$snapname" : $volname;
+
+    run_command(['lvchange', '-an', "$vg/$lv"], errmsg => "deactivate_volume '$vg/$lv' error");
+
+    $cache->{lvs}->{$vg}->{$lv}->{lv_state} = '-' # update cache
+       if $cache->{lvs} && $cache->{lvs}->{$vg} && $cache->{lvs}->{$vg}->{$lv};
+
+    return;
+}
+
+sub clone_image {
+    my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
+
+    my $vg = $scfg->{vgname};
+
+    my $lv;
+
+    if ($snap) {
+       $lv = "$vg/snap_${volname}_$snap";
+    } else {
+       my ($vtype, undef, undef, undef, undef, $isBase, $format) =
+           $class->parse_volname($volname);
+
+       die "clone_image only works on base images\n" if !$isBase;
+
+       $lv = "$vg/$volname";
+    }
+
+    my $name = $class->find_free_diskname($storeid, $scfg, $vmid);
+
+    my $cmd = ['/sbin/lvcreate', '-n', $name, '-prw', '-kn', '-s', $lv];
+    run_command($cmd, errmsg => "clone image '$lv' error");
+
+    return $name;
+}
+
+sub create_base {
+    my ($class, $storeid, $scfg, $volname) = @_;
+
+    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
+        $class->parse_volname($volname);
+
+    die "create_base not possible with base image\n" if $isBase;
+
+    my $vg = $scfg->{vgname};
+    my $lvs = PVE::Storage::LVMPlugin::lvm_list_volumes($vg);
+
+    if (my $dat = $lvs->{$vg}) {
+       # to avoid confusion, reject if we find volume snapshots
+       foreach my $lv (keys %$dat) {
+           die "unable to create base volume - found snaphost '$lv'\n"
+               if $lv =~ m/^snap_${volname}_(\w+)$/;
+       }
+    }
+
+    my $newname = $name;
+    $newname =~ s/^vm-/base-/;
+
+    my $cmd = ['/sbin/lvrename', $vg, $volname, $newname];
+    run_command($cmd, errmsg => "lvrename '$vg/$volname' => '$vg/$newname' error");
+
+    # set inactive, read-only and activationskip flags
+    $cmd = ['/sbin/lvchange', '-an', '-pr', '-ky', "$vg/$newname"];
+    eval { run_command($cmd); };
+    warn $@ if $@;
+
+    my $newvolname = $newname;
+
+    return $newvolname;
+}
+
+# sub volume_resize {} reuse code from parent class
+
+sub volume_snapshot {
+    my ($class, $scfg, $storeid, $volname, $snap) = @_;
+
+    my $vg = $scfg->{vgname};
+    my $snapvol = "snap_${volname}_$snap";
+
+    my $cmd = ['/sbin/lvcreate', '-n', $snapvol, '-pr', '-s', "$vg/$volname"];
+    run_command($cmd, errmsg => "lvcreate snapshot '$vg/$snapvol' error");
+
+}
+
+sub volume_snapshot_rollback {
+    my ($class, $scfg, $storeid, $volname, $snap) = @_;
+
+    my $vg = $scfg->{vgname};
+    my $snapvol = "snap_${volname}_$snap";
+
+    my $cmd = ['/sbin/lvremove', '-f', "$vg/$volname"];
+    run_command($cmd, errmsg => "lvremove '$vg/$volname' error");
+
+    $cmd = ['/sbin/lvcreate', '-kn', '-n', $volname, '-s', "$vg/$snapvol"];
+    run_command($cmd, errmsg => "lvm rollback '$vg/$snapvol' error");
+}
+
+sub volume_snapshot_delete {
+    my ($class, $scfg, $storeid, $volname, $snap) = @_;
+
+    my $vg = $scfg->{vgname};
+    my $snapvol = "snap_${volname}_$snap";
+
+    my $cmd = ['/sbin/lvremove', '-f', "$vg/$snapvol"];
+    run_command($cmd, errmsg => "lvremove snapshot '$vg/$snapvol' error");
+}
+
+sub volume_has_feature {
+    my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
+
+    my $features = {
+       snapshot => { current => 1 },
+       clone => { base => 1, snap => 1},
+       template => { current => 1},
+       copy => { base => 1, current => 1, snap => 1},
+       sparseinit => { base => 1, current => 1},
+       rename => {current => 1},
+    };
+
+    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
+       $class->parse_volname($volname);
+
+    my $key = undef;
+    if($snapname){
+       $key = 'snap';
+    }else{
+       $key =  $isBase ? 'base' : 'current';
+    }
+    return 1 if $features->{$feature}->{$key};
+
+    return undef;
+}
+
+# used in LVMPlugin->volume_import
+sub volume_import_write {
+    my ($class, $input_fh, $output_file) = @_;
+    run_command(['dd', "of=$output_file", 'conv=sparse', 'bs=64k'],
+       input => '<&'.fileno($input_fh));
+}
+
+1;
diff --git a/src/PVE/Storage/Makefile b/src/PVE/Storage/Makefile
new file mode 100644 (file)
index 0000000..857c485
--- /dev/null
@@ -0,0 +1,21 @@
+SOURCES= \
+       Plugin.pm \
+       DirPlugin.pm \
+       LVMPlugin.pm \
+       NFSPlugin.pm \
+       CIFSPlugin.pm \
+       ISCSIPlugin.pm \
+       CephFSPlugin.pm \
+       RBDPlugin.pm \
+       ISCSIDirectPlugin.pm \
+       GlusterfsPlugin.pm \
+       ZFSPoolPlugin.pm \
+       ZFSPlugin.pm \
+       PBSPlugin.pm \
+       BTRFSPlugin.pm \
+       LvmThinPlugin.pm
+
+.PHONY: install
+install:
+       for i in ${SOURCES}; do install -D -m 0644 $$i ${DESTDIR}${PERLDIR}/PVE/Storage/$$i; done
+       make -C LunCmd install
diff --git a/src/PVE/Storage/NFSPlugin.pm b/src/PVE/Storage/NFSPlugin.pm
new file mode 100644 (file)
index 0000000..c2d4176
--- /dev/null
@@ -0,0 +1,228 @@
+package PVE::Storage::NFSPlugin;
+
+use strict;
+use warnings;
+use IO::File;
+use Net::IP;
+use File::Path;
+
+use PVE::Network;
+use PVE::Tools qw(run_command);
+use PVE::ProcFSTools;
+use PVE::Storage::Plugin;
+use PVE::JSONSchema qw(get_standard_option);
+
+use base qw(PVE::Storage::Plugin);
+
+# NFS helper functions
+
+sub nfs_is_mounted {
+    my ($server, $export, $mountpoint, $mountdata) = @_;
+
+    $server = "[$server]" if Net::IP::ip_is_ipv6($server);
+    my $source = "$server:$export";
+
+    $mountdata = PVE::ProcFSTools::parse_proc_mounts() if !$mountdata;
+    return $mountpoint if grep {
+       $_->[2] =~ /^nfs/ &&
+       $_->[0] =~ m|^\Q$source\E/?$| &&
+       $_->[1] eq $mountpoint
+    } @$mountdata;
+    return undef;
+}
+
+sub nfs_mount {
+    my ($server, $export, $mountpoint, $options) = @_;
+
+    $server = "[$server]" if Net::IP::ip_is_ipv6($server);
+    my $source = "$server:$export";
+
+    my $cmd = ['/bin/mount', '-t', 'nfs', $source, $mountpoint];
+    if ($options) {
+       push @$cmd, '-o', $options;
+    } 
+
+    run_command($cmd, errmsg => "mount error");
+}
+
+# Configuration
+
+sub type {
+    return 'nfs';
+}
+
+sub plugindata {
+    return {
+       content => [ { images => 1, rootdir => 1, vztmpl => 1, iso => 1, backup => 1, snippets => 1 },
+                    { images => 1 }],
+       format => [ { raw => 1, qcow2 => 1, vmdk => 1 } , 'raw' ],
+    };
+}   
+
+sub properties {
+    return {
+       export => {
+           description => "NFS export path.",
+           type => 'string', format => 'pve-storage-path',
+       },
+       server => {
+           description => "Server IP or DNS name.",
+           type => 'string', format => 'pve-storage-server',
+       },
+       options => {
+           description => "NFS mount options (see 'man nfs')",
+           type => 'string',  format => 'pve-storage-options',
+       },
+    };
+}
+
+sub options {
+    return {
+       path => { fixed => 1 },
+       'content-dirs' => { optional => 1 },
+       server => { fixed => 1 },
+       export => { fixed => 1 },
+       nodes => { optional => 1 },
+       disable => { optional => 1 },
+       maxfiles => { optional => 1 },
+       'prune-backups' => { optional => 1 },
+       'max-protected-backups' => { optional => 1 },
+       options => { optional => 1 },
+       content => { optional => 1 },
+       format => { optional => 1 },
+       mkdir => { optional => 1 },
+       bwlimit => { optional => 1 },
+       preallocation => { optional => 1 },
+    };
+}
+
+
+sub check_config {
+    my ($class, $sectionId, $config, $create, $skipSchemaCheck) = @_;
+
+    $config->{path} = "/mnt/pve/$sectionId" if $create && !$config->{path};
+
+    return $class->SUPER::check_config($sectionId, $config, $create, $skipSchemaCheck);
+}
+
+# Storage implementation
+
+sub status {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    $cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts()
+       if !$cache->{mountdata};
+
+    my $path = $scfg->{path};
+    my $server = $scfg->{server};
+    my $export = $scfg->{export};
+
+    return undef if !nfs_is_mounted($server, $export, $path, $cache->{mountdata}); 
+
+    return $class->SUPER::status($storeid, $scfg, $cache);
+}
+
+sub activate_storage {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    $cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts()
+       if !$cache->{mountdata};
+
+    my $path = $scfg->{path};
+    my $server = $scfg->{server};
+    my $export = $scfg->{export};
+
+    if (!nfs_is_mounted($server, $export, $path, $cache->{mountdata})) {
+       # NOTE: only call mkpath when not mounted (avoid hang when NFS server is offline
+       mkpath $path if !(defined($scfg->{mkdir}) && !$scfg->{mkdir});
+
+       die "unable to activate storage '$storeid' - " .
+           "directory '$path' does not exist\n" if ! -d $path;
+
+       nfs_mount($server, $export, $path, $scfg->{options});
+    }
+
+    $class->SUPER::activate_storage($storeid, $scfg, $cache);
+}
+
+sub deactivate_storage {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    $cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts()
+       if !$cache->{mountdata};
+
+    my $path = $scfg->{path};
+    my $server = $scfg->{server};
+    my $export = $scfg->{export};
+
+    if (nfs_is_mounted($server, $export, $path, $cache->{mountdata})) {    
+       my $cmd = ['/bin/umount', $path];
+       run_command($cmd, errmsg => 'umount error'); 
+    }
+}
+
+sub check_connection {
+    my ($class, $storeid, $scfg) = @_;
+
+    my $server = $scfg->{server};
+    my $opts = $scfg->{options};
+
+    my $cmd;
+
+    my $is_v4 = defined($opts) && $opts =~ /vers=4.*/;
+    if ($is_v4) {
+       my $ip = PVE::JSONSchema::pve_verify_ip($server, 1);
+       if (!defined($ip)) {
+           $ip = PVE::Network::get_ip_from_hostname($server);
+       }
+
+       my $transport = PVE::JSONSchema::pve_verify_ipv4($ip, 1) ? 'tcp' : 'tcp6';
+
+       # nfsv4 uses a pseudo-filesystem always beginning with /
+       # no exports are listed
+       $cmd = ['/usr/sbin/rpcinfo', '-T', $transport, $ip, 'nfs', '4'];
+    } else {
+       $cmd = ['/sbin/showmount', '--no-headers', '--exports', $server];
+    }
+
+    eval { run_command($cmd, timeout => 10, outfunc => sub {}, errfunc => sub {}) };
+    if (my $err = $@) {
+       if ($is_v4) {
+           my $port = 2049;
+           $port = $1 if defined($opts) && $opts =~ /port=(\d+)/;
+
+           # rpcinfo is expected to work when the port is 0 (see 'man 5 nfs') and tcp_ping()
+           # defaults to port 7 when passing in 0.
+           return 0 if $port == 0;
+
+           return PVE::Network::tcp_ping($server, $port, 2);
+       }
+       return 0;
+    }
+
+    return 1;
+}
+
+# FIXME remove on the next APIAGE reset.
+# Deprecated, use get_volume_attribute instead.
+sub get_volume_notes {
+    my $class = shift;
+    PVE::Storage::DirPlugin::get_volume_notes($class, @_);
+}
+
+# FIXME remove on the next APIAGE reset.
+# Deprecated, use update_volume_attribute instead.
+sub update_volume_notes {
+    my $class = shift;
+    PVE::Storage::DirPlugin::update_volume_notes($class, @_);
+}
+
+sub get_volume_attribute {
+    return PVE::Storage::DirPlugin::get_volume_attribute(@_);
+}
+
+sub update_volume_attribute {
+    return PVE::Storage::DirPlugin::update_volume_attribute(@_);
+}
+
+1;
diff --git a/src/PVE/Storage/PBSPlugin.pm b/src/PVE/Storage/PBSPlugin.pm
new file mode 100644 (file)
index 0000000..4320974
--- /dev/null
@@ -0,0 +1,981 @@
+package PVE::Storage::PBSPlugin;
+
+# Plugin to access Proxmox Backup Server
+
+use strict;
+use warnings;
+
+use Fcntl qw(F_GETFD F_SETFD FD_CLOEXEC);
+use IO::File;
+use JSON;
+use MIME::Base64 qw(decode_base64);
+use POSIX qw(mktime strftime ENOENT);
+use POSIX::strptime;
+
+use PVE::APIClient::LWP;
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::Network;
+use PVE::PBSClient;
+use PVE::Storage::Plugin;
+use PVE::Tools qw(run_command file_read_firstline trim dir_glob_regex dir_glob_foreach $IPV6RE);
+
+use base qw(PVE::Storage::Plugin);
+
+# Configuration
+
+sub type {
+    return 'pbs';
+}
+
+sub plugindata {
+    return {
+       content => [ {backup => 1, none => 1}, { backup => 1 }],
+    };
+}
+
+sub properties {
+    return {
+       datastore => {
+           description => "Proxmox Backup Server datastore name.",
+           type => 'string',
+       },
+       # openssl s_client -connect <host>:8007 2>&1 |openssl x509 -fingerprint -sha256
+       fingerprint => get_standard_option('fingerprint-sha256'),
+       'encryption-key' => {
+           description => "Encryption key. Use 'autogen' to generate one automatically without passphrase.",
+           type => 'string',
+       },
+       'master-pubkey' => {
+           description => "Base64-encoded, PEM-formatted public RSA key. Used to encrypt a copy of the encryption-key which will be added to each encrypted backup.",
+           type => 'string',
+       },
+       port => {
+           description => "For non default port.",
+           type => 'integer',
+           minimum => 1,
+           maximum => 65535,
+           default => 8007,
+       },
+    };
+}
+
+sub options {
+    return {
+       server => { fixed => 1 },
+       datastore => { fixed => 1 },
+       namespace => { optional => 1 },
+       port => { optional => 1 },
+       nodes => { optional => 1},
+       disable => { optional => 1},
+       content => { optional => 1},
+       username => { optional => 1 },
+       password => { optional => 1 },
+       'encryption-key' => { optional => 1 },
+       'master-pubkey' => { optional => 1 },
+       maxfiles => { optional => 1 },
+       'prune-backups' => { optional => 1 },
+       'max-protected-backups' => { optional => 1 },
+       fingerprint => { optional => 1 },
+    };
+}
+
+# Helpers
+
+sub pbs_password_file_name {
+    my ($scfg, $storeid) = @_;
+
+    return "/etc/pve/priv/storage/${storeid}.pw";
+}
+
+sub pbs_set_password {
+    my ($scfg, $storeid, $password) = @_;
+
+    my $pwfile = pbs_password_file_name($scfg, $storeid);
+    mkdir "/etc/pve/priv/storage";
+
+    PVE::Tools::file_set_contents($pwfile, "$password\n");
+}
+
+sub pbs_delete_password {
+    my ($scfg, $storeid) = @_;
+
+    my $pwfile = pbs_password_file_name($scfg, $storeid);
+
+    unlink $pwfile;
+}
+
+sub pbs_get_password {
+    my ($scfg, $storeid) = @_;
+
+    my $pwfile = pbs_password_file_name($scfg, $storeid);
+
+    return PVE::Tools::file_read_firstline($pwfile);
+}
+
+sub pbs_encryption_key_file_name {
+    my ($scfg, $storeid) = @_;
+
+    return "/etc/pve/priv/storage/${storeid}.enc";
+}
+
+sub pbs_set_encryption_key {
+    my ($scfg, $storeid, $key) = @_;
+
+    my $pwfile = pbs_encryption_key_file_name($scfg, $storeid);
+    mkdir "/etc/pve/priv/storage";
+
+    PVE::Tools::file_set_contents($pwfile, "$key\n");
+}
+
+sub pbs_delete_encryption_key {
+    my ($scfg, $storeid) = @_;
+
+    my $pwfile = pbs_encryption_key_file_name($scfg, $storeid);
+
+    if (!unlink $pwfile) {
+       return if $! == ENOENT;
+       die "failed to delete encryption key! $!\n";
+    }
+    delete $scfg->{'encryption-key'};
+}
+
+sub pbs_get_encryption_key {
+    my ($scfg, $storeid) = @_;
+
+    my $pwfile = pbs_encryption_key_file_name($scfg, $storeid);
+
+    return PVE::Tools::file_get_contents($pwfile);
+}
+
+# Returns a file handle if there is an encryption key, or `undef` if there is not. Dies on error.
+sub pbs_open_encryption_key {
+    my ($scfg, $storeid) = @_;
+
+    my $encryption_key_file = pbs_encryption_key_file_name($scfg, $storeid);
+
+    my $keyfd;
+    if (!open($keyfd, '<', $encryption_key_file)) {
+       if ($! == ENOENT) {
+           my $encryption_fp = $scfg->{'encryption-key'};
+           die "encryption configured ('$encryption_fp') but no encryption key file found!\n"
+               if $encryption_fp;
+           return undef;
+       }
+       die "failed to open encryption key: $encryption_key_file: $!\n";
+    }
+
+    return $keyfd;
+}
+
+sub pbs_master_pubkey_file_name {
+    my ($scfg, $storeid) = @_;
+
+    return "/etc/pve/priv/storage/${storeid}.master.pem";
+}
+
+sub pbs_set_master_pubkey {
+    my ($scfg, $storeid, $key) = @_;
+
+    my $pwfile = pbs_master_pubkey_file_name($scfg, $storeid);
+    mkdir "/etc/pve/priv/storage";
+
+    PVE::Tools::file_set_contents($pwfile, "$key\n");
+}
+
+sub pbs_delete_master_pubkey {
+    my ($scfg, $storeid) = @_;
+
+    my $pwfile = pbs_master_pubkey_file_name($scfg, $storeid);
+
+    if (!unlink $pwfile) {
+       return if $! == ENOENT;
+       die "failed to delete master public key! $!\n";
+    }
+    delete $scfg->{'master-pubkey'};
+}
+
+sub pbs_get_master_pubkey {
+    my ($scfg, $storeid) = @_;
+
+    my $pwfile = pbs_master_pubkey_file_name($scfg, $storeid);
+
+    return PVE::Tools::file_get_contents($pwfile);
+}
+
+# Returns a file handle if there is a master key, or `undef` if there is not. Dies on error.
+sub pbs_open_master_pubkey {
+    my ($scfg, $storeid) = @_;
+
+    my $master_pubkey_file = pbs_master_pubkey_file_name($scfg, $storeid);
+
+    my $keyfd;
+    if (!open($keyfd, '<', $master_pubkey_file)) {
+       if ($! == ENOENT) {
+           die "master public key configured but no key file found!\n"
+               if $scfg->{'master-pubkey'};
+           return undef;
+       }
+       die "failed to open master public key: $master_pubkey_file: $!\n";
+    }
+
+    return $keyfd;
+}
+
+sub print_volid {
+    my ($storeid, $btype, $bid, $btime) = @_;
+
+    my $time_str = strftime("%FT%TZ", gmtime($btime));
+    my $volname = "backup/${btype}/${bid}/${time_str}";
+
+    return "${storeid}:${volname}";
+}
+
+my sub ns : prototype($$) {
+    my ($scfg, $name) = @_;
+    my $ns = $scfg->{namespace};
+    return defined($ns) ? ($name, $ns) : ();
+}
+
+# essentially the inverse of print_volid
+my sub api_param_from_volname : prototype($$$) {
+    my ($class, $scfg, $volname) = @_;
+
+    my $name = ($class->parse_volname($volname))[1];
+
+    my ($btype, $bid, $timestr) = split('/', $name);
+
+    my @tm = (POSIX::strptime($timestr, "%FT%TZ"));
+    # expect sec, min, hour, mday, mon, year
+    die "error parsing time from '$volname'" if grep { !defined($_) } @tm[0..5];
+
+    my $btime;
+    {
+       local $ENV{TZ} = 'UTC'; # $timestr is UTC
+
+       # Fill in isdst to avoid undef warning. No daylight saving time for UTC.
+       $tm[8] //= 0;
+
+       my $since_epoch = mktime(@tm) or die "error converting time from '$volname'\n";
+       $btime = int($since_epoch);
+    }
+
+    return {
+       (ns($scfg, 'ns')),
+       'backup-type' => $btype,
+       'backup-id' => $bid,
+       'backup-time' => $btime,
+    };
+}
+
+my $USE_CRYPT_PARAMS = {
+    backup => 1,
+    restore => 1,
+    'upload-log' => 1,
+};
+
+my $USE_MASTER_KEY = {
+    backup => 1,
+};
+
+my sub do_raw_client_cmd {
+    my ($scfg, $storeid, $client_cmd, $param, %opts) = @_;
+
+    my $use_crypto = $USE_CRYPT_PARAMS->{$client_cmd};
+    my $use_master = $USE_MASTER_KEY->{$client_cmd};
+
+    my $client_exe = '/usr/bin/proxmox-backup-client';
+    die "executable not found '$client_exe'! Proxmox backup client not installed?\n"
+       if ! -x $client_exe;
+
+    my $repo = PVE::PBSClient::get_repository($scfg);
+
+    my $userns_cmd = delete $opts{userns_cmd};
+
+    my $cmd = [];
+
+    push @$cmd, @$userns_cmd if defined($userns_cmd);
+
+    push @$cmd, $client_exe, $client_cmd;
+
+    # This must live in the top scope to not get closed before the `run_command`
+    my ($keyfd, $master_fd);
+    if ($use_crypto) {
+       if (defined($keyfd = pbs_open_encryption_key($scfg, $storeid))) {
+           my $flags = fcntl($keyfd, F_GETFD, 0)
+               // die "failed to get file descriptor flags: $!\n";
+           fcntl($keyfd, F_SETFD, $flags & ~FD_CLOEXEC)
+               or die "failed to remove FD_CLOEXEC from encryption key file descriptor\n";
+           push @$cmd, '--crypt-mode=encrypt', '--keyfd='.fileno($keyfd);
+           if ($use_master && defined($master_fd = pbs_open_master_pubkey($scfg, $storeid))) {
+               my $flags = fcntl($master_fd, F_GETFD, 0)
+                   // die "failed to get file descriptor flags: $!\n";
+               fcntl($master_fd, F_SETFD, $flags & ~FD_CLOEXEC)
+                   or die "failed to remove FD_CLOEXEC from master public key file descriptor\n";
+               push @$cmd, '--master-pubkey-fd='.fileno($master_fd);
+           }
+       } else {
+           push @$cmd, '--crypt-mode=none';
+       }
+    }
+
+    push @$cmd, @$param if defined($param);
+
+    push @$cmd, "--repository", $repo;
+    if ($client_cmd ne 'status' && defined(my $ns = $scfg->{namespace})) {
+       push @$cmd, '--ns', $ns;
+    }
+
+    local $ENV{PBS_PASSWORD} = pbs_get_password($scfg, $storeid);
+
+    local $ENV{PBS_FINGERPRINT} = $scfg->{fingerprint};
+
+    # no ascii-art on task logs
+    local $ENV{PROXMOX_OUTPUT_NO_BORDER} = 1;
+    local $ENV{PROXMOX_OUTPUT_NO_HEADER} = 1;
+
+    if (my $logfunc = $opts{logfunc}) {
+       $logfunc->("run: " . join(' ', @$cmd));
+    }
+
+    run_command($cmd, %opts);
+}
+
+# FIXME: External perl code should NOT have access to this.
+#
+# There should be separate functions to
+# - make backups
+# - restore backups
+# - restore files
+# with a sane API
+sub run_raw_client_cmd {
+    my ($scfg, $storeid, $client_cmd, $param, %opts) = @_;
+    return do_raw_client_cmd($scfg, $storeid, $client_cmd, $param, %opts);
+}
+
+sub run_client_cmd {
+    my ($scfg, $storeid, $client_cmd, $param, $no_output) = @_;
+
+    my $json_str = '';
+    my $outfunc = sub { $json_str .= "$_[0]\n" };
+
+    $param = [] if !defined($param);
+    $param = [ $param ] if !ref($param);
+
+    $param = [@$param, '--output-format=json'] if !$no_output;
+
+    do_raw_client_cmd($scfg, $storeid, $client_cmd, $param,
+                     outfunc => $outfunc, errmsg => 'proxmox-backup-client failed');
+
+    return undef if $no_output;
+
+    my $res = decode_json($json_str);
+
+    return $res;
+}
+
+# Storage implementation
+
+sub extract_vzdump_config {
+    my ($class, $scfg, $volname, $storeid) = @_;
+
+    my ($vtype, $name, $vmid, undef, undef, undef, $format) = $class->parse_volname($volname);
+
+    my $config = '';
+    my $outfunc = sub { $config .= "$_[0]\n" };
+
+    my $config_name;
+    if ($format eq 'pbs-vm') {
+       $config_name = 'qemu-server.conf';
+    } elsif  ($format eq 'pbs-ct') {
+       $config_name = 'pct.conf';
+    } else {
+       die "unable to extract configuration for backup format '$format'\n";
+    }
+
+    do_raw_client_cmd($scfg, $storeid, 'restore', [ $name, $config_name, '-' ],
+                     outfunc => $outfunc, errmsg => 'proxmox-backup-client failed');
+
+    return $config;
+}
+
+sub prune_backups {
+    my ($class, $scfg, $storeid, $keep, $vmid, $type, $dryrun, $logfunc) = @_;
+
+    $logfunc //= sub { print "$_[1]\n" };
+
+    $type = 'vm' if defined($type) && $type eq 'qemu';
+    $type = 'ct' if defined($type) && $type eq 'lxc';
+
+    my $backup_groups = {};
+
+    if (defined($vmid) && defined($type)) {
+       # no need to get the list of volumes, we only got a single backup group anyway
+       $backup_groups->{"$type/$vmid"} = 1;
+    } else {
+       my $backups = eval { $class->list_volumes($storeid, $scfg, $vmid, ['backup']) };
+       die "failed to get list of all backups to prune - $@" if $@;
+
+       foreach my $backup (@{$backups}) {
+           (my $backup_type = $backup->{format}) =~ s/^pbs-//;
+           next if defined($type) && $backup_type ne $type;
+
+           my $backup_group = "$backup_type/$backup->{vmid}";
+           $backup_groups->{$backup_group} = 1;
+       }
+    }
+
+    my @param;
+
+    my $keep_all = delete $keep->{'keep-all'};
+
+    if (!$keep_all) {
+       foreach my $opt (keys %{$keep}) {
+           next if $keep->{$opt} == 0;
+           push @param, "--$opt";
+           push @param, "$keep->{$opt}";
+       }
+    } else { # no need to pass anything to PBS
+       $keep = { 'keep-all' => 1 };
+    }
+
+    push @param, '--dry-run' if $dryrun;
+
+    my $prune_list = [];
+    my $failed;
+
+    foreach my $backup_group (keys %{$backup_groups}) {
+       $logfunc->('info', "running 'proxmox-backup-client prune' for '$backup_group'")
+           if !$dryrun;
+       eval {
+           my $res = run_client_cmd($scfg, $storeid, 'prune', [ $backup_group, @param ]);
+
+           foreach my $backup (@{$res}) {
+               die "result from proxmox-backup-client is not as expected\n"
+                   if !defined($backup->{'backup-time'})
+                   || !defined($backup->{'backup-type'})
+                   || !defined($backup->{'backup-id'})
+                   || !defined($backup->{'keep'});
+
+               my $ctime = $backup->{'backup-time'};
+               my $type = $backup->{'backup-type'};
+               my $vmid = $backup->{'backup-id'};
+               my $volid = print_volid($storeid, $type, $vmid, $ctime);
+
+               my $mark = $backup->{keep} ? 'keep' : 'remove';
+               $mark = 'protected' if $backup->{protected};
+
+               push @{$prune_list}, {
+                   ctime => $ctime,
+                   mark => $mark,
+                   type => $type eq 'vm' ? 'qemu' : 'lxc',
+                   vmid => $vmid,
+                   volid => $volid,
+               };
+           }
+       };
+       if (my $err = $@) {
+           $logfunc->('err', "prune '$backup_group': $err\n");
+           $failed = 1;
+       }
+    }
+    die "error pruning backups - check log\n" if $failed;
+
+    return $prune_list;
+}
+
+my $autogen_encryption_key = sub {
+    my ($scfg, $storeid) = @_;
+    my $encfile = pbs_encryption_key_file_name($scfg, $storeid);
+    if (-f $encfile) {
+       rename $encfile, "$encfile.old";
+    }
+    my $cmd = ['proxmox-backup-client', 'key', 'create', '--kdf', 'none', $encfile];
+    run_command($cmd, errmsg => 'failed to create encryption key');
+    return PVE::Tools::file_get_contents($encfile);
+};
+
+sub on_add_hook {
+    my ($class, $storeid, $scfg, %param) = @_;
+
+    my $res = {};
+
+    if (defined(my $password = $param{password})) {
+       pbs_set_password($scfg, $storeid, $password);
+    } else {
+       pbs_delete_password($scfg, $storeid);
+    }
+
+    if (defined(my $encryption_key = $param{'encryption-key'})) {
+       my $decoded_key;
+       if ($encryption_key eq 'autogen') {
+           $res->{'encryption-key'} = $autogen_encryption_key->($scfg, $storeid);
+           $decoded_key = decode_json($res->{'encryption-key'});
+       } else {
+           $decoded_key = eval { decode_json($encryption_key) };
+           if ($@ || !exists($decoded_key->{data})) {
+               die "Value does not seems like a valid, JSON formatted encryption key!\n";
+           }
+           pbs_set_encryption_key($scfg, $storeid, $encryption_key);
+           $res->{'encryption-key'} = $encryption_key;
+       }
+       $scfg->{'encryption-key'} = $decoded_key->{fingerprint} || 1;
+    } else {
+       pbs_delete_encryption_key($scfg, $storeid);
+    }
+
+    if (defined(my $master_key = delete $param{'master-pubkey'})) {
+       die "'master-pubkey' can only be used together with 'encryption-key'\n"
+           if !defined($scfg->{'encryption-key'});
+
+       my $decoded = decode_base64($master_key);
+       pbs_set_master_pubkey($scfg, $storeid, $decoded);
+       $scfg->{'master-pubkey'} = 1;
+    } else {
+       pbs_delete_master_pubkey($scfg, $storeid);
+    }
+
+    return $res;
+}
+
+sub on_update_hook {
+    my ($class, $storeid, $scfg, %param) = @_;
+
+    my $res = {};
+
+    if (exists($param{password})) {
+       if (defined($param{password})) {
+           pbs_set_password($scfg, $storeid, $param{password});
+       } else {
+           pbs_delete_password($scfg, $storeid);
+       }
+    }
+
+    if (exists($param{'encryption-key'})) {
+       if (defined(my $encryption_key = delete($param{'encryption-key'}))) {
+           my $decoded_key;
+           if ($encryption_key eq 'autogen') {
+               $res->{'encryption-key'} = $autogen_encryption_key->($scfg, $storeid);
+               $decoded_key = decode_json($res->{'encryption-key'});
+           } else {
+               $decoded_key = eval { decode_json($encryption_key) };
+               if ($@ || !exists($decoded_key->{data})) {
+                   die "Value does not seems like a valid, JSON formatted encryption key!\n";
+               }
+               pbs_set_encryption_key($scfg, $storeid, $encryption_key);
+               $res->{'encryption-key'} = $encryption_key;
+           }
+           $scfg->{'encryption-key'} = $decoded_key->{fingerprint} || 1;
+       } else {
+           pbs_delete_encryption_key($scfg, $storeid);
+           delete $scfg->{'encryption-key'};
+       }
+    }
+
+    if (exists($param{'master-pubkey'})) {
+       if (defined(my $master_key = delete($param{'master-pubkey'}))) {
+           my $decoded = decode_base64($master_key);
+
+           pbs_set_master_pubkey($scfg, $storeid, $decoded);
+           $scfg->{'master-pubkey'} = 1;
+       } else {
+           pbs_delete_master_pubkey($scfg, $storeid);
+       }
+    }
+
+    return $res;
+}
+
+sub on_delete_hook {
+    my ($class, $storeid, $scfg) = @_;
+
+    pbs_delete_password($scfg, $storeid);
+    pbs_delete_encryption_key($scfg, $storeid);
+    pbs_delete_master_pubkey($scfg, $storeid);
+
+    return;
+}
+
+sub parse_volname {
+    my ($class, $volname) = @_;
+
+    if ($volname =~ m!^backup/([^\s_]+)/([^\s_]+)/([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z)$!) {
+       my $btype = $1;
+       my $bid = $2;
+       my $btime = $3;
+       my $format = "pbs-$btype";
+
+       my $name = "$btype/$bid/$btime";
+
+       if ($bid =~ m/^\d+$/) {
+           return ('backup', $name, $bid, undef, undef, undef, $format);
+       } else {
+           return ('backup', $name, undef, undef, undef, undef, $format);
+       }
+    }
+
+    die "unable to parse PBS volume name '$volname'\n";
+}
+
+sub path {
+    my ($class, $scfg, $volname, $storeid, $snapname) = @_;
+
+    die "volume snapshot is not possible on pbs storage"
+       if defined($snapname);
+
+    my ($vtype, $name, $vmid) = $class->parse_volname($volname);
+
+    my $repo = PVE::PBSClient::get_repository($scfg);
+
+    # artificial url - we currently do not use that anywhere
+    my $path = "pbs://$repo/$name";
+    if (defined(my $ns = $scfg->{namespace})) {
+       $ns =~ s|/|%2f|g; # other characters to escape aren't allowed in the namespace schema
+       $path .= "?ns=$ns";
+    }
+
+    return ($path, $vmid, $vtype);
+}
+
+sub create_base {
+    my ($class, $storeid, $scfg, $volname) = @_;
+
+    die "can't create base images in pbs storage\n";
+}
+
+sub clone_image {
+    my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
+
+    die "can't clone images in pbs storage\n";
+}
+
+sub alloc_image {
+    my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
+
+    die "can't allocate space in pbs storage\n";
+}
+
+sub free_image {
+    my ($class, $storeid, $scfg, $volname, $isBase) = @_;
+
+    my ($vtype, $name, $vmid) = $class->parse_volname($volname);
+
+    run_client_cmd($scfg, $storeid, "forget", [ $name ], 1);
+
+    return;
+}
+
+
+sub list_images {
+    my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
+
+    my $res = [];
+
+    return $res;
+}
+
+my sub snapshot_files_encrypted {
+    my ($files) = @_;
+    return 0 if !$files;
+
+    my $any;
+    my $all = 1;
+    for my $file (@$files) {
+       my $fn = $file->{filename};
+       next if $fn eq 'client.log.blob' || $fn eq 'index.json.blob';
+
+       my $crypt = $file->{'crypt-mode'};
+
+       $all = 0 if !$crypt || $crypt ne 'encrypt';
+       $any ||= defined($crypt) && $crypt eq 'encrypt';
+    }
+    return $any && $all;
+}
+
+# TODO: use a client with native rust/proxmox-backup bindings to profit from
+# API schema checks and types
+my sub pbs_api_connect {
+    my ($scfg, $password, $timeout) = @_;
+
+    my $params = {};
+
+    my $user = $scfg->{username} // 'root@pam';
+
+    if (my $tokenid = PVE::AccessControl::pve_verify_tokenid($user, 1)) {
+       $params->{apitoken} = "PBSAPIToken=${tokenid}:${password}";
+    } else {
+       $params->{password} = $password;
+       $params->{username} = $user;
+    }
+
+    if (my $fp = $scfg->{fingerprint}) {
+       $params->{cached_fingerprints}->{uc($fp)} = 1;
+    }
+
+    my $conn = PVE::APIClient::LWP->new(
+       %$params,
+       host => $scfg->{server},
+       port => $scfg->{port} // 8007,
+       timeout => ($timeout // 7), # cope with a 401 (3s api delay) and high latency
+       cookie_name => 'PBSAuthCookie',
+    );
+
+    return $conn;
+}
+
+sub list_volumes {
+    my ($class, $storeid, $scfg, $vmid, $content_types) = @_;
+
+    my $res = [];
+
+    return $res if !grep { $_ eq 'backup' } @$content_types;
+
+    my $password = pbs_get_password($scfg, $storeid);
+    my $conn = pbs_api_connect($scfg, $password, 120);
+    my $datastore = $scfg->{datastore};
+
+    my $param = {};
+    $param->{'backup-id'} = "$vmid" if defined($vmid);
+    $param->{'ns'} = "$scfg->{namespace}" if defined($scfg->{namespace});
+    my $data = eval { $conn->get("/api2/json/admin/datastore/$datastore/snapshots", $param); };
+    die "error listing snapshots - $@" if $@;
+
+    foreach my $item (@$data) {
+       my $btype = $item->{"backup-type"};
+       my $bid = $item->{"backup-id"};
+       my $epoch = $item->{"backup-time"};
+       my $size = $item->{size} // 1;
+
+       next if !($btype eq 'vm' || $btype eq 'ct');
+       next if $bid !~ m/^\d+$/;
+       next if defined($vmid) && $bid ne $vmid;
+
+       my $volid = print_volid($storeid, $btype, $bid, $epoch);
+
+       my $info = {
+           volid => $volid,
+           format => "pbs-$btype",
+           size => $size,
+           content => 'backup',
+           vmid => int($bid),
+           ctime => $epoch,
+           subtype => $btype eq 'vm' ? 'qemu' : 'lxc', # convert to PVE backup type
+       };
+
+       $info->{verification} = $item->{verification} if defined($item->{verification});
+       $info->{notes} = $item->{comment} if defined($item->{comment});
+       $info->{protected} = 1 if $item->{protected};
+       if (defined($item->{fingerprint})) {
+           $info->{encrypted} = $item->{fingerprint};
+       } elsif (snapshot_files_encrypted($item->{files})) {
+           $info->{encrypted} = '1';
+       }
+
+       push @$res, $info;
+    }
+
+    return $res;
+}
+
+sub status {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    my $total = 0;
+    my $free = 0;
+    my $used = 0;
+    my $active = 0;
+
+    eval {
+       my $res = run_client_cmd($scfg, $storeid, "status");
+
+       $active = 1;
+       $total = $res->{total};
+       $used = $res->{used};
+       $free = $res->{avail};
+    };
+    if (my $err = $@) {
+       warn $err;
+    }
+
+    return ($total, $free, $used, $active);
+}
+
+# can also be used for not (yet) added storages, pass $scfg with
+# {
+#   server
+#   user
+#   port          (optional default to 8007)
+#   fingerprint   (optional for trusted certs)
+# }
+sub scan_datastores {
+    my ($scfg, $password) = @_;
+
+    my $conn = pbs_api_connect($scfg, $password);
+
+    my $response = eval { $conn->get('/api2/json/admin/datastore', {}) };
+    die "error fetching datastores - $@" if $@;
+
+    return $response;
+}
+
+sub activate_storage {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    my $password = pbs_get_password($scfg, $storeid);
+
+    my $datastores = eval { scan_datastores($scfg, $password) };
+    die "$storeid: $@" if $@;
+
+    my $datastore = $scfg->{datastore};
+
+    for my $ds (@$datastores) {
+       if ($ds->{store} eq $datastore) {
+           return 1;
+       }
+    }
+
+    die "$storeid: Cannot find datastore '$datastore', check permissions and existence!\n";
+}
+
+sub deactivate_storage {
+    my ($class, $storeid, $scfg, $cache) = @_;
+    return 1;
+}
+
+sub activate_volume {
+    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
+
+    die "volume snapshot is not possible on pbs device" if $snapname;
+
+    return 1;
+}
+
+sub deactivate_volume {
+    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
+
+    die "volume snapshot is not possible on pbs device" if $snapname;
+
+    return 1;
+}
+
+# FIXME remove on the next APIAGE reset.
+# Deprecated, use get_volume_attribute instead.
+sub get_volume_notes {
+    my ($class, $scfg, $storeid, $volname, $timeout) = @_;
+
+    my (undef, $name,  undef, undef, undef, undef, $format) = $class->parse_volname($volname);
+
+    my $data = run_client_cmd($scfg, $storeid, "snapshot", [ "notes", "show", $name ]);
+
+    return $data->{notes};
+}
+
+# FIXME remove on the next APIAGE reset.
+# Deprecated, use update_volume_attribute instead.
+sub update_volume_notes {
+    my ($class, $scfg, $storeid, $volname, $notes, $timeout) = @_;
+
+    my (undef, $name,  undef, undef, undef, undef, $format) = $class->parse_volname($volname);
+
+    run_client_cmd($scfg, $storeid, "snapshot", [ "notes", "update", $name, $notes ], 1);
+
+    return undef;
+}
+
+sub get_volume_attribute {
+    my ($class, $scfg, $storeid, $volname, $attribute) = @_;
+
+    if ($attribute eq 'notes') {
+       return $class->get_volume_notes($scfg, $storeid, $volname);
+    }
+
+    if ($attribute eq 'protected') {
+       my $param = api_param_from_volname($class, $scfg, $volname);
+
+       my $password = pbs_get_password($scfg, $storeid);
+       my $conn = pbs_api_connect($scfg, $password);
+       my $datastore = $scfg->{datastore};
+
+       my $res = eval { $conn->get("/api2/json/admin/datastore/$datastore/$attribute", $param); };
+       if (my $err = $@) {
+           return if $err->{code} == 404; # not supported
+           die $err;
+       }
+       return $res;
+    }
+
+    return;
+}
+
+sub update_volume_attribute {
+    my ($class, $scfg, $storeid, $volname, $attribute, $value) = @_;
+
+    if ($attribute eq 'notes') {
+       return $class->update_volume_notes($scfg, $storeid, $volname, $value);
+    }
+
+    if ($attribute eq 'protected') {
+       my $param = api_param_from_volname($class, $scfg, $volname);
+       $param->{$attribute} = $value;
+
+       my $password = pbs_get_password($scfg, $storeid);
+       my $conn = pbs_api_connect($scfg, $password);
+       my $datastore = $scfg->{datastore};
+
+       eval { $conn->put("/api2/json/admin/datastore/$datastore/$attribute", $param); };
+       if (my $err = $@) {
+           die "Server is not recent enough to support feature '$attribute'\n"
+               if $err->{code} == 404;
+           die $err;
+       }
+       return;
+    }
+
+    die "attribute '$attribute' is not supported for storage type '$scfg->{type}'\n";
+}
+
+sub volume_size_info {
+    my ($class, $scfg, $storeid, $volname, $timeout) = @_;
+
+    my ($vtype, $name,  undef, undef, undef, undef, $format) = $class->parse_volname($volname);
+
+    my $data = run_client_cmd($scfg, $storeid, "files", [ $name ]);
+
+    my $size = 0;
+    foreach my $info (@$data) {
+       if ($info->{size} && $info->{size} =~ /^(\d+)$/) { # untaints
+           $size += $1;
+       }
+    }
+
+    my $used = $size;
+
+    return wantarray ? ($size, $format, $used, undef) : $size;
+}
+
+sub volume_resize {
+    my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
+    die "volume resize is not possible on pbs device";
+}
+
+sub volume_snapshot {
+    my ($class, $scfg, $storeid, $volname, $snap) = @_;
+    die "volume snapshot is not possible on pbs device";
+}
+
+sub volume_snapshot_rollback {
+    my ($class, $scfg, $storeid, $volname, $snap) = @_;
+    die "volume snapshot rollback is not possible on pbs device";
+}
+
+sub volume_snapshot_delete {
+    my ($class, $scfg, $storeid, $volname, $snap) = @_;
+    die "volume snapshot delete is not possible on pbs device";
+}
+
+sub volume_has_feature {
+    my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
+
+    return undef;
+}
+
+1;
diff --git a/src/PVE/Storage/Plugin.pm b/src/PVE/Storage/Plugin.pm
new file mode 100644 (file)
index 0000000..c323085
--- /dev/null
@@ -0,0 +1,1701 @@
+package PVE::Storage::Plugin;
+
+use strict;
+use warnings;
+
+use Encode qw(decode);
+use Fcntl ':mode';
+use File::chdir;
+use File::Path;
+use File::Basename;
+use File::stat qw();
+
+use PVE::Tools qw(run_command);
+use PVE::JSONSchema qw(get_standard_option register_standard_option);
+use PVE::Cluster qw(cfs_register_file);
+
+use JSON;
+
+use base qw(PVE::SectionConfig);
+
+use constant COMPRESSOR_RE => 'gz|lzo|zst';
+
+use constant LOG_EXT => ".log";
+use constant NOTES_EXT => ".notes";
+
+our @COMMON_TAR_FLAGS = qw(
+    --one-file-system
+    -p --sparse --numeric-owner --acls
+    --xattrs --xattrs-include=user.* --xattrs-include=security.capability
+    --warning=no-file-ignored --warning=no-xattr-write
+);
+
+our @SHARED_STORAGE = (
+    'iscsi',
+    'nfs',
+    'cifs',
+    'rbd',
+    'cephfs',
+    'iscsidirect',
+    'glusterfs',
+    'zfs',
+    'drbd',
+    'pbs',
+);
+
+our $QCOW2_PREALLOCATION = {
+    off => 1,
+    metadata => 1,
+    falloc => 1,
+    full => 1,
+};
+
+our $RAW_PREALLOCATION = {
+    off => 1,
+    falloc => 1,
+    full => 1,
+};
+
+our $MAX_VOLUMES_PER_GUEST = 1024;
+
+cfs_register_file ('storage.cfg',
+                  sub { __PACKAGE__->parse_config(@_); },
+                  sub { __PACKAGE__->write_config(@_); });
+
+my %prune_option = (
+    optional => 1,
+    type => 'integer', minimum => '0',
+    format_description => 'N',
+);
+
+our $prune_backups_format = {
+    'keep-all' => {
+       type => 'boolean',
+       description => 'Keep all backups. Conflicts with the other options when true.',
+       optional => 1,
+    },
+    'keep-last' => {
+       %prune_option,
+       description => 'Keep the last <N> backups.',
+    },
+    'keep-hourly' => {
+       %prune_option,
+       description => 'Keep backups for the last <N> different hours. If there is more' .
+                      'than one backup for a single hour, only the latest one is kept.'
+    },
+    'keep-daily' => {
+       %prune_option,
+       description => 'Keep backups for the last <N> different days. If there is more' .
+                      'than one backup for a single day, only the latest one is kept.'
+    },
+    'keep-weekly' => {
+       %prune_option,
+       description => 'Keep backups for the last <N> different weeks. If there is more' .
+                      'than one backup for a single week, only the latest one is kept.'
+    },
+    'keep-monthly' => {
+       %prune_option,
+       description => 'Keep backups for the last <N> different months. If there is more' .
+                      'than one backup for a single month, only the latest one is kept.'
+    },
+    'keep-yearly' => {
+       %prune_option,
+       description => 'Keep backups for the last <N> different years. If there is more' .
+                      'than one backup for a single year, only the latest one is kept.'
+    },
+};
+PVE::JSONSchema::register_format('prune-backups', $prune_backups_format, \&validate_prune_backups);
+sub validate_prune_backups {
+    my ($prune_backups) = @_;
+
+    my $res = { $prune_backups->%* };
+
+    my $keep_all = delete $res->{'keep-all'};
+
+    if (scalar(grep { $_ > 0 } values %{$res}) == 0) {
+       $res = { 'keep-all' => 1 };
+    } elsif ($keep_all) {
+       die "keep-all cannot be set together with other options.\n";
+    }
+
+    return $res;
+}
+register_standard_option('prune-backups', {
+    description => "The retention options with shorter intervals are processed first " .
+                  "with --keep-last being the very first one. Each option covers a " .
+                  "specific period of time. We say that backups within this period " .
+                  "are covered by this option. The next option does not take care " .
+                  "of already covered backups and only considers older backups.",
+    optional => 1,
+    type => 'string',
+    format => 'prune-backups',
+});
+
+my $defaultData = {
+    propertyList => {
+       type => { description => "Storage type." },
+       storage => get_standard_option('pve-storage-id',
+           { completion => \&PVE::Storage::complete_storage }),
+       nodes => get_standard_option('pve-node-list', { optional => 1 }),
+       content => {
+           description => "Allowed content types.\n\nNOTE: the value " .
+               "'rootdir' is used for Containers, and value 'images' for VMs.\n",
+           type => 'string', format => 'pve-storage-content-list',
+           optional => 1,
+           completion => \&PVE::Storage::complete_content_type,
+       },
+       disable => {
+           description => "Flag to disable the storage.",
+           type => 'boolean',
+           optional => 1,
+       },
+       maxfiles => {
+           description => "Deprecated: use 'prune-backups' instead. " .
+               "Maximal number of backup files per VM. Use '0' for unlimited.",
+           type => 'integer',
+           minimum => 0,
+           optional => 1,
+       },
+       'prune-backups' => get_standard_option('prune-backups'),
+       'max-protected-backups' => {
+           description => "Maximal number of protected backups per guest. Use '-1' for unlimited.",
+           type => 'integer',
+           minimum => -1,
+           optional => 1,
+           default => "Unlimited for users with Datastore.Allocate privilege, 5 for other users",
+       },
+       shared => {
+           description => "Mark storage as shared.",
+           type => 'boolean',
+           optional => 1,
+       },
+       subdir => {
+           description => "Subdir to mount.",
+           type => 'string', format => 'pve-storage-path',
+           optional => 1,
+       },
+       'format' => {
+           description => "Default image format.",
+           type => 'string', format => 'pve-storage-format',
+           optional => 1,
+       },
+       preallocation => {
+           description => "Preallocation mode for raw and qcow2 images. " .
+               "Using 'metadata' on raw images results in preallocation=off.",
+           type => 'string', enum => ['off', 'metadata', 'falloc', 'full'],
+           default => 'metadata',
+           optional => 1,
+       },
+       'content-dirs' => {
+           description => "Overrides for default content type directories.",
+           type => "string", format => "pve-dir-override-list",
+           optional => 1,
+       },
+    },
+};
+
+sub content_hash_to_string {
+    my $hash = shift;
+
+    my @cta;
+    foreach my $ct (keys %$hash) {
+       push @cta, $ct if $hash->{$ct};
+    }
+
+    return join(',', @cta);
+}
+
+sub valid_content_types {
+    my ($type) = @_;
+
+    my $def = $defaultData->{plugindata}->{$type};
+
+    return {} if !$def;
+
+    return $def->{content}->[0];
+}
+
+sub dirs_hash_to_string {
+    my $hash = shift;
+
+    return join(',', map { "$_=$hash->{$_}" } sort keys %$hash);
+}
+
+sub default_format {
+    my ($scfg) = @_;
+
+    my $type = $scfg->{type};
+    my $def = $defaultData->{plugindata}->{$type};
+
+    my $def_format = 'raw';
+    my $valid_formats = [ $def_format ];
+
+    if (defined($def->{format})) {
+       $def_format = $scfg->{format} || $def->{format}->[1];
+       $valid_formats = [ sort keys %{$def->{format}->[0]} ];
+    }
+
+    return wantarray ? ($def_format, $valid_formats) : $def_format;
+}
+
+PVE::JSONSchema::register_format('pve-storage-path', \&verify_path);
+sub verify_path {
+    my ($path, $noerr) = @_;
+
+    # fixme: exclude more shell meta characters?
+    # we need absolute paths
+    if ($path !~ m|^/[^;\(\)]+|) {
+       return undef if $noerr;
+       die "value does not look like a valid absolute path\n";
+    }
+    return $path;
+}
+
+PVE::JSONSchema::register_format('pve-storage-server', \&verify_server);
+sub verify_server {
+    my ($server, $noerr) = @_;
+
+    if (!(PVE::JSONSchema::pve_verify_ip($server, 1) ||
+          PVE::JSONSchema::pve_verify_dns_name($server, 1)))
+    {
+       return undef if $noerr;
+       die "value does not look like a valid server name or IP address\n";
+    }
+    return $server;
+}
+
+PVE::JSONSchema::register_format('pve-storage-vgname', \&parse_lvm_name);
+sub parse_lvm_name {
+    my ($name, $noerr) = @_;
+
+    if ($name !~ m/^[a-z0-9][a-z0-9\-\_\.]*[a-z0-9]$/i) {
+       return undef if $noerr;
+       die "lvm name '$name' contains illegal characters\n";
+    }
+
+    return $name;
+}
+
+# fixme: do we need this
+#PVE::JSONSchema::register_format('pve-storage-portal', \&verify_portal);
+#sub verify_portal {
+#    my ($portal, $noerr) = @_;
+#
+#    # IP with optional port
+#    if ($portal !~ m/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d+)?$/) {
+#      return undef if $noerr;
+#      die "value does not look like a valid portal address\n";
+#    }
+#    return $portal;
+#}
+
+PVE::JSONSchema::register_format('pve-storage-portal-dns', \&verify_portal_dns);
+sub verify_portal_dns {
+    my ($portal, $noerr) = @_;
+
+    # IP or DNS name with optional port
+    if (!PVE::Tools::parse_host_and_port($portal)) {
+       return undef if $noerr;
+       die "value does not look like a valid portal address\n";
+    }
+    return $portal;
+}
+
+PVE::JSONSchema::register_format('pve-storage-content', \&verify_content);
+sub verify_content {
+    my ($ct, $noerr) = @_;
+
+    my $valid_content = valid_content_types('dir'); # dir includes all types
+
+    if (!$valid_content->{$ct}) {
+       return undef if $noerr;
+       die "invalid content type '$ct'\n";
+    }
+
+    return $ct;
+}
+
+PVE::JSONSchema::register_format('pve-storage-format', \&verify_format);
+sub verify_format {
+    my ($fmt, $noerr) = @_;
+
+    if ($fmt !~ m/(raw|qcow2|vmdk|subvol)/) {
+       return undef if $noerr;
+       die "invalid format '$fmt'\n";
+    }
+
+    return $fmt;
+}
+
+PVE::JSONSchema::register_format('pve-storage-options', \&verify_options);
+sub verify_options {
+    my ($value, $noerr) = @_;
+
+    # mount options (see man fstab)
+    if ($value !~ m/^\S+$/) {
+       return undef if $noerr;
+       die "invalid options '$value'\n";
+    }
+
+    return $value;
+}
+
+PVE::JSONSchema::register_format('pve-volume-id', \&parse_volume_id);
+sub parse_volume_id {
+    my ($volid, $noerr) = @_;
+
+    if ($volid =~ m/^([a-z][a-z0-9\-\_\.]*[a-z0-9]):(.+)$/i) {
+       return wantarray ? ($1, $2) : $1;
+    }
+    return undef if $noerr;
+    die "unable to parse volume ID '$volid'\n";
+}
+
+PVE::JSONSchema::register_format('pve-dir-override', \&verify_dir_override);
+sub verify_dir_override {
+    my ($value, $noerr) = @_;
+
+    if ($value =~ m/^([a-z]+)=([^.]*(?:\.?[^.]+)+)$/) {
+       my ($content_type, $relative_path) = ($1, $2);
+       if (verify_content($content_type, $noerr)) {
+           # linux has 4k max-path, but limit total length to lower as its concat'd for full path
+           if (length($relative_path) < 1023 && !(grep { length($_) >= 255 } split('/', $relative_path))) {
+               return $value;
+           }
+       }
+    }
+
+    return undef if $noerr;
+    die "invalid override '$value'\n";
+}
+
+sub private {
+    return $defaultData;
+}
+
+sub parse_section_header {
+    my ($class, $line) = @_;
+
+    if ($line =~ m/^(\S+):\s*(\S+)\s*$/) {
+       my ($type, $storeid) = (lc($1), $2);
+       my $errmsg = undef; # set if you want to skip whole section
+       eval { PVE::JSONSchema::parse_storage_id($storeid); };
+       $errmsg = $@ if $@;
+       my $config = {}; # to return additional attributes
+       return ($type, $storeid, $errmsg, $config);
+    }
+    return undef;
+}
+
+sub decode_value {
+    my ($class, $type, $key, $value) = @_;
+
+    my $def = $defaultData->{plugindata}->{$type};
+
+    if ($key eq 'content') {
+       my $valid_content = $def->{content}->[0];
+
+       my $res = {};
+
+       foreach my $c (PVE::Tools::split_list($value)) {
+           if (!$valid_content->{$c}) {
+               warn "storage does not support content type '$c'\n";
+               next;
+           }
+           $res->{$c} = 1;
+       }
+
+       if ($res->{none} && scalar (keys %$res) > 1) {
+           die "unable to combine 'none' with other content types\n";
+       }
+
+       if (scalar(keys $res->%*) == 0 && !$valid_content->{none}) {
+           die "storage does not support content type 'none'\n";
+       }
+
+       return $res;
+    } elsif ($key eq 'format') {
+       my $valid_formats = $def->{format}->[0];
+
+       if (!$valid_formats->{$value}) {
+           warn "storage does not support format '$value'\n";
+           next;
+       }
+
+       return $value;
+    } elsif ($key eq 'nodes') {
+       my $res = {};
+
+       foreach my $node (PVE::Tools::split_list($value)) {
+           if (PVE::JSONSchema::pve_verify_node_name($node)) {
+               $res->{$node} = 1;
+           }
+       }
+
+       # fixme:
+       # no node restrictions for local storage
+       #if ($storeid && $storeid eq 'local' && scalar(keys(%$res))) {
+       #    die "storage '$storeid' does not allow node restrictions\n";
+       #}
+
+       return $res;
+    } elsif ($key eq 'content-dirs') {
+       my $valid_content = $def->{content}->[0];
+       my $res = {};
+
+       foreach my $dir (PVE::Tools::split_list($value)) {
+           my ($content, $path) = split(/=/, $dir, 2);
+
+           if (!$valid_content->{$content}) {
+               warn "storage does not support content type '$content'\n";
+               next;
+           }
+
+           $res->{$content} = $path;
+       }
+
+       return $res;
+    }
+
+    return $value;
+}
+
+sub encode_value {
+    my ($class, $type, $key, $value) = @_;
+
+    if ($key eq 'nodes') {
+        return join(',', keys(%$value));
+    } elsif ($key eq 'content') {
+       my $res = content_hash_to_string($value) || 'none';
+       return $res;
+    } elsif ($key eq 'content-dirs') {
+       my $res = dirs_hash_to_string($value);
+       return $res;
+    }
+
+    return $value;
+}
+
+sub parse_config {
+    my ($class, $filename, $raw) = @_;
+
+    my $cfg = $class->SUPER::parse_config($filename, $raw);
+    my $ids = $cfg->{ids};
+
+    # make sure we have a reasonable 'local:' storage
+    # we want 'local' to be always the same 'type' (on all cluster nodes)
+    if (!$ids->{local} || $ids->{local}->{type} ne 'dir' ||
+       ($ids->{local}->{path} && $ids->{local}->{path} ne '/var/lib/vz')) {
+       $ids->{local} = {
+           type => 'dir',
+           priority => 0, # force first entry
+           path => '/var/lib/vz',
+           'prune-backups' => 'keep-all=1',
+           content => {
+               backup => 1,
+               images => 1,
+               iso => 1,
+               rootdir => 1,
+               snippets => 1,
+               vztmpl => 1,
+           },
+       };
+    }
+
+    # make sure we have a path
+    $ids->{local}->{path} = '/var/lib/vz' if !$ids->{local}->{path};
+
+    # remove node restrictions for local storage
+    delete($ids->{local}->{nodes});
+
+    foreach my $storeid (keys %$ids) {
+       my $d = $ids->{$storeid};
+       my $type = $d->{type};
+
+       my $def = $defaultData->{plugindata}->{$type};
+
+       if ($def->{content}) {
+           $d->{content} = $def->{content}->[1] if !$d->{content};
+       }
+       if (grep { $_ eq $type }  @SHARED_STORAGE) {
+           $d->{shared} = 1;
+       }
+    }
+
+    return $cfg;
+}
+
+sub preallocation_cmd_option {
+    my ($scfg, $fmt) = @_;
+
+    my $prealloc = $scfg->{preallocation};
+
+    if ($fmt eq 'qcow2') {
+       $prealloc = $prealloc // 'metadata';
+
+       die "preallocation mode '$prealloc' not supported by format '$fmt'\n"
+           if !$QCOW2_PREALLOCATION->{$prealloc};
+
+       return "preallocation=$prealloc";
+    } elsif ($fmt eq 'raw') {
+       $prealloc = $prealloc // 'off';
+       $prealloc = 'off' if $prealloc eq 'metadata';
+
+       die "preallocation mode '$prealloc' not supported by format '$fmt'\n"
+           if !$RAW_PREALLOCATION->{$prealloc};
+
+       return "preallocation=$prealloc";
+    }
+
+    return;
+}
+
+# Storage implementation
+
+# called during addition of storage (before the new storage config got written)
+# die to abort addition if there are (grave) problems
+# NOTE: runs in a storage config *locked* context
+sub on_add_hook {
+    my ($class, $storeid, $scfg, %param) = @_;
+
+    # do nothing by default
+    return undef;
+}
+
+# called during storage configuration update (before the updated storage config got written)
+# die to abort the update if there are (grave) problems
+# NOTE: runs in a storage config *locked* context
+sub on_update_hook {
+    my ($class, $storeid, $scfg, %param) = @_;
+
+    # do nothing by default
+    return undef;
+}
+
+# called during deletion of storage (before the new storage config got written)
+# and if the activate check on addition fails, to cleanup all storage traces
+# which on_add_hook may have created.
+# die to abort deletion if there are (very grave) problems
+# NOTE: runs in a storage config *locked* context
+sub on_delete_hook {
+    my ($class, $storeid, $scfg) = @_;
+
+    # do nothing by default
+    return undef;
+}
+
+sub cluster_lock_storage {
+    my ($class, $storeid, $shared, $timeout, $func, @param) = @_;
+
+    my $res;
+    if (!$shared) {
+       my $lockid = "pve-storage-$storeid";
+       my $lockdir = "/var/lock/pve-manager";
+       mkdir $lockdir;
+       $res = PVE::Tools::lock_file("$lockdir/$lockid", $timeout, $func, @param);
+       die $@ if $@;
+    } else {
+       $res = PVE::Cluster::cfs_lock_storage($storeid, $timeout, $func, @param);
+       die $@ if $@;
+    }
+    return $res;
+}
+
+sub parse_name_dir {
+    my $name = shift;
+
+    if ($name =~ m!^((base-)?[^/\s]+\.(raw|qcow2|vmdk|subvol))$!) {
+       return ($1, $3, $2); # (name, format, isBase)
+    }
+
+    die "unable to parse volume filename '$name'\n";
+}
+
+sub parse_volname {
+    my ($class, $volname) = @_;
+
+    if ($volname =~ m!^(\d+)/(\S+)/(\d+)/(\S+)$!) {
+       my ($basedvmid, $basename) = ($1, $2);
+       parse_name_dir($basename);
+       my ($vmid, $name) = ($3, $4);
+       my (undef, $format, $isBase) = parse_name_dir($name);
+       return ('images', $name, $vmid, $basename, $basedvmid, $isBase, $format);
+    } elsif ($volname =~ m!^(\d+)/(\S+)$!) {
+       my ($vmid, $name) = ($1, $2);
+       my (undef, $format, $isBase) = parse_name_dir($name);
+       return ('images', $name, $vmid, undef, undef, $isBase, $format);
+    } elsif ($volname =~ m!^iso/([^/]+$PVE::Storage::ISO_EXT_RE_0)$!) {
+       return ('iso', $1);
+    } elsif ($volname =~ m!^vztmpl/([^/]+$PVE::Storage::VZTMPL_EXT_RE_1)$!) {
+       return ('vztmpl', $1);
+    } elsif ($volname =~ m!^rootdir/(\d+)$!) {
+       return ('rootdir', $1, $1);
+    } elsif ($volname =~ m!^backup/([^/]+$PVE::Storage::BACKUP_EXT_RE_2)$!) {
+       my $fn = $1;
+       if ($fn =~ m/^vzdump-(openvz|lxc|qemu)-(\d+)-.+/) {
+           return ('backup', $fn, $2);
+       }
+       return ('backup', $fn);
+    } elsif ($volname =~ m!^snippets/([^/]+)$!) {
+       return ('snippets', $1);
+    }
+
+    die "unable to parse directory volume name '$volname'\n";
+}
+
+my $vtype_subdirs = {
+    images => 'images',
+    rootdir => 'private',
+    iso => 'template/iso',
+    vztmpl => 'template/cache',
+    backup => 'dump',
+    snippets => 'snippets',
+};
+
+sub get_vtype_subdirs {
+    return $vtype_subdirs;
+}
+
+sub get_subdir {
+    my ($class, $scfg, $vtype) = @_;
+
+    my $path = $scfg->{path};
+
+    die "storage definition has no path\n" if !$path;
+    die "unknown vtype '$vtype'\n" if !exists($vtype_subdirs->{$vtype});
+
+    my $subdir = $scfg->{"content-dirs"}->{$vtype} // $vtype_subdirs->{$vtype};
+
+    return "$path/$subdir";
+}
+
+sub filesystem_path {
+    my ($class, $scfg, $volname, $snapname) = @_;
+
+    my ($vtype, $name, $vmid, undef, undef, $isBase, $format) =
+       $class->parse_volname($volname);
+
+    # Note: qcow2/qed has internal snapshot, so path is always
+    # the same (with or without snapshot => same file).
+    die "can't snapshot this image format\n"
+       if defined($snapname) && $format !~ m/^(qcow2|qed)$/;
+
+    my $dir = $class->get_subdir($scfg, $vtype);
+
+    $dir .= "/$vmid" if $vtype eq 'images';
+
+    my $path = "$dir/$name";
+
+    return wantarray ? ($path, $vmid, $vtype) : $path;
+}
+
+sub path {
+    my ($class, $scfg, $volname, $storeid, $snapname) = @_;
+
+    return $class->filesystem_path($scfg, $volname, $snapname);
+}
+
+sub create_base {
+    my ($class, $storeid, $scfg, $volname) = @_;
+
+    # this only works for file based storage types
+    die "storage definition has no path\n" if !$scfg->{path};
+
+    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) =
+       $class->parse_volname($volname);
+
+    die "create_base on wrong vtype '$vtype'\n" if $vtype ne 'images';
+
+    die "create_base not possible with base image\n" if $isBase;
+
+    my $path = $class->filesystem_path($scfg, $volname);
+
+    my ($size, undef, $used, $parent) = file_size_info($path);
+    die "file_size_info on '$volname' failed\n" if !($format && defined($size));
+
+    die "volname '$volname' contains wrong information about parent\n"
+       if $basename && (!$parent || $parent ne "../$basevmid/$basename");
+
+    my $newname = $name;
+    $newname =~ s/^vm-/base-/;
+
+    my $newvolname = $basename ? "$basevmid/$basename/$vmid/$newname" :
+       "$vmid/$newname";
+
+    my $newpath = $class->filesystem_path($scfg, $newvolname);
+
+    die "file '$newpath' already exists\n" if -f $newpath;
+
+    rename($path, $newpath) ||
+       die "rename '$path' to '$newpath' failed - $!\n";
+
+    # We try to protect base volume
+
+    chmod(0444, $newpath); # nobody should write anything
+
+    # also try to set immutable flag
+    eval { run_command(['/usr/bin/chattr', '+i', $newpath]); };
+    warn $@ if $@;
+
+    return $newvolname;
+}
+
+my $get_vm_disk_number = sub {
+    my ($disk_name, $scfg, $vmid, $suffix) = @_;
+
+    my $disk_regex = qr/(vm|base)-$vmid-disk-(\d+)$suffix/;
+
+    my $type = $scfg->{type};
+    my $def = { %{$defaultData->{plugindata}->{$type}} };
+
+    my $valid = $def->{format}[0];
+    if ($valid->{subvol}) {
+       $disk_regex = qr/(vm|base|subvol|basevol)-$vmid-disk-(\d+)/;
+    }
+
+    if ($disk_name =~ m/$disk_regex/) {
+       return $2;
+    }
+
+    return undef;
+};
+
+sub get_next_vm_diskname {
+    my ($disk_list, $storeid, $vmid, $fmt, $scfg, $add_fmt_suffix) = @_;
+
+    $fmt //= '';
+    my $prefix = ($fmt eq 'subvol') ? 'subvol' : 'vm';
+    my $suffix = $add_fmt_suffix ? ".$fmt" : '';
+
+    my $disk_ids = {};
+    foreach my $disk (@$disk_list) {
+       my $disknum = $get_vm_disk_number->($disk, $scfg, $vmid, $suffix);
+       $disk_ids->{$disknum} = 1 if defined($disknum);
+    }
+
+    for (my $i = 0; $i < $MAX_VOLUMES_PER_GUEST; $i++) {
+       if (!$disk_ids->{$i}) {
+           return "$prefix-$vmid-disk-$i$suffix";
+       }
+    }
+
+    die "unable to allocate an image name for VM $vmid in storage '$storeid'\n"
+}
+
+sub find_free_diskname {
+    my ($class, $storeid, $scfg, $vmid, $fmt, $add_fmt_suffix) = @_;
+
+    my $disks = $class->list_images($storeid, $scfg, $vmid);
+
+    my $disk_list = [ map { $_->{volid} } @$disks ];
+
+    return get_next_vm_diskname($disk_list, $storeid, $vmid, $fmt, $scfg, $add_fmt_suffix);
+}
+
+sub clone_image {
+    my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
+
+    # this only works for file based storage types
+    die "storage definition has no path\n" if !$scfg->{path};
+
+    my ($vtype, $basename, $basevmid, undef, undef, $isBase, $format) =
+       $class->parse_volname($volname);
+
+    die "clone_image on wrong vtype '$vtype'\n" if $vtype ne 'images';
+
+    die "this storage type does not support clone_image on snapshot\n" if $snap;
+
+    die "this storage type does not support clone_image on subvolumes\n" if $format eq 'subvol';
+
+    die "clone_image only works on base images\n" if !$isBase;
+
+    my $imagedir = $class->get_subdir($scfg, 'images');
+    $imagedir .= "/$vmid";
+
+    mkpath $imagedir;
+
+    my $name = $class->find_free_diskname($storeid, $scfg, $vmid, "qcow2", 1);
+
+    warn "clone $volname: $vtype, $name, $vmid to $name (base=../$basevmid/$basename)\n";
+
+    my $newvol = "$basevmid/$basename/$vmid/$name";
+
+    my $path = $class->filesystem_path($scfg, $newvol);
+
+    # Note: we use relative paths, so we need to call chdir before qemu-img
+    eval {
+       local $CWD = $imagedir;
+
+       my $cmd = ['/usr/bin/qemu-img', 'create', '-b', "../$basevmid/$basename",
+                  '-F', $format, '-f', 'qcow2', $path];
+
+       run_command($cmd);
+    };
+    my $err = $@;
+
+    die $err if $err;
+
+    return $newvol;
+}
+
+sub alloc_image {
+    my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
+
+    my $imagedir = $class->get_subdir($scfg, 'images');
+    $imagedir .= "/$vmid";
+
+    mkpath $imagedir;
+
+    $name = $class->find_free_diskname($storeid, $scfg, $vmid, $fmt, 1) if !$name;
+
+    my (undef, $tmpfmt) = parse_name_dir($name);
+
+    die "illegal name '$name' - wrong extension for format ('$tmpfmt != '$fmt')\n"
+       if $tmpfmt ne $fmt;
+
+    my $path = "$imagedir/$name";
+
+    die "disk image '$path' already exists\n" if -e $path;
+
+    if ($fmt eq 'subvol') {
+       # only allow this if size = 0, so that user knows what he is doing
+       die "storage does not support subvol quotas\n" if $size != 0;
+
+       my $old_umask = umask(0022);
+       my $err;
+       mkdir($path) or $err = "unable to create subvol '$path' - $!\n";
+       umask $old_umask;
+       die $err if $err;
+    } else {
+       my $cmd = ['/usr/bin/qemu-img', 'create'];
+
+       my $prealloc_opt = preallocation_cmd_option($scfg, $fmt);
+       push @$cmd, '-o', $prealloc_opt if defined($prealloc_opt);
+
+       push @$cmd, '-f', $fmt, $path, "${size}K";
+
+       eval { run_command($cmd, errmsg => "unable to create image"); };
+       if ($@) {
+           unlink $path;
+           rmdir $imagedir;
+           die "$@";
+       }
+    }
+
+    return "$vmid/$name";
+}
+
+sub free_image {
+    my ($class, $storeid, $scfg, $volname, $isBase, $format) = @_;
+
+    die "cannot remove protected volume '$volname' on '$storeid'\n"
+       if $class->get_volume_attribute($scfg, $storeid, $volname, 'protected');
+
+    my $path = $class->filesystem_path($scfg, $volname);
+
+    if ($isBase) {
+       # try to remove immutable flag
+       eval { run_command(['/usr/bin/chattr', '-i', $path]); };
+       warn $@ if $@;
+    }
+
+    if (defined($format) && ($format eq 'subvol')) {
+       File::Path::remove_tree($path);
+    } else {
+       if (!(-f $path || -l $path)) {
+           warn "disk image '$path' does not exist\n";
+           return undef;
+       }
+
+       unlink($path) || die "unlink '$path' failed - $!\n";
+    }
+
+    # try to cleanup directory to not clutter storage with empty $vmid dirs if
+    # all images from a guest got deleted
+    my $dir = dirname($path);
+    rmdir($dir);
+
+    return undef;
+}
+
+sub file_size_info {
+    my ($filename, $timeout) = @_;
+
+    my $st = File::stat::stat($filename);
+
+    if (!defined($st)) {
+       my $extramsg = -l $filename ? ' - dangling symlink?' : '';
+       warn "failed to stat '$filename'$extramsg\n";
+       return undef;
+    }
+
+    if (S_ISDIR($st->mode)) {
+       return wantarray ? (0, 'subvol', 0, undef, $st->ctime) : 1;
+    }
+
+    my $json = '';
+    eval {
+       run_command(['/usr/bin/qemu-img', 'info', '--output=json', $filename],
+           timeout => $timeout,
+           outfunc => sub { $json .= shift },
+           errfunc => sub { warn "$_[0]\n" }
+       );
+    };
+    warn $@ if $@;
+
+    my $info = eval { decode_json($json) };
+    if (my $err = $@) {
+       warn "could not parse qemu-img info command output for '$filename' - $err\n";
+       return wantarray ? (undef, undef, undef, undef, $st->ctime) : undef;
+    }
+
+    my ($size, $format, $used, $parent) = $info->@{qw(virtual-size format actual-size backing-filename)};
+
+    ($size) = ($size =~ /^(\d+)$/) or die "size '$size' not an integer\n"; # untaint
+    # coerce back from string
+    $size = int($size);
+    ($used) = ($used =~ /^(\d+)$/) or die "used '$used' not an integer\n"; # untaint
+    # coerce back from string
+    $used = int($used);
+    ($format) = ($format =~ /^(\S+)$/) or die "format '$format' includes whitespace\n"; # untaint
+    if (defined($parent)) {
+       ($parent) = ($parent =~ /^(\S+)$/) or die "parent '$parent' includes whitespace\n"; # untaint
+    }
+    return wantarray ? ($size, $format, $used, $parent, $st->ctime) : $size;
+}
+
+# FIXME remove on the next APIAGE reset.
+# Deprecated, use get_volume_attribute instead.
+sub get_volume_notes {
+    my ($class, $scfg, $storeid, $volname, $timeout) = @_;
+
+    die "volume notes are not supported for $class";
+}
+
+# FIXME remove on the next APIAGE reset.
+# Deprecated, use update_volume_attribute instead.
+sub update_volume_notes {
+    my ($class, $scfg, $storeid, $volname, $notes, $timeout) = @_;
+
+    die "volume notes are not supported for $class";
+}
+
+# Returns undef if the attribute is not supported for the volume.
+# Should die if there is an error fetching the attribute.
+# Possible attributes:
+# notes     - user-provided comments/notes.
+# protected - not to be removed by free_image, and for backups, ignored when pruning.
+sub get_volume_attribute {
+    my ($class, $scfg, $storeid, $volname, $attribute) = @_;
+
+    if ($attribute eq 'notes') {
+        my $notes = eval { $class->get_volume_notes($scfg, $storeid, $volname); };
+        if (my $err = $@) {
+            return if $err =~ m/^volume notes are not supported/;
+            die $err;
+        }
+        return $notes;
+    }
+
+    return;
+}
+
+# Dies if the attribute is not supported for the volume.
+sub update_volume_attribute {
+    my ($class, $scfg, $storeid, $volname, $attribute, $value) = @_;
+
+    if ($attribute eq 'notes') {
+       $class->update_volume_notes($scfg, $storeid, $volname, $value);
+    }
+
+    die "attribute '$attribute' is not supported for storage type '$scfg->{type}'\n";
+}
+
+sub volume_size_info {
+    my ($class, $scfg, $storeid, $volname, $timeout) = @_;
+    my $path = $class->filesystem_path($scfg, $volname);
+    return file_size_info($path, $timeout);
+
+}
+
+sub volume_resize {
+    my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
+
+    die "can't resize this image format\n" if $volname !~ m/\.(raw|qcow2)$/;
+
+    return 1 if $running;
+
+    my $path = $class->filesystem_path($scfg, $volname);
+
+    my $format = ($class->parse_volname($volname))[6];
+
+    my $cmd = ['/usr/bin/qemu-img', 'resize', '-f', $format, $path , $size];
+
+    run_command($cmd, timeout => 10);
+
+    return undef;
+}
+
+sub volume_snapshot {
+    my ($class, $scfg, $storeid, $volname, $snap) = @_;
+
+    die "can't snapshot this image format\n" if $volname !~ m/\.(qcow2|qed)$/;
+
+    my $path = $class->filesystem_path($scfg, $volname);
+
+    my $cmd = ['/usr/bin/qemu-img', 'snapshot','-c', $snap, $path];
+
+    run_command($cmd);
+
+    return undef;
+}
+
+# Asserts that a rollback to $snap on $volname is possible.
+# If certain snapshots are preventing the rollback and $blockers is an array
+# reference, the snapshot names can be pushed onto $blockers prior to dying.
+sub volume_rollback_is_possible {
+    my ($class, $scfg, $storeid, $volname, $snap, $blockers) = @_;
+
+    return 1;
+}
+
+sub volume_snapshot_rollback {
+    my ($class, $scfg, $storeid, $volname, $snap) = @_;
+
+    die "can't rollback snapshot this image format\n" if $volname !~ m/\.(qcow2|qed)$/;
+
+    my $path = $class->filesystem_path($scfg, $volname);
+
+    my $cmd = ['/usr/bin/qemu-img', 'snapshot','-a', $snap, $path];
+
+    run_command($cmd);
+
+    return undef;
+}
+
+sub volume_snapshot_delete {
+    my ($class, $scfg, $storeid, $volname, $snap, $running) = @_;
+
+    die "can't delete snapshot for this image format\n" if $volname !~ m/\.(qcow2|qed)$/;
+
+    return 1 if $running;
+
+    my $path = $class->filesystem_path($scfg, $volname);
+
+    $class->deactivate_volume($storeid, $scfg, $volname, $snap, {});
+
+    my $cmd = ['/usr/bin/qemu-img', 'snapshot','-d', $snap, $path];
+
+    run_command($cmd);
+
+    return undef;
+}
+
+sub volume_snapshot_needs_fsfreeze {
+
+    return 0;
+}
+sub storage_can_replicate {
+    my ($class, $scfg, $storeid, $format) = @_;
+
+    return 0;
+}
+
+sub volume_has_feature {
+    my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running, $opts) = @_;
+
+    my $features = {
+       snapshot => {
+           current => { qcow2 => 1 },
+           snap => { qcow2 => 1 },
+       },
+       clone => {
+           base => { qcow2 => 1, raw => 1, vmdk => 1 },
+       },
+       template => {
+           current => { qcow2 => 1, raw => 1, vmdk => 1, subvol => 1 },
+       },
+       copy => {
+           base => { qcow2 => 1, raw => 1, vmdk => 1 },
+           current => { qcow2 => 1, raw => 1, vmdk => 1 },
+           snap => { qcow2 => 1 },
+       },
+       sparseinit => {
+           base => { qcow2 => 1, raw => 1, vmdk => 1 },
+           current => { qcow2 => 1, raw => 1, vmdk => 1 },
+       },
+       rename => {
+           current => {qcow2 => 1, raw => 1, vmdk => 1},
+       },
+    };
+
+    if ($feature eq 'clone') {
+       if (
+           defined($opts->{valid_target_formats})
+           && !(grep { $_ eq 'qcow2' } @{$opts->{valid_target_formats}})
+       ) {
+           return 0; # clone_image creates a qcow2 volume
+       }
+    } elsif ($feature eq 'rename') {
+       return 0 if $class->can('api') && $class->api() < 10;
+    }
+
+
+    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) = $class->parse_volname($volname);
+
+    my $key = undef;
+    if($snapname){
+        $key = 'snap';
+    }else{
+        $key =  $isBase ? 'base' : 'current';
+    }
+
+    return 1 if defined($features->{$feature}->{$key}->{$format});
+
+    return undef;
+}
+
+sub list_images {
+    my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
+
+    my $imagedir = $class->get_subdir($scfg, 'images');
+
+    my ($defFmt, $vaidFmts) = default_format($scfg);
+    my $fmts = join ('|', @$vaidFmts);
+
+    my $res = [];
+
+    foreach my $fn (<$imagedir/[0-9][0-9]*/*>) {
+
+       next if $fn !~ m!^(/.+/(\d+)/([^/]+\.($fmts)))$!;
+       $fn = $1; # untaint
+
+       my $owner = $2;
+       my $name = $3;
+
+       next if !$vollist && defined($vmid) && ($owner ne $vmid);
+
+       my ($size, $format, $used, $parent, $ctime) = file_size_info($fn);
+       next if !($format && defined($size));
+
+       my $volid;
+       if ($parent && $parent =~ m!^../(\d+)/([^/]+\.($fmts))$!) {
+           my ($basevmid, $basename) = ($1, $2);
+           $volid = "$storeid:$basevmid/$basename/$owner/$name";
+       } else {
+           $volid = "$storeid:$owner/$name";
+       }
+
+       if ($vollist) {
+           my $found = grep { $_ eq $volid } @$vollist;
+           next if !$found;
+       }
+
+        my $info = {
+           volid => $volid, format => $format,
+           size => $size, vmid => $owner, used => $used, parent => $parent
+       };
+
+        $info->{ctime} = $ctime if $ctime;
+
+        push @$res, $info;
+    }
+
+    return $res;
+}
+
+# list templates ($tt = <iso|vztmpl|backup|snippets>)
+my $get_subdir_files = sub {
+    my ($sid, $path, $tt, $vmid) = @_;
+
+    my $res = [];
+
+    foreach my $fn (<$path/*>) {
+       my $st = File::stat::stat($fn);
+
+       next if (!$st || S_ISDIR($st->mode));
+
+       my $info;
+
+       if ($tt eq 'iso') {
+           next if $fn !~ m!/([^/]+$PVE::Storage::ISO_EXT_RE_0)$!i;
+
+           $info = { volid => "$sid:iso/$1", format => 'iso' };
+
+       } elsif ($tt eq 'vztmpl') {
+           next if $fn !~ m!/([^/]+$PVE::Storage::VZTMPL_EXT_RE_1)$!;
+
+           $info = { volid => "$sid:vztmpl/$1", format => "t$2" };
+
+       } elsif ($tt eq 'backup') {
+           next if $fn !~ m!/([^/]+$PVE::Storage::BACKUP_EXT_RE_2)$!;
+           my $original = $fn;
+           my $format = $2;
+           $fn = $1;
+
+           # only match for VMID now, to avoid false positives (VMID in parent directory name)
+           next if defined($vmid) && $fn !~ m/\S+-$vmid-\S+/;
+
+           $info = { volid => "$sid:backup/$fn", format => $format };
+
+           my $archive_info = eval { PVE::Storage::archive_info($fn) } // {};
+
+           $info->{ctime} = $archive_info->{ctime} if defined($archive_info->{ctime});
+           $info->{subtype} = $archive_info->{type} // 'unknown';
+
+           if (defined($vmid) || $fn =~ m!\-([1-9][0-9]{2,8})\-[^/]+\.${format}$!) {
+               $info->{vmid} = $vmid // $1;
+           }
+
+           my $notes_fn = $original.NOTES_EXT;
+           if (-f $notes_fn) {
+               my $notes = PVE::Tools::file_read_firstline($notes_fn);
+               $info->{notes} = eval { decode('UTF-8', $notes, 1) } // $notes if defined($notes);
+           }
+
+           $info->{protected} = 1 if -e PVE::Storage::protection_file_path($original);
+       } elsif ($tt eq 'snippets') {
+
+           $info = {
+               volid => "$sid:snippets/". basename($fn),
+               format => 'snippet',
+           };
+       }
+
+       $info->{size} = $st->size;
+       $info->{ctime} //= $st->ctime;
+
+       push @$res, $info;
+    }
+
+    return $res;
+};
+
+# If attributes are set on a volume, they should be included in the result.
+# See get_volume_attribute for a list of possible attributes.
+sub list_volumes {
+    my ($class, $storeid, $scfg, $vmid, $content_types) = @_;
+
+    my $res = [];
+    my $vmlist = PVE::Cluster::get_vmlist();
+    foreach my $type (@$content_types) {
+       my $data;
+
+       if ($type eq 'images' || $type eq 'rootdir') {
+           $data = $class->list_images($storeid, $scfg, $vmid);
+       } elsif ($scfg->{path}) {
+           my $path = $class->get_subdir($scfg, $type);
+
+           if ($type eq 'iso' && !defined($vmid)) {
+               $data = $get_subdir_files->($storeid, $path, 'iso');
+           } elsif ($type eq 'vztmpl'&& !defined($vmid)) {
+               $data = $get_subdir_files->($storeid, $path, 'vztmpl');
+           } elsif ($type eq 'backup') {
+               $data = $get_subdir_files->($storeid, $path, 'backup', $vmid);
+           } elsif ($type eq 'snippets') {
+               $data = $get_subdir_files->($storeid, $path, 'snippets');
+           }
+       }
+
+       next if !$data;
+
+       foreach my $item (@$data) {
+           if ($type eq 'images' || $type eq 'rootdir') {
+               my $vminfo = $vmlist->{ids}->{$item->{vmid}};
+               my $vmtype;
+               if (defined($vminfo)) {
+                   $vmtype = $vminfo->{type};
+               }
+               if (defined($vmtype) && $vmtype eq 'lxc') {
+                   $item->{content} = 'rootdir';
+               } else {
+                   $item->{content} = 'images';
+               }
+               next if $type ne $item->{content};
+           } else {
+               $item->{content} = $type;
+           }
+
+           push @$res, $item;
+       }
+    }
+
+    return $res;
+}
+
+sub status {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    my $path = $scfg->{path};
+
+    die "storage definition has no path\n" if !$path;
+
+    my $timeout = 2;
+    my $res = PVE::Tools::df($path, $timeout);
+
+    return undef if !$res || !$res->{total};
+
+    return ($res->{total}, $res->{avail}, $res->{used}, 1);
+}
+
+# Returns a hash with the snapshot names as keys and the following data:
+# id        - Unique id to distinguish different snapshots even if the have the same name.
+# timestamp - Creation time of the snapshot (seconds since epoch).
+# Returns an empty hash if the volume does not exist.
+sub volume_snapshot_info {
+    my ($class, $scfg, $storeid, $volname) = @_;
+
+    die "volume_snapshot_info is not implemented for $class";
+}
+
+sub activate_storage {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    my $path = $scfg->{path};
+
+    die "storage definition has no path\n" if !$path;
+
+    # this path test may hang indefinitely on unresponsive mounts
+    my $timeout = 2;
+    if (! PVE::Tools::run_fork_with_timeout($timeout, sub {-d $path})) {
+       die "unable to activate storage '$storeid' - " .
+       "directory '$path' does not exist or is unreachable\n";
+    }
+
+
+    return if defined($scfg->{mkdir}) && !$scfg->{mkdir};
+
+    if (defined($scfg->{content})) {
+       foreach my $vtype (keys %$vtype_subdirs) {
+           # OpenVZMigrate uses backup (dump) dir
+           if (defined($scfg->{content}->{$vtype}) ||
+               ($vtype eq 'backup' && defined($scfg->{content}->{'rootdir'}))) {
+               my $subdir = $class->get_subdir($scfg, $vtype);
+               mkpath $subdir if $subdir ne $path;
+           }
+       }
+    }
+}
+
+sub deactivate_storage {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    # do nothing by default
+}
+
+sub map_volume {
+    my ($class, $storeid, $scfg, $volname, $snapname) = @_;
+
+    my ($path) = $class->path($scfg, $volname, $storeid, $snapname);
+    return $path;
+}
+
+sub unmap_volume {
+    my ($class, $storeid, $scfg, $volname, $snapname) = @_;
+
+    return 1;
+}
+
+sub activate_volume {
+    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
+
+    my $path = $class->filesystem_path($scfg, $volname, $snapname);
+
+    # check is volume exists
+    if ($scfg->{path}) {
+       die "volume '$storeid:$volname' does not exist\n" if ! -e $path;
+    } else {
+       die "volume '$storeid:$volname' does not exist\n" if ! -b $path;
+    }
+}
+
+sub deactivate_volume {
+    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
+
+    # do nothing by default
+}
+
+sub check_connection {
+    my ($class, $storeid, $scfg) = @_;
+    # do nothing by default
+    return 1;
+}
+
+sub prune_backups {
+    my ($class, $scfg, $storeid, $keep, $vmid, $type, $dryrun, $logfunc) = @_;
+
+    $logfunc //= sub { print "$_[1]\n" };
+
+    my $backups = $class->list_volumes($storeid, $scfg, $vmid, ['backup']);
+
+    my $backup_groups = {};
+    my $prune_list = [];
+
+    foreach my $backup (@{$backups}) {
+       my $volid = $backup->{volid};
+       my $archive_info = eval { PVE::Storage::archive_info($volid) } // {};
+       my $backup_type = $archive_info->{type} // 'unknown';
+       my $backup_vmid = $archive_info->{vmid} // $backup->{vmid};
+
+       next if defined($type) && $type ne $backup_type;
+
+       my $prune_entry = {
+           ctime => $backup->{ctime},
+           type => $backup_type,
+           volid => $volid,
+       };
+
+       $prune_entry->{vmid} = $backup_vmid if defined($backup_vmid);
+
+       if ($archive_info->{is_std_name}) {
+           die "internal error - got no VMID\n" if !defined($backup_vmid);
+           die "internal error - got wrong VMID '$backup_vmid' != '$vmid'\n"
+               if defined($vmid) && $backup_vmid ne $vmid;
+
+           $prune_entry->{ctime} = $archive_info->{ctime};
+           my $group = "$backup_type/$backup_vmid";
+           push @{$backup_groups->{$group}}, $prune_entry;
+       } else {
+           # ignore backups that don't use the standard naming scheme
+           $prune_entry->{mark} = 'renamed';
+       }
+
+       $prune_entry->{mark} = 'protected' if $backup->{protected};
+
+       push @{$prune_list}, $prune_entry;
+    }
+
+    foreach my $backup_group (values %{$backup_groups}) {
+       PVE::Storage::prune_mark_backup_group($backup_group, $keep);
+    }
+
+    my $failed;
+    if (!$dryrun) {
+       foreach my $prune_entry (@{$prune_list}) {
+           next if $prune_entry->{mark} ne 'remove';
+
+           my $volid = $prune_entry->{volid};
+           $logfunc->('info', "removing backup '$volid'");
+           eval {
+               my (undef, $volname) = parse_volume_id($volid);
+               my $archive_path = $class->filesystem_path($scfg, $volname);
+               PVE::Storage::archive_remove($archive_path);
+           };
+           if (my $err = $@) {
+               $logfunc->('err', "error when removing backup '$volid' - $err\n");
+               $failed = 1;
+           }
+       }
+    }
+    die "error pruning backups - check log\n" if $failed;
+
+    return $prune_list;
+}
+
+# Import/Export interface:
+#   Any path based storage is assumed to support 'raw' and 'tar' streams, so
+#   the default implementations will return this if $scfg->{path} is set,
+#   mimicking the old PVE::Storage::storage_migrate() function.
+#
+# Plugins may fall back to PVE::Storage::Plugin::volume_{export,import}...
+#   functions in case the format doesn't match their specialized
+#   implementations to reuse the raw/tar code.
+#
+# Format specification:
+#   The following formats are all prefixed with image information in the form
+#   of a 64 bit little endian unsigned integer (pack('Q<')) in order to be able
+#   to preallocate the image on storages which require it.
+#
+#   raw+size: (image files only)
+#     A raw binary data stream such as produced via `dd if=TheImageFile`.
+#   qcow2+size, vmdk: (image files only)
+#     A raw qcow2/vmdk/... file such as produced via `dd if=some.qcow2` for
+#     files which are already in qcow2 format, or via `qemu-img convert`.
+#     Note that these formats are only valid with $with_snapshots being true.
+#   tar+size: (subvolumes only)
+#     A GNU tar stream containing just the inner contents of the subvolume.
+#     This does not distinguish between the contents of a privileged or
+#     unprivileged container. In other words, this is from the root user
+#     namespace's point of view with no uid-mapping in effect.
+#     As produced via `tar -C vm-100-disk-1.subvol -cpf TheOutputFile.dat .`
+
+# Plugins may reuse these helpers. Changes to the header format should be
+# reflected by changes to the function prototypes.
+sub write_common_header($$) {
+    my ($fh, $image_size_in_bytes) = @_;
+    syswrite($fh, pack("Q<", $image_size_in_bytes), 8);
+}
+
+sub read_common_header($) {
+    my ($fh) = @_;
+    sysread($fh, my $size, 8);
+    $size = unpack('Q<', $size);
+    die "import: no size found in export header, aborting.\n" if !defined($size);
+    # Size is in bytes!
+    return $size;
+}
+
+# Export a volume into a file handle as a stream of desired format.
+sub volume_export {
+    my ($class, $scfg, $storeid, $fh, $volname, $format, $snapshot, $base_snapshot, $with_snapshots) = @_;
+    if ($scfg->{path} && !defined($snapshot) && !defined($base_snapshot)) {
+       my $file = $class->path($scfg, $volname, $storeid)
+           or goto unsupported;
+       my ($size, $file_format) = file_size_info($file);
+
+       if ($format eq 'raw+size') {
+           goto unsupported if $with_snapshots || $file_format eq 'subvol';
+           write_common_header($fh, $size);
+           if ($file_format eq 'raw') {
+               run_command(['dd', "if=$file", "bs=4k"], output => '>&'.fileno($fh));
+           } else {
+               run_command(['qemu-img', 'convert', '-f', $file_format, '-O', 'raw', $file, '/dev/stdout'],
+                           output => '>&'.fileno($fh));
+           }
+           return;
+       } elsif ($format =~ /^(qcow2|vmdk)\+size$/) {
+           my $data_format = $1;
+           goto unsupported if !$with_snapshots || $file_format ne $data_format;
+           write_common_header($fh, $size);
+           run_command(['dd', "if=$file", "bs=4k"], output => '>&'.fileno($fh));
+           return;
+       } elsif ($format eq 'tar+size') {
+           goto unsupported if $file_format ne 'subvol';
+           write_common_header($fh, $size);
+           run_command(['tar', @COMMON_TAR_FLAGS, '-cf', '-', '-C', $file, '.'],
+                       output => '>&'.fileno($fh));
+           return;
+       }
+    }
+ unsupported:
+    die "volume export format $format not available for $class";
+}
+
+sub volume_export_formats {
+    my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
+    if ($scfg->{path} && !defined($snapshot) && !defined($base_snapshot)) {
+       my $file = $class->path($scfg, $volname, $storeid)
+           or return;
+       my ($size, $format) = file_size_info($file);
+
+       if ($with_snapshots) {
+           return ($format.'+size') if ($format eq 'qcow2' || $format eq 'vmdk');
+           return ();
+       }
+       return ('tar+size') if $format eq 'subvol';
+       return ('raw+size');
+    }
+    return ();
+}
+
+# Import data from a stream, creating a new or replacing or adding to an existing volume.
+sub volume_import {
+    my ($class, $scfg, $storeid, $fh, $volname, $format, $snapshot, $base_snapshot, $with_snapshots, $allow_rename) = @_;
+
+    die "volume import format '$format' not available for $class\n"
+       if $format !~ /^(raw|tar|qcow2|vmdk)\+size$/;
+    my $data_format = $1;
+
+    die "format $format cannot be imported without snapshots\n"
+       if !$with_snapshots && ($data_format eq 'qcow2' || $data_format eq 'vmdk');
+    die "format $format cannot be imported with snapshots\n"
+       if $with_snapshots && ($data_format eq 'raw' || $data_format eq 'tar');
+
+    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $file_format) =
+       $class->parse_volname($volname);
+
+    # XXX: Should we bother with conversion routines at this level? This won't
+    # happen without manual CLI usage, so for now we just error out...
+    die "cannot import format $format into a file of format $file_format\n"
+       if $data_format ne $file_format && !($data_format eq 'tar' && $file_format eq 'subvol');
+
+    # Check for an existing file first since interrupting alloc_image doesn't
+    # free it.
+    my $file = $class->path($scfg, $volname, $storeid);
+    if (-e $file) {
+       die "file '$file' already exists\n" if !$allow_rename;
+       warn "file '$file' already exists - importing with a different name\n";
+       $name = undef;
+    }
+
+    my ($size) = read_common_header($fh);
+    $size = int($size/1024);
+
+    eval {
+       my $allocname = $class->alloc_image($storeid, $scfg, $vmid, $file_format, $name, $size);
+       my $oldname = $volname;
+       $volname = $allocname;
+       if (defined($name) && $allocname ne $oldname) {
+           die "internal error: unexpected allocated name: '$allocname' != '$oldname'\n";
+       }
+       my $file = $class->path($scfg, $volname, $storeid)
+           or die "internal error: failed to get path to newly allocated volume $volname\n";
+       if ($data_format eq 'raw' || $data_format eq 'qcow2' || $data_format eq 'vmdk') {
+           run_command(['dd', "of=$file", 'conv=sparse', 'bs=64k'],
+                       input => '<&'.fileno($fh));
+       } elsif ($data_format eq 'tar') {
+           run_command(['tar', @COMMON_TAR_FLAGS, '-C', $file, '-xf', '-'],
+                       input => '<&'.fileno($fh));
+       } else {
+           die "volume import format '$format' not available for $class";
+       }
+    };
+    if (my $err = $@) {
+       eval { $class->free_image($storeid, $scfg, $volname, 0, $file_format) };
+       warn $@ if $@;
+       die $err;
+    }
+
+    return "$storeid:$volname";
+}
+
+sub volume_import_formats {
+    my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
+    if ($scfg->{path} && !defined($base_snapshot)) {
+       my $format = ($class->parse_volname($volname))[6];
+       if ($with_snapshots) {
+           return ($format.'+size') if ($format eq 'qcow2' || $format eq 'vmdk');
+           return ();
+       }
+       return ('tar+size') if $format eq 'subvol';
+       return ('raw+size');
+    }
+    return ();
+}
+
+sub rename_volume {
+    my ($class, $scfg, $storeid, $source_volname, $target_vmid, $target_volname) = @_;
+    die "not implemented in storage plugin '$class'\n" if $class->can('api') && $class->api() < 10;
+    die "no path found\n" if !$scfg->{path};
+
+    my (
+       undef,
+       $source_image,
+       $source_vmid,
+       $base_name,
+       $base_vmid,
+       undef,
+       $format
+    ) = $class->parse_volname($source_volname);
+
+    $target_volname = $class->find_free_diskname($storeid, $scfg, $target_vmid, $format, 1)
+       if !$target_volname;
+
+    my $basedir = $class->get_subdir($scfg, 'images');
+
+    mkpath "${basedir}/${target_vmid}";
+
+    my $old_path = "${basedir}/${source_vmid}/${source_image}";
+    my $new_path = "${basedir}/${target_vmid}/${target_volname}";
+
+    die "target volume '${target_volname}' already exists\n" if -e $new_path;
+
+    my $base = $base_name ? "${base_vmid}/${base_name}/" : '';
+
+    rename($old_path, $new_path) ||
+       die "rename '$old_path' to '$new_path' failed - $!\n";
+
+    return "${storeid}:${base}${target_vmid}/${target_volname}";
+}
+
+1;
diff --git a/src/PVE/Storage/RBDPlugin.pm b/src/PVE/Storage/RBDPlugin.pm
new file mode 100644 (file)
index 0000000..73703fb
--- /dev/null
@@ -0,0 +1,891 @@
+package PVE::Storage::RBDPlugin;
+
+use strict;
+use warnings;
+
+use Cwd qw(abs_path);
+use IO::File;
+use JSON;
+use Net::IP;
+
+use PVE::CephConfig;
+use PVE::Cluster qw(cfs_read_file);;
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::ProcFSTools;
+use PVE::RADOS;
+use PVE::RPCEnvironment;
+use PVE::Storage::Plugin;
+use PVE::Tools qw(run_command trim file_read_firstline);
+
+use base qw(PVE::Storage::Plugin);
+
+my $get_parent_image_name = sub {
+    my ($parent) = @_;
+    return undef if !$parent;
+    return $parent->{image} . "@" . $parent->{snapshot};
+};
+
+my $librados_connect = sub {
+    my ($scfg, $storeid, $options) = @_;
+
+    $options->{timeout} = 60
+       if !defined($options->{timeout}) && PVE::RPCEnvironment->is_worker();
+
+    my $librados_config = PVE::CephConfig::ceph_connect_option($scfg, $storeid, $options->%*);
+
+    my $rados = PVE::RADOS->new(%$librados_config);
+
+    return $rados;
+};
+
+my sub get_rbd_path {
+    my ($scfg, $volume) = @_;
+    my $path = $scfg->{pool} ? $scfg->{pool} : 'rbd';
+    $path .= "/$scfg->{namespace}" if defined($scfg->{namespace});
+    $path .= "/$volume" if defined($volume);
+    return $path;
+};
+
+my sub get_rbd_dev_path {
+    my ($scfg, $storeid, $volume) = @_;
+
+    my $cluster_id = '';
+    if ($scfg->{fsid}) {
+       # NOTE: the config doesn't support this currently (but it could!), hack for qemu-server tests
+       $cluster_id = $scfg->{fsid};
+    } elsif ($scfg->{monhost}) {
+       my $rados = $librados_connect->($scfg, $storeid);
+       $cluster_id = $rados->mon_command({ prefix => 'fsid', format => 'json' })->{fsid};
+    } else {
+       $cluster_id = cfs_read_file('ceph.conf')->{global}->{fsid};
+    }
+
+    my $uuid_pattern = "([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})";
+    if ($cluster_id =~ qr/^${uuid_pattern}$/is) {
+       $cluster_id = $1; # use untained value
+    } else {
+       die "cluster fsid has invalid format\n";
+    }
+
+    my $rbd_path = get_rbd_path($scfg, $volume);
+    my $pve_path = "/dev/rbd-pve/${cluster_id}/${rbd_path}";
+    my $path = "/dev/rbd/${rbd_path}";
+
+    if (!-e $pve_path && -e $path) {
+       # possibly mapped before rbd-pve rule existed
+       my $real_dev = abs_path($path);
+       my ($rbd_id) = ($real_dev =~ m|/dev/rbd([0-9]+)$|);
+       my $dev_cluster_id = file_read_firstline("/sys/devices/rbd/${rbd_id}/cluster_fsid");
+       return $path if $cluster_id eq $dev_cluster_id;
+    }
+    return $pve_path;
+}
+
+my $build_cmd = sub {
+    my ($binary, $scfg, $storeid, $op, @options) = @_;
+
+    my $cmd_option = PVE::CephConfig::ceph_connect_option($scfg, $storeid);
+    my $pool =  $scfg->{pool} ? $scfg->{pool} : 'rbd';
+
+    my $cmd = [$binary, '-p', $pool];
+
+    if (defined(my $namespace = $scfg->{namespace})) {
+       # some subcommands will fail if the --namespace parameter is present
+       my $no_namespace_parameter = {
+           unmap => 1,
+       };
+       push @$cmd, '--namespace', "$namespace" if !$no_namespace_parameter->{$op};
+    }
+    push @$cmd, '-c', $cmd_option->{ceph_conf} if ($cmd_option->{ceph_conf});
+    push @$cmd, '-m', $cmd_option->{mon_host} if ($cmd_option->{mon_host});
+    push @$cmd, '--auth_supported', $cmd_option->{auth_supported} if ($cmd_option->{auth_supported});
+    push @$cmd, '-n', "client.$cmd_option->{userid}" if ($cmd_option->{userid});
+    push @$cmd, '--keyring', $cmd_option->{keyring} if ($cmd_option->{keyring});
+
+    push @$cmd, $op;
+
+    push @$cmd, @options if scalar(@options);
+
+    return $cmd;
+};
+
+my $rbd_cmd = sub {
+    my ($scfg, $storeid, $op, @options) = @_;
+
+    return $build_cmd->('/usr/bin/rbd', $scfg, $storeid, $op, @options);
+};
+
+my $rados_cmd = sub {
+    my ($scfg, $storeid, $op, @options) = @_;
+
+    return $build_cmd->('/usr/bin/rados', $scfg, $storeid, $op, @options);
+};
+
+# needed for volumes created using ceph jewel (or higher)
+my $krbd_feature_update = sub {
+    my ($scfg, $storeid, $name) = @_;
+
+    my (@disable, @enable);
+    my ($kmajor, $kminor) = PVE::ProcFSTools::kernel_version();
+
+    if ($kmajor > 5 || $kmajor == 5 && $kminor >= 3) {
+       # 'deep-flatten' can only be disabled, not enabled after image creation
+       push @enable, 'fast-diff', 'object-map';
+    } else {
+       push @disable, 'fast-diff', 'object-map', 'deep-flatten';
+    }
+
+    if ($kmajor >= 5) {
+       push @enable, 'exclusive-lock';
+    } else {
+       push @disable, 'exclusive-lock';
+    }
+
+    my $active_features_list = (rbd_volume_info($scfg, $storeid, $name))[4];
+    my $active_features = { map { $_ => 1 } @$active_features_list };
+
+    my $to_disable = join(',', grep {  $active_features->{$_} } @disable);
+    my $to_enable  = join(',', grep { !$active_features->{$_} } @enable );
+
+    if ($to_disable) {
+       print "disable RBD image features this kernel RBD drivers is not compatible with: $to_disable\n";
+       my $cmd = $rbd_cmd->($scfg, $storeid, 'feature', 'disable', $name, $to_disable);
+       run_rbd_command(
+           $cmd,
+           errmsg => "could not disable krbd-incompatible image features '$to_disable' for rbd image: $name",
+       );
+    }
+    if ($to_enable) {
+       print "enable RBD image features this kernel RBD drivers supports: $to_enable\n";
+       eval {
+           my $cmd = $rbd_cmd->($scfg, $storeid, 'feature', 'enable', $name, $to_enable);
+           run_rbd_command(
+               $cmd,
+               errmsg => "could not enable krbd-compatible image features '$to_enable' for rbd image: $name",
+           );
+       };
+       warn "$@" if $@;
+    }
+};
+
+sub run_rbd_command {
+    my ($cmd, %args) = @_;
+
+    my $lasterr;
+    my $errmsg = $args{errmsg} . ": " || "";
+    if (!exists($args{errfunc})) {
+       # ' error: 2014-02-06 11:51:59.839135 7f09f94d0760 -1 librbd: snap_unprotect: can't unprotect;
+       # at least 1 child(ren) in pool cephstor1
+       $args{errfunc} = sub {
+           my $line = shift;
+           if ($line =~ m/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+ [0-9a-f]+ [\-\d]+ librbd: (.*)$/) {
+               $lasterr = "$1\n";
+           } else {
+               $lasterr = $line;
+           }
+           print STDERR $lasterr;
+           *STDERR->flush();
+       };
+    }
+
+    eval { run_command($cmd, %args); };
+    if (my $err = $@) {
+       die $errmsg . $lasterr if length($lasterr);
+       die $err;
+    }
+
+    return undef;
+}
+
+sub rbd_ls {
+    my ($scfg, $storeid) = @_;
+
+    my $pool =  $scfg->{pool} ? $scfg->{pool} : 'rbd';
+    $pool .= "/$scfg->{namespace}" if defined($scfg->{namespace});
+
+    my $raw = '';
+    my $parser = sub { $raw .= shift };
+
+    my $cmd = $rbd_cmd->($scfg, $storeid, 'ls', '-l', '--format', 'json');
+    eval {
+       run_rbd_command($cmd, errmsg => "rbd error", errfunc => sub {}, outfunc => $parser);
+    };
+    my $err = $@;
+
+    die $err if $err && $err !~ m/doesn't contain rbd images/ ;
+
+    my $result;
+    if ($raw eq '') {
+       $result = [];
+    } elsif ($raw =~ m/^(\[.*\])$/s) { # untaint
+       $result = JSON::decode_json($1);
+    } else {
+       die "got unexpected data from rbd ls: '$raw'\n";
+    }
+
+    my $list = {};
+
+    foreach my $el (@$result) {
+       next if defined($el->{snapshot});
+
+       my $image = $el->{image};
+
+       my ($owner) = $image =~ m/^(?:vm|base)-(\d+)-/;
+       next if !defined($owner);
+
+       $list->{$pool}->{$image} = {
+           name => $image,
+           size => $el->{size},
+           parent => $get_parent_image_name->($el->{parent}),
+           vmid => $owner
+       };
+    }
+
+    return $list;
+}
+
+sub rbd_ls_snap {
+    my ($scfg, $storeid, $name) = @_;
+
+    my $cmd = $rbd_cmd->($scfg, $storeid, 'snap', 'ls', $name, '--format', 'json');
+
+    my $raw = '';
+    run_rbd_command($cmd, errmsg => "rbd error", errfunc => sub {}, outfunc => sub { $raw .= shift; });
+
+    my $list;
+    if ($raw =~ m/^(\[.*\])$/s) { # untaint
+       $list = eval { JSON::decode_json($1) };
+       die "invalid JSON output from 'rbd snap ls $name': $@\n" if $@;
+    } else {
+       die "got unexpected data from 'rbd snap ls $name': '$raw'\n";
+    }
+
+    $list = [] if !defined($list);
+
+    my $res = {};
+    foreach my $el (@$list) {
+       my $snap = $el->{name};
+       my $protected = defined($el->{protected}) && $el->{protected} eq "true" ? 1 : undef;
+       $res->{$snap} = {
+           name => $snap,
+           id => $el->{id} // undef,
+           size => $el->{size} // 0,
+           protected => $protected,
+       };
+    }
+    return $res;
+}
+
+sub rbd_volume_info {
+    my ($scfg, $storeid, $volname, $snap) = @_;
+
+    my $cmd = undef;
+
+    my @options = ('info', $volname, '--format', 'json');
+    if ($snap) {
+       push @options, '--snap', $snap;
+    }
+
+    $cmd = $rbd_cmd->($scfg, $storeid, @options);
+
+    my $raw = '';
+    my $parser = sub { $raw .= shift };
+
+    run_rbd_command($cmd, errmsg => "rbd error", errfunc => sub {}, outfunc => $parser);
+
+    my $volume;
+    if ($raw eq '') {
+       $volume = {};
+    } elsif ($raw =~ m/^(\{.*\})$/s) { # untaint
+       $volume = JSON::decode_json($1);
+    } else {
+       die "got unexpected data from rbd info: '$raw'\n";
+    }
+
+    $volume->{parent} = $get_parent_image_name->($volume->{parent});
+    $volume->{protected} = defined($volume->{protected}) && $volume->{protected} eq "true" ? 1 : undef;
+
+    return $volume->@{qw(size parent format protected features)};
+}
+
+sub rbd_volume_du {
+    my ($scfg, $storeid, $volname) = @_;
+
+    my @options = ('du', $volname, '--format', 'json');
+    my $cmd = $rbd_cmd->($scfg, $storeid, @options);
+
+    my $raw = '';
+    my $parser = sub { $raw .= shift };
+
+    run_rbd_command($cmd, errmsg => "rbd error", errfunc => sub {}, outfunc => $parser);
+
+    my $volume;
+    if ($raw eq '') {
+       $volume = {};
+    } elsif ($raw =~ m/^(\{.*\})$/s) { # untaint
+       $volume = JSON::decode_json($1);
+    } else {
+       die "got unexpected data from rbd du: '$raw'\n";
+    }
+
+    if (!defined($volume->{images})) {
+       die "got no images from rbd du\n";
+    }
+
+    # `rbd du` returns array of images for name matching `volname`,
+    # including snapshots.
+    my $images = $volume->{images};
+    foreach my $image (@$images) {
+       next if defined($image->{snapshot});
+       next if !defined($image->{used_size}) || !defined($image->{name});
+
+       # Return `used_size` of first volume with matching name which
+       # is not a snapshot.
+       return $image->{used_size} if $image->{name} eq $volname;
+    }
+
+    die "got no matching image from rbd du\n";
+}
+
+# Configuration
+
+sub type {
+    return 'rbd';
+}
+
+sub plugindata {
+    return {
+       content => [ {images => 1, rootdir => 1}, { images => 1 }],
+    };
+}
+
+sub properties {
+    return {
+       monhost => {
+           description => "IP addresses of monitors (for external clusters).",
+           type => 'string', format => 'pve-storage-portal-dns-list',
+       },
+       pool => {
+           description => "Pool.",
+           type => 'string',
+       },
+       'data-pool' => {
+           description => "Data Pool (for erasure coding only)",
+           type => 'string',
+       },
+       namespace => {
+           description => "Namespace.",
+           type => 'string',
+       },
+       username => {
+           description => "RBD Id.",
+           type => 'string',
+       },
+       authsupported => {
+           description => "Authsupported.",
+           type => 'string',
+       },
+       krbd => {
+           description => "Always access rbd through krbd kernel module.",
+           type => 'boolean',
+       },
+       keyring => {
+           description => "Client keyring contents (for external clusters).",
+           type => 'string',
+       },
+    };
+}
+
+sub options {
+    return {
+       nodes => { optional => 1 },
+       disable => { optional => 1 },
+       monhost => { optional => 1},
+       pool => { optional => 1 },
+       'data-pool' => { optional => 1 },
+       namespace => { optional => 1 },
+       username => { optional => 1 },
+       content => { optional => 1 },
+       krbd => { optional => 1 },
+       keyring => { optional => 1 },
+       bwlimit => { optional => 1 },
+    };
+}
+
+# Storage implementation
+
+sub on_add_hook {
+    my ($class, $storeid, $scfg, %param) = @_;
+
+    PVE::CephConfig::ceph_create_keyfile($scfg->{type}, $storeid, $param{keyring});
+
+    return;
+}
+
+sub on_update_hook {
+    my ($class, $storeid, $scfg, %param) = @_;
+
+    if (exists($param{keyring})) {
+       if (defined($param{keyring})) {
+           PVE::CephConfig::ceph_create_keyfile($scfg->{type}, $storeid, $param{keyring});
+       } else {
+           PVE::CephConfig::ceph_remove_keyfile($scfg->{type}, $storeid);
+       }
+    }
+
+    return;
+}
+
+sub on_delete_hook {
+    my ($class, $storeid, $scfg) = @_;
+    PVE::CephConfig::ceph_remove_keyfile($scfg->{type}, $storeid);
+    return;
+}
+
+sub parse_volname {
+    my ($class, $volname) = @_;
+
+    if ($volname =~ m/^((base-(\d+)-\S+)\/)?((base)?(vm)?-(\d+)-\S+)$/) {
+       return ('images', $4, $7, $2, $3, $5, 'raw');
+    }
+
+    die "unable to parse rbd volume name '$volname'\n";
+}
+
+sub path {
+    my ($class, $scfg, $volname, $storeid, $snapname) = @_;
+
+    my $cmd_option = PVE::CephConfig::ceph_connect_option($scfg, $storeid);
+    my ($vtype, $name, $vmid) = $class->parse_volname($volname);
+    $name .= '@'.$snapname if $snapname;
+
+    if ($scfg->{krbd}) {
+       my $rbd_dev_path = get_rbd_dev_path($scfg, $storeid, $name);
+       return ($rbd_dev_path, $vmid, $vtype);
+    }
+
+    my $rbd_path = get_rbd_path($scfg, $name);
+    my $path = "rbd:${rbd_path}";
+
+    $path .= ":conf=$cmd_option->{ceph_conf}" if $cmd_option->{ceph_conf};
+    if (defined($scfg->{monhost})) {
+       my $monhost = PVE::CephConfig::hostlist($scfg->{monhost}, ';');
+       $monhost =~ s/:/\\:/g;
+       $path .= ":mon_host=$monhost";
+       $path .= ":auth_supported=$cmd_option->{auth_supported}";
+    }
+
+    $path .= ":id=$cmd_option->{userid}:keyring=$cmd_option->{keyring}" if ($cmd_option->{keyring});
+
+    return ($path, $vmid, $vtype);
+}
+
+sub find_free_diskname {
+    my ($class, $storeid, $scfg, $vmid, $fmt, $add_fmt_suffix) = @_;
+
+    my $cmd = $rbd_cmd->($scfg, $storeid, 'ls');
+
+    my $disk_list = [];
+
+    my $parser = sub {
+       my $line = shift;
+       if ($line =~ m/^(.*)$/) { # untaint
+           push @$disk_list, $1;
+       }
+    };
+
+    eval {
+       run_rbd_command($cmd, errmsg => "rbd error", errfunc => sub {}, outfunc => $parser);
+    };
+    my $err = $@;
+
+    die $err if $err && $err !~ m/doesn't contain rbd images/;
+
+    return PVE::Storage::Plugin::get_next_vm_diskname($disk_list, $storeid, $vmid, undef, $scfg);
+}
+
+sub create_base {
+    my ($class, $storeid, $scfg, $volname) = @_;
+
+    my $snap = '__base__';
+
+    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
+        $class->parse_volname($volname);
+
+    die "create_base not possible with base image\n" if $isBase;
+
+    my ($size, $parent, $format, undef) = rbd_volume_info($scfg, $storeid, $name);
+    die "rbd volume info on '$name' failed\n" if !($size);
+
+    die "rbd image must be at format V2" if $format ne "2";
+
+    die "volname '$volname' contains wrong information about parent $parent $basename\n"
+        if $basename && (!$parent || $parent ne $basename."@".$snap);
+
+    my $newname = $name;
+    $newname =~ s/^vm-/base-/;
+
+    my $newvolname = $basename ? "$basename/$newname" : "$newname";
+
+    my $cmd = $rbd_cmd->(
+       $scfg,
+       $storeid,
+       'rename',
+       get_rbd_path($scfg, $name),
+       get_rbd_path($scfg, $newname),
+    );
+    run_rbd_command($cmd, errmsg => "rbd rename '$name' error");
+
+    eval { $class->unmap_volume($storeid, $scfg, $volname); };
+    warn $@ if $@;
+
+    my $running  = undef; #fixme : is create_base always offline ?
+
+    $class->volume_snapshot($scfg, $storeid, $newname, $snap, $running);
+
+    my (undef, undef, undef, $protected) = rbd_volume_info($scfg, $storeid, $newname, $snap);
+
+    if (!$protected){
+       my $cmd = $rbd_cmd->($scfg, $storeid, 'snap', 'protect', $newname, '--snap', $snap);
+       run_rbd_command($cmd, errmsg => "rbd protect $newname snap '$snap' error");
+    }
+
+    return $newvolname;
+
+}
+
+sub clone_image {
+    my ($class, $scfg, $storeid, $volname, $vmid, $snapname) = @_;
+
+    my $snap = '__base__';
+    $snap = $snapname if length $snapname;
+
+    my ($vtype, $basename, $basevmid, undef, undef, $isBase) =
+        $class->parse_volname($volname);
+
+    die "$volname is not a base image and snapname is not provided\n" 
+       if !$isBase && !length($snapname);
+
+    my $name = $class->find_free_diskname($storeid, $scfg, $vmid);
+
+    warn "clone $volname: $basename snapname $snap to $name\n";
+
+    if (length($snapname)) {
+       my (undef, undef, undef, $protected) = rbd_volume_info($scfg, $storeid, $volname, $snapname);
+
+       if (!$protected) {
+           my $cmd = $rbd_cmd->($scfg, $storeid, 'snap', 'protect', $volname, '--snap', $snapname);
+           run_rbd_command($cmd, errmsg => "rbd protect $volname snap $snapname error");
+       }
+    }
+
+    my $newvol = "$basename/$name";
+    $newvol = $name if length($snapname);
+
+    my @options = (
+       get_rbd_path($scfg, $basename),
+       '--snap', $snap,
+    );
+    push @options, ('--data-pool', $scfg->{'data-pool'}) if $scfg->{'data-pool'};
+
+    my $cmd = $rbd_cmd->($scfg, $storeid, 'clone', @options, get_rbd_path($scfg, $name));
+    run_rbd_command($cmd, errmsg => "rbd clone '$basename' error");
+
+    return $newvol;
+}
+
+sub alloc_image {
+    my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
+
+
+    die "illegal name '$name' - should be 'vm-$vmid-*'\n"
+       if  $name && $name !~ m/^vm-$vmid-/;
+
+    $name = $class->find_free_diskname($storeid, $scfg, $vmid) if !$name;
+
+    my @options = (
+       '--image-format' , 2,
+       '--size', int(($size + 1023) / 1024),
+    );
+    push @options, ('--data-pool', $scfg->{'data-pool'}) if $scfg->{'data-pool'};
+
+    my $cmd = $rbd_cmd->($scfg, $storeid, 'create', @options, $name);
+    run_rbd_command($cmd, errmsg => "rbd create '$name' error");
+
+    return $name;
+}
+
+sub free_image {
+    my ($class, $storeid, $scfg, $volname, $isBase) = @_;
+
+    my ($vtype, $name, $vmid, undef, undef, undef) =
+       $class->parse_volname($volname);
+
+
+    my $snaps = rbd_ls_snap($scfg, $storeid, $name);
+    foreach my $snap (keys %$snaps) {
+       if ($snaps->{$snap}->{protected}) {
+           my $cmd = $rbd_cmd->($scfg, $storeid, 'snap', 'unprotect', $name, '--snap', $snap);
+           run_rbd_command($cmd, errmsg => "rbd unprotect $name snap '$snap' error");
+       }
+    }
+
+    $class->deactivate_volume($storeid, $scfg, $volname);
+
+    my $cmd = $rbd_cmd->($scfg, $storeid, 'snap', 'purge',  $name);
+    run_rbd_command($cmd, errmsg => "rbd snap purge '$name' error");
+
+    $cmd = $rbd_cmd->($scfg, $storeid, 'rm', $name);
+    run_rbd_command($cmd, errmsg => "rbd rm '$name' error");
+
+    return undef;
+}
+
+sub list_images {
+    my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
+
+    $cache->{rbd} = rbd_ls($scfg, $storeid) if !$cache->{rbd};
+
+    my $dat = $cache->{rbd}->{get_rbd_path($scfg)};
+    return [] if !$dat; # nothing found
+
+    my $res = [];
+    for my $image (sort keys %$dat) {
+       my $info = $dat->{$image};
+       my ($volname, $parent, $owner) = $info->@{'name', 'parent', 'vmid'};
+
+       if ($parent && $parent =~ m/^(base-\d+-\S+)\@__base__$/) {
+           $info->{volid} = "$storeid:$1/$volname";
+       } else {
+           $info->{volid} = "$storeid:$volname";
+       }
+
+       if ($vollist) {
+           my $found = grep { $_ eq $info->{volid} } @$vollist;
+           next if !$found;
+       } else {
+           next if defined ($vmid) && ($owner ne $vmid);
+       }
+
+       $info->{format} = 'raw';
+
+       push @$res, $info;
+    }
+
+    return $res;
+}
+
+sub status {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    my $rados = $librados_connect->($scfg, $storeid);
+    my $df = $rados->mon_command({ prefix => 'df', format => 'json' });
+
+    my $pool = $scfg->{'data-pool'} // $scfg->{pool} // 'rbd';
+
+    my ($d) = grep { $_->{name} eq $pool } @{$df->{pools}};
+
+    if (!defined($d)) {
+       warn "could not get usage stats for pool '$pool'\n";
+       return;
+    }
+
+    # max_avail -> max available space for data w/o replication in the pool
+    # bytes_used -> data w/o replication in the pool
+    my $free = $d->{stats}->{max_avail};
+    my $used = $d->{stats}->{stored} // $d->{stats}->{bytes_used};
+    my $total = $used + $free;
+    my $active = 1;
+
+    return ($total, $free, $used, $active);
+}
+
+sub activate_storage {
+    my ($class, $storeid, $scfg, $cache) = @_;
+    return 1;
+}
+
+sub deactivate_storage {
+    my ($class, $storeid, $scfg, $cache) = @_;
+    return 1;
+}
+
+sub map_volume {
+    my ($class, $storeid, $scfg, $volname, $snapname) = @_;
+
+    my ($vtype, $img_name, $vmid) = $class->parse_volname($volname);
+
+    my $name = $img_name;
+    $name .= '@'.$snapname if $snapname;
+
+    my $kerneldev = get_rbd_dev_path($scfg, $storeid, $name);
+
+    return $kerneldev if -b $kerneldev; # already mapped
+
+    # features can only be enabled/disabled for image, not for snapshot!
+    $krbd_feature_update->($scfg, $storeid, $img_name);
+
+    my $cmd = $rbd_cmd->($scfg, $storeid, 'map', $name);
+    run_rbd_command($cmd, errmsg => "can't map rbd volume $name");
+
+    return $kerneldev;
+}
+
+sub unmap_volume {
+    my ($class, $storeid, $scfg, $volname, $snapname) = @_;
+
+    my ($vtype, $name, $vmid) = $class->parse_volname($volname);
+    $name .= '@'.$snapname if $snapname;
+
+    my $kerneldev = get_rbd_dev_path($scfg, $storeid, $name);
+
+    if (-b $kerneldev) {
+       my $cmd = $rbd_cmd->($scfg, $storeid, 'unmap', $kerneldev);
+       run_rbd_command($cmd, errmsg => "can't unmap rbd device $kerneldev");
+    }
+
+    return 1;
+}
+
+sub activate_volume {
+    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
+
+    $class->map_volume($storeid, $scfg, $volname, $snapname) if $scfg->{krbd};
+
+    return 1;
+}
+
+sub deactivate_volume {
+    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
+
+    $class->unmap_volume($storeid, $scfg, $volname, $snapname);
+
+    return 1;
+}
+
+sub volume_size_info {
+    my ($class, $scfg, $storeid, $volname, $timeout) = @_;
+
+    my ($vtype, $name, $vmid) = $class->parse_volname($volname);
+    my ($size, $parent) = rbd_volume_info($scfg, $storeid, $name);
+    my $used = wantarray ? rbd_volume_du($scfg, $storeid, $name) : 0;
+    return wantarray ? ($size, 'raw', $used, $parent) : $size;
+}
+
+sub volume_resize {
+    my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
+
+    return 1 if $running && !$scfg->{krbd}; # FIXME???
+
+    my ($vtype, $name, $vmid) = $class->parse_volname($volname);
+
+    my $cmd = $rbd_cmd->($scfg, $storeid, 'resize', '--allow-shrink', '--size', ($size/1024/1024), $name);
+    run_rbd_command($cmd, errmsg => "rbd resize '$volname' error");
+    return undef;
+}
+
+sub volume_snapshot {
+    my ($class, $scfg, $storeid, $volname, $snap) = @_;
+
+    my ($vtype, $name, $vmid) = $class->parse_volname($volname);
+
+    my $cmd = $rbd_cmd->($scfg, $storeid, 'snap', 'create', '--snap', $snap, $name);
+    run_rbd_command($cmd, errmsg => "rbd snapshot '$volname' error");
+    return undef;
+}
+
+sub volume_snapshot_rollback {
+    my ($class, $scfg, $storeid, $volname, $snap) = @_;
+
+    my ($vtype, $name, $vmid) = $class->parse_volname($volname);
+
+    my $cmd = $rbd_cmd->($scfg, $storeid, 'snap', 'rollback', '--snap', $snap, $name);
+    run_rbd_command($cmd, errmsg => "rbd snapshot $volname to '$snap' error");
+}
+
+sub volume_snapshot_delete {
+    my ($class, $scfg, $storeid, $volname, $snap, $running) = @_;
+
+    $class->deactivate_volume($storeid, $scfg, $volname, $snap, {});
+
+    my ($vtype, $name, $vmid) = $class->parse_volname($volname);
+
+    my (undef, undef, undef, $protected) = rbd_volume_info($scfg, $storeid, $name, $snap);
+    if ($protected){
+       my $cmd = $rbd_cmd->($scfg, $storeid, 'snap', 'unprotect', $name, '--snap', $snap);
+       run_rbd_command($cmd, errmsg => "rbd unprotect $name snap '$snap' error");
+    }
+
+    my $cmd = $rbd_cmd->($scfg, $storeid, 'snap', 'rm', '--snap', $snap, $name);
+
+    run_rbd_command($cmd, errmsg => "rbd snapshot '$volname' error");
+
+    return undef;
+}
+
+sub volume_snapshot_needs_fsfreeze {
+    return 1;
+}
+
+sub volume_has_feature {
+    my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
+
+   my $features = {
+       snapshot => { current => 1, snap => 1},
+       clone => { base => 1, snap => 1},
+       template => { current => 1},
+       copy => { base => 1, current => 1, snap => 1},
+       sparseinit => { base => 1, current => 1},
+       rename => {current => 1},
+    };
+
+    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) = $class->parse_volname($volname);
+
+    my $key = undef;
+    if ($snapname){
+       $key = 'snap';
+    } else {
+       $key = $isBase ? 'base' : 'current';
+    }
+    return 1 if $features->{$feature}->{$key};
+
+    return undef;
+}
+
+sub rename_volume {
+    my ($class, $scfg, $storeid, $source_volname, $target_vmid, $target_volname) = @_;
+
+    my (
+       undef,
+       $source_image,
+       $source_vmid,
+       $base_name,
+       $base_vmid,
+       undef,
+       $format
+    ) = $class->parse_volname($source_volname);
+    $target_volname = $class->find_free_diskname($storeid, $scfg, $target_vmid, $format)
+       if !$target_volname;
+
+    eval {
+       my $cmd = $rbd_cmd->($scfg, $storeid, 'info', $target_volname);
+       run_rbd_command($cmd, errmsg => "exist check",  quiet => 1);
+    };
+    die "target volume '${target_volname}' already exists\n" if !$@;
+
+    my $cmd = $rbd_cmd->($scfg, $storeid, 'rename', $source_image, $target_volname);
+
+    run_rbd_command(
+       $cmd,
+       errmsg => "could not rename image '${source_image}' to '${target_volname}'",
+    );
+
+    eval { $class->unmap_volume($storeid, $scfg, $source_volname); };
+    warn $@ if $@;
+
+    $base_name = $base_name ? "${base_name}/" : '';
+
+    return "${storeid}:${base_name}${target_volname}";
+}
+
+1;
diff --git a/src/PVE/Storage/ZFSPlugin.pm b/src/PVE/Storage/ZFSPlugin.pm
new file mode 100644 (file)
index 0000000..d4dc2a4
--- /dev/null
@@ -0,0 +1,422 @@
+package PVE::Storage::ZFSPlugin;
+
+use strict;
+use warnings;
+use IO::File;
+use POSIX;
+use PVE::Tools qw(run_command);
+use PVE::Storage::ZFSPoolPlugin;
+use PVE::RPCEnvironment;
+
+use base qw(PVE::Storage::ZFSPoolPlugin);
+use PVE::Storage::LunCmd::Comstar;
+use PVE::Storage::LunCmd::Istgt;
+use PVE::Storage::LunCmd::Iet;
+use PVE::Storage::LunCmd::LIO;
+
+
+my @ssh_opts = ('-o', 'BatchMode=yes');
+my @ssh_cmd = ('/usr/bin/ssh', @ssh_opts);
+my $id_rsa_path = '/etc/pve/priv/zfs';
+
+my $lun_cmds = {
+    create_lu   => 1,
+    delete_lu   => 1,
+    import_lu   => 1,
+    modify_lu   => 1,
+    add_view    => 1,
+    list_view   => 1,
+    list_lu     => 1,
+};
+
+my $zfs_unknown_scsi_provider = sub {
+    my ($provider) = @_;
+
+    die "$provider: unknown iscsi provider. Available [comstar, istgt, iet, LIO]";
+};
+
+my $zfs_get_base = sub {
+    my ($scfg) = @_;
+
+    if ($scfg->{iscsiprovider} eq 'comstar') {
+        return PVE::Storage::LunCmd::Comstar::get_base;
+    } elsif ($scfg->{iscsiprovider} eq 'istgt') {
+        return PVE::Storage::LunCmd::Istgt::get_base;
+    } elsif ($scfg->{iscsiprovider} eq 'iet') {
+        return PVE::Storage::LunCmd::Iet::get_base;
+    } elsif ($scfg->{iscsiprovider} eq 'LIO') {
+        return PVE::Storage::LunCmd::LIO::get_base;
+    } else {
+        $zfs_unknown_scsi_provider->($scfg->{iscsiprovider});
+    }
+};
+
+sub zfs_request {
+    my ($class, $scfg, $timeout, $method, @params) = @_;
+
+    $timeout = PVE::RPCEnvironment->is_worker() ? 60*60 : 10
+       if !$timeout;
+
+    my $msg = '';
+
+    if ($lun_cmds->{$method}) {
+        if ($scfg->{iscsiprovider} eq 'comstar') {
+            $msg = PVE::Storage::LunCmd::Comstar::run_lun_command($scfg, $timeout, $method, @params);
+        } elsif ($scfg->{iscsiprovider} eq 'istgt') {
+            $msg = PVE::Storage::LunCmd::Istgt::run_lun_command($scfg, $timeout, $method, @params);
+        } elsif ($scfg->{iscsiprovider} eq 'iet') {
+            $msg = PVE::Storage::LunCmd::Iet::run_lun_command($scfg, $timeout, $method, @params);
+        } elsif ($scfg->{iscsiprovider} eq 'LIO') {
+            $msg = PVE::Storage::LunCmd::LIO::run_lun_command($scfg, $timeout, $method, @params);
+        } else {
+            $zfs_unknown_scsi_provider->($scfg->{iscsiprovider});
+        }
+    } else {
+
+       my $target = 'root@' . $scfg->{portal};
+
+       my $cmd = [@ssh_cmd, '-i', "$id_rsa_path/$scfg->{portal}_id_rsa", $target];
+
+        if ($method eq 'zpool_list') {
+           push @$cmd, 'zpool', 'list';
+       } else {
+           push @$cmd, 'zfs', $method;
+        }
+
+       push @$cmd, @params;
+
+       my $output = sub {
+           my $line = shift;
+           $msg .= "$line\n";
+        };
+
+        run_command($cmd, outfunc => $output, timeout => $timeout);
+    }
+
+    return $msg;
+}
+
+sub zfs_get_lu_name {
+    my ($class, $scfg, $zvol) = @_;
+
+    my $base = $zfs_get_base->($scfg);
+
+    $zvol = ($class->parse_volname($zvol))[1];
+
+    my $object = ($zvol =~ /^.+\/.+/) ? "$base/$zvol" : "$base/$scfg->{pool}/$zvol";
+
+    my $lu_name = $class->zfs_request($scfg, undef, 'list_lu', $object);
+
+    return $lu_name if $lu_name;
+
+    die "Could not find lu_name for zvol $zvol";
+}
+
+sub zfs_add_lun_mapping_entry {
+    my ($class, $scfg, $zvol, $guid) = @_;
+
+    if (!defined($guid)) {
+       $guid = $class->zfs_get_lu_name($scfg, $zvol);
+    }
+
+    $class->zfs_request($scfg, undef, 'add_view', $guid);
+}
+
+sub zfs_delete_lu {
+    my ($class, $scfg, $zvol) = @_;
+
+    my $guid = $class->zfs_get_lu_name($scfg, $zvol);
+
+    $class->zfs_request($scfg, undef, 'delete_lu', $guid);
+}
+
+sub zfs_create_lu {
+    my ($class, $scfg, $zvol) = @_;
+
+    my $base = $zfs_get_base->($scfg);
+    my $guid = $class->zfs_request($scfg, undef, 'create_lu', "$base/$scfg->{pool}/$zvol");
+
+    return $guid;
+}
+
+sub zfs_import_lu {
+    my ($class, $scfg, $zvol) = @_;
+
+    my $base = $zfs_get_base->($scfg);
+    $class->zfs_request($scfg, undef, 'import_lu', "$base/$scfg->{pool}/$zvol");
+}
+
+sub zfs_resize_lu {
+    my ($class, $scfg, $zvol, $size) = @_;
+
+    my $guid = $class->zfs_get_lu_name($scfg, $zvol);
+
+    $class->zfs_request($scfg, undef, 'modify_lu', "${size}K", $guid);
+}
+
+sub zfs_get_lun_number {
+    my ($class, $scfg, $guid) = @_;
+
+    die "could not find lun_number for guid $guid" if !$guid;
+
+    if ($class->zfs_request($scfg, undef, 'list_view', $guid) =~ /^(\d+)$/) {
+       return $1;
+    }
+
+    die "lun_number for guid $guid is not a number";
+}
+
+# Configuration
+
+sub type {
+    return 'zfs';
+}
+
+sub plugindata {
+    return {
+       content => [ {images => 1}, { images => 1 }],
+    };
+}
+
+sub properties {
+    return {
+       iscsiprovider => {
+           description => "iscsi provider",
+           type => 'string',
+       },
+       # this will disable write caching on comstar and istgt.
+       # it is not implemented for iet. iet blockio always operates with
+       # writethrough caching when not in readonly mode
+       nowritecache => {
+           description => "disable write caching on the target",
+           type => 'boolean',
+       },
+       comstar_tg => {
+           description => "target group for comstar views",
+           type => 'string',
+       },
+       comstar_hg => {
+           description => "host group for comstar views",
+           type => 'string',
+       },
+       lio_tpg => {
+           description => "target portal group for Linux LIO targets",
+           type => 'string',
+       },
+    };
+}
+
+sub options {
+    return {
+       nodes => { optional => 1 },
+       disable => { optional => 1 },
+       portal => { fixed => 1 },
+       target => { fixed => 1 },
+       pool => { fixed => 1 },
+       blocksize => { fixed => 1 },
+       iscsiprovider => { fixed => 1 },
+       nowritecache => { optional => 1 },
+       sparse => { optional => 1 },
+       comstar_hg => { optional => 1 },
+       comstar_tg => { optional => 1 },
+       lio_tpg => { optional => 1 },
+       content => { optional => 1 },
+       bwlimit => { optional => 1 },
+    };
+}
+
+# Storage implementation
+
+sub path {
+    my ($class, $scfg, $volname, $storeid, $snapname) = @_;
+
+    die "direct access to snapshots not implemented"
+       if defined($snapname);
+
+    my ($vtype, $name, $vmid) = $class->parse_volname($volname);
+
+    my $target = $scfg->{target};
+    my $portal = $scfg->{portal};
+
+    my $guid = $class->zfs_get_lu_name($scfg, $name);
+    my $lun = $class->zfs_get_lun_number($scfg, $guid);
+
+    my $path = "iscsi://$portal/$target/$lun";
+
+    return ($path, $vmid, $vtype);
+}
+
+sub create_base {
+    my ($class, $storeid, $scfg, $volname) = @_;
+
+    my $snap = '__base__';
+
+    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
+        $class->parse_volname($volname);
+
+    die "create_base not possible with base image\n" if $isBase;
+
+    my $newname = $name;
+    $newname =~ s/^vm-/base-/;
+
+    my $newvolname = $basename ? "$basename/$newname" : "$newname";
+
+    $class->zfs_delete_lu($scfg, $name);
+    $class->zfs_request($scfg, undef, 'rename', "$scfg->{pool}/$name", "$scfg->{pool}/$newname");
+
+    my $guid = $class->zfs_create_lu($scfg, $newname);
+    $class->zfs_add_lun_mapping_entry($scfg, $newname, $guid);
+
+    my $running  = undef; #fixme : is create_base always offline ?
+
+    $class->volume_snapshot($scfg, $storeid, $newname, $snap, $running);
+
+    return $newvolname;
+}
+
+sub clone_image {
+    my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
+
+    my $name = $class->SUPER::clone_image($scfg, $storeid, $volname, $vmid, $snap);
+
+    # get ZFS dataset name from PVE volname
+    my (undef, $clonedname) = $class->parse_volname($name);
+
+    my $guid = $class->zfs_create_lu($scfg, $clonedname);
+    $class->zfs_add_lun_mapping_entry($scfg, $clonedname, $guid);
+
+    return $name;
+}
+
+sub alloc_image {
+    my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
+    
+    die "unsupported format '$fmt'" if $fmt ne 'raw';
+
+    die "illegal name '$name' - should be 'vm-$vmid-*'\n"
+    if $name && $name !~ m/^vm-$vmid-/;
+
+    my $volname = $name;
+
+    $volname = $class->find_free_diskname($storeid, $scfg, $vmid, $fmt) if !$volname;
+    
+    $class->zfs_create_zvol($scfg, $volname, $size);
+    my $guid = $class->zfs_create_lu($scfg, $volname);
+    $class->zfs_add_lun_mapping_entry($scfg, $volname, $guid);
+
+    return $volname;
+}
+
+sub free_image {
+    my ($class, $storeid, $scfg, $volname, $isBase) = @_;
+
+    my ($vtype, $name, $vmid) = $class->parse_volname($volname);
+
+    $class->zfs_delete_lu($scfg, $name);
+
+    eval { $class->zfs_delete_zvol($scfg, $name); };
+    if (my $err = $@) {
+        my $guid = $class->zfs_create_lu($scfg, $name);
+        $class->zfs_add_lun_mapping_entry($scfg, $name, $guid);
+        die $err;
+    }
+
+    return undef;
+}
+
+sub volume_resize {
+    my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
+
+    $volname = ($class->parse_volname($volname))[1];
+
+    my $new_size = $class->SUPER::volume_resize($scfg, $storeid, $volname, $size, $running);
+
+    $class->zfs_resize_lu($scfg, $volname, $new_size);
+
+    return $new_size;
+}
+
+sub volume_snapshot_delete {
+    my ($class, $scfg, $storeid, $volname, $snap, $running) = @_;
+
+    $volname = ($class->parse_volname($volname))[1];
+
+    $class->zfs_request($scfg, undef, 'destroy', "$scfg->{pool}/$volname\@$snap");
+}
+
+sub volume_snapshot_rollback {
+    my ($class, $scfg, $storeid, $volname, $snap) = @_;
+
+    $volname = ($class->parse_volname($volname))[1];
+
+    $class->zfs_delete_lu($scfg, $volname);
+
+    $class->zfs_request($scfg, undef, 'rollback', "$scfg->{pool}/$volname\@$snap");
+
+    $class->zfs_import_lu($scfg, $volname);
+
+    $class->zfs_add_lun_mapping_entry($scfg, $volname);
+}
+
+sub storage_can_replicate {
+    my ($class, $scfg, $storeid, $format) = @_;
+
+    return 0;
+}
+
+sub volume_has_feature {
+    my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
+
+    my $features = {
+       snapshot => { current => 1, snap => 1},
+       clone => { base => 1},
+       template => { current => 1},
+       copy => { base => 1, current => 1},
+    };
+
+    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
+       $class->parse_volname($volname);
+
+    my $key = undef;
+
+    if ($snapname) {
+       $key = 'snap';
+    } else {
+       $key = $isBase ? 'base' : 'current';
+    }
+
+    return 1 if $features->{$feature}->{$key};
+
+    return undef;
+}
+
+sub activate_storage {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    return 1;
+}
+
+sub deactivate_storage {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    return 1;
+}
+
+sub activate_volume {
+    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
+
+    die "unable to activate snapshot from remote zfs storage" if $snapname;
+
+    return 1;
+}
+
+sub deactivate_volume {
+    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
+
+    die "unable to deactivate snapshot from remote zfs storage" if $snapname;
+
+    return 1;
+}
+
+1;
diff --git a/src/PVE/Storage/ZFSPoolPlugin.pm b/src/PVE/Storage/ZFSPoolPlugin.pm
new file mode 100644 (file)
index 0000000..951d027
--- /dev/null
@@ -0,0 +1,852 @@
+package PVE::Storage::ZFSPoolPlugin;
+
+use strict;
+use warnings;
+
+use IO::File;
+use Net::IP;
+use POSIX;
+
+use PVE::ProcFSTools;
+use PVE::RPCEnvironment;
+use PVE::Storage::Plugin;
+use PVE::Tools qw(run_command);
+
+use base qw(PVE::Storage::Plugin);
+
+sub type {
+    return 'zfspool';
+}
+
+sub plugindata {
+    return {
+       content => [ {images => 1, rootdir => 1}, {images => 1 , rootdir => 1}],
+       format => [ { raw => 1, subvol => 1 } , 'raw' ],
+    };
+}
+
+sub properties {
+    return {
+       blocksize => {
+           description => "block size",
+           type => 'string',
+       },
+       sparse => {
+           description => "use sparse volumes",
+           type => 'boolean',
+       },
+       mountpoint => {
+           description => "mount point",
+           type => 'string', format => 'pve-storage-path',
+       },
+    };
+}
+
+sub options {
+    return {
+       pool => { fixed => 1 },
+       blocksize => { optional => 1 },
+       sparse => { optional => 1 },
+       nodes => { optional => 1 },
+       disable => { optional => 1 },
+       content => { optional => 1 },
+       bwlimit => { optional => 1 },
+       mountpoint => { optional => 1 },
+    };
+}
+
+# static zfs helper methods
+
+sub zfs_parse_zvol_list {
+    my ($text, $pool) = @_;
+
+    my $list = ();
+
+    return $list if !$text;
+
+    my @lines = split /\n/, $text;
+    foreach my $line (@lines) {
+       my ($dataset, $size, $origin, $type, $refquota) = split(/\s+/, $line);
+       next if !($type eq 'volume' || $type eq 'filesystem');
+
+       my $zvol = {};
+       my @parts = split /\//, $dataset;
+       next if scalar(@parts) < 2; # we need pool/name
+       my $name = pop @parts;
+       my $parsed_pool = join('/', @parts);
+       next if $parsed_pool ne $pool;
+
+       next unless $name =~ m!^(vm|base|subvol|basevol)-(\d+)-(\S+)$!;
+       $zvol->{owner} = $2;
+
+       $zvol->{name} = $name;
+       if ($type eq 'filesystem') {
+           if ($refquota eq 'none') {
+               $zvol->{size} = 0;
+           } else {
+               $zvol->{size} = $refquota + 0;
+           }
+           $zvol->{format} = 'subvol';
+       } else {
+           $zvol->{size} = $size + 0;
+           $zvol->{format} = 'raw';
+       }
+       if ($origin !~ /^-$/) {
+           $zvol->{origin} = $origin;
+       }
+       push @$list, $zvol;
+    }
+
+    return $list;
+}
+
+sub parse_volname {
+    my ($class, $volname) = @_;
+
+    if ($volname =~ m/^(((base|basevol)-(\d+)-\S+)\/)?((base|basevol|vm|subvol)-(\d+)-\S+)$/) {
+       my $format = ($6 eq 'subvol' || $6 eq 'basevol') ? 'subvol' : 'raw';
+       my $isBase = ($6 eq 'base' || $6 eq 'basevol');
+       return ('images', $5, $7, $2, $4, $isBase, $format);
+    }
+
+    die "unable to parse zfs volume name '$volname'\n";
+}
+
+# virtual zfs methods (subclass can overwrite them)
+
+sub on_add_hook {
+    my ($class, $storeid, $scfg, %param) = @_;
+
+    my $cfg_mountpoint = $scfg->{mountpoint};
+
+    # ignore failure, pool might currently not be imported
+    my $mountpoint;
+    eval {
+       my $res = $class->zfs_get_properties($scfg, 'mountpoint', $scfg->{pool}, 1);
+       $mountpoint = PVE::Storage::Plugin::verify_path($res, 1) if defined($res);
+    };
+
+    if (defined($cfg_mountpoint)) {
+       if (defined($mountpoint) && !($cfg_mountpoint =~ m|^\Q$mountpoint\E/?$|)) {
+           warn "warning for $storeid - mountpoint: $cfg_mountpoint " .
+                "does not match current mount point: $mountpoint\n";
+       }
+    } else {
+       $scfg->{mountpoint} = $mountpoint;
+    }
+
+    return;
+}
+
+sub path {
+    my ($class, $scfg, $volname, $storeid, $snapname) = @_;
+
+    my ($vtype, $name, $vmid) = $class->parse_volname($volname);
+
+    my $path = '';
+    my $mountpoint = $scfg->{mountpoint} // "/$scfg->{pool}";
+
+    if ($vtype eq "images") {
+       if ($name =~ m/^subvol-/ || $name =~ m/^basevol-/) {
+           $path = "$mountpoint/$name";
+       } else {
+           $path = "/dev/zvol/$scfg->{pool}/$name";
+       }
+       $path .= "\@$snapname" if defined($snapname);
+    } else {
+       die "$vtype is not allowed in ZFSPool!";
+    }
+
+    return ($path, $vmid, $vtype);
+}
+
+sub zfs_request {
+    my ($class, $scfg, $timeout, $method, @params) = @_;
+
+    my $cmd = [];
+
+    if ($method eq 'zpool_list') {
+       push @$cmd, 'zpool', 'list';
+    } elsif ($method eq 'zpool_import') {
+       push @$cmd, 'zpool', 'import';
+       $timeout = 15 if !$timeout || $timeout < 15;
+    } else {
+       push @$cmd, 'zfs', $method;
+    }
+    push @$cmd, @params;
+
+    my $msg = '';
+    my $output = sub { $msg .= "$_[0]\n" };
+
+    if (PVE::RPCEnvironment->is_worker()) {
+       $timeout = 60*60 if !$timeout;
+       $timeout = 60*5 if $timeout < 60*5;
+    } else {
+       $timeout = 10 if !$timeout;
+    }
+
+    run_command($cmd, errmsg => "zfs error", outfunc => $output, timeout => $timeout);
+
+    return $msg;
+}
+
+sub zfs_wait_for_zvol_link {
+    my ($class, $scfg, $volname, $timeout) = @_;
+
+    my $default_timeout = PVE::RPCEnvironment->is_worker() ? 60*5 : 10;
+    $timeout = $default_timeout if !defined($timeout);
+
+    my ($devname, undef, undef) = $class->path($scfg, $volname);
+
+    for (my $i = 1; $i <= $timeout; $i++) {
+       last if -b $devname;
+       die "timeout: no zvol device link for '$volname' found after $timeout sec found.\n"
+           if $i == $timeout;
+
+       sleep(1);
+    }
+}
+
+sub alloc_image {
+    my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_;
+
+    my $volname = $name;
+
+    if ($fmt eq 'raw') {
+
+       die "illegal name '$volname' - should be 'vm-$vmid-*'\n"
+           if $volname && $volname !~ m/^vm-$vmid-/;
+       $volname = $class->find_free_diskname($storeid, $scfg, $vmid, $fmt)
+           if !$volname;
+
+       $class->zfs_create_zvol($scfg, $volname, $size);
+       $class->zfs_wait_for_zvol_link($scfg, $volname);
+
+    } elsif ( $fmt eq 'subvol') {
+
+       die "illegal name '$volname' - should be 'subvol-$vmid-*'\n"
+           if $volname && $volname !~ m/^subvol-$vmid-/;
+       $volname = $class->find_free_diskname($storeid, $scfg, $vmid, $fmt)
+           if !$volname;
+
+       die "illegal name '$volname' - should be 'subvol-$vmid-*'\n"
+           if $volname !~ m/^subvol-$vmid-/;
+
+       $class->zfs_create_subvol($scfg, $volname, $size);
+
+    } else {
+       die "unsupported format '$fmt'";
+    }
+
+    return $volname;
+}
+
+sub free_image {
+    my ($class, $storeid, $scfg, $volname, $isBase) = @_;
+
+    my (undef, $name, undef) = $class->parse_volname($volname);
+
+    $class->zfs_delete_zvol($scfg, $name);
+
+    return undef;
+}
+
+sub list_images {
+    my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_;
+
+    my $zfs_list = $class->zfs_list_zvol($scfg);
+
+    my $res = [];
+
+    for my $info (values $zfs_list->%*) {
+       my $volname = $info->{name};
+       my $parent = $info->{parent};
+       my $owner = $info->{vmid};
+
+       if ($parent && $parent =~ m/^(\S+)\@__base__$/) {
+           my ($basename) = ($1);
+           $info->{volid} = "$storeid:$basename/$volname";
+       } else {
+           $info->{volid} = "$storeid:$volname";
+       }
+
+       if ($vollist) {
+           my $found = grep { $_ eq $info->{volid} } @$vollist;
+           next if !$found;
+       } else {
+           next if defined ($vmid) && ($owner ne $vmid);
+       }
+
+       push @$res, $info;
+    }
+    return $res;
+}
+
+sub zfs_get_properties {
+    my ($class, $scfg, $properties, $dataset, $timeout) = @_;
+
+    my $result = $class->zfs_request($scfg, $timeout, 'get', '-o', 'value',
+                                    '-Hp', $properties, $dataset);
+    my @values = split /\n/, $result;
+    return wantarray ? @values : $values[0];
+}
+
+sub zfs_get_pool_stats {
+    my ($class, $scfg) = @_;
+
+    my $available = 0;
+    my $used = 0;
+
+    my @lines = $class->zfs_get_properties($scfg, 'available,used', $scfg->{pool});
+
+    if($lines[0] =~ /^(\d+)$/) {
+       $available = $1;
+    }
+
+    if($lines[1] =~ /^(\d+)$/) {
+       $used = $1;
+    }
+
+    return ($available, $used);
+}
+
+sub zfs_create_zvol {
+    my ($class, $scfg, $zvol, $size) = @_;
+
+    # always align size to 1M as workaround until
+    # https://github.com/zfsonlinux/zfs/issues/8541 is solved
+    my $padding = (1024 - $size % 1024) % 1024;
+    $size = $size + $padding;
+
+    my $cmd = ['create'];
+
+    push @$cmd, '-s' if $scfg->{sparse};
+
+    push @$cmd, '-b', $scfg->{blocksize} if $scfg->{blocksize};
+
+    push @$cmd, '-V', "${size}k", "$scfg->{pool}/$zvol";
+
+    $class->zfs_request($scfg, undef, @$cmd);
+}
+
+sub zfs_create_subvol {
+    my ($class, $scfg, $volname, $size) = @_;
+
+    my $dataset = "$scfg->{pool}/$volname";
+    my $quota = $size ? "${size}k" : "none";
+
+    my $cmd = ['create', '-o', 'acltype=posixacl', '-o', 'xattr=sa',
+              '-o', "refquota=${quota}", $dataset];
+
+    $class->zfs_request($scfg, undef, @$cmd);
+}
+
+sub zfs_delete_zvol {
+    my ($class, $scfg, $zvol) = @_;
+
+    my $err;
+
+    for (my $i = 0; $i < 6; $i++) {
+
+       eval { $class->zfs_request($scfg, undef, 'destroy', '-r', "$scfg->{pool}/$zvol"); };
+       if ($err = $@) {
+           if ($err =~ m/^zfs error:(.*): dataset is busy.*/) {
+               sleep(1);
+           } elsif ($err =~ m/^zfs error:.*: dataset does not exist.*$/) {
+               $err = undef;
+               last;
+           } else {
+               die $err;
+           }
+       } else {
+           last;
+       }
+    }
+
+    die $err if $err;
+}
+
+sub zfs_list_zvol {
+    my ($class, $scfg) = @_;
+
+    my $text = $class->zfs_request(
+       $scfg,
+       10,
+       'list',
+       '-o',
+       'name,volsize,origin,type,refquota',
+       '-t',
+       'volume,filesystem',
+       '-d1',
+       '-Hp',
+       $scfg->{pool},
+    );
+    # It's still required to have zfs_parse_zvol_list filter by pool, because -d1 lists
+    # $scfg->{pool} too and while unlikely, it could be named to be mistaken for a volume.
+    my $zvols = zfs_parse_zvol_list($text, $scfg->{pool});
+    return {} if !$zvols;
+
+    my $list = {};
+    foreach my $zvol (@$zvols) {
+       my $name = $zvol->{name};
+       my $parent = $zvol->{origin};
+       if($zvol->{origin} && $zvol->{origin} =~ m/^$scfg->{pool}\/(\S+)$/){
+           $parent = $1;
+       }
+
+       $list->{$name} = {
+           name => $name,
+           size => $zvol->{size},
+           parent => $parent,
+           format => $zvol->{format},
+            vmid => $zvol->{owner},
+        };
+    }
+
+    return $list;
+}
+
+sub zfs_get_sorted_snapshot_list {
+    my ($class, $scfg, $volname, $sort_params) = @_;
+
+    my @params = ('-H', '-r', '-t', 'snapshot', '-o', 'name', $sort_params->@*);
+
+    my $vname = ($class->parse_volname($volname))[1];
+    push @params, "$scfg->{pool}\/$vname";
+
+    my $text = $class->zfs_request($scfg, undef, 'list', @params);
+    my @snapshots = split(/\n/, $text);
+
+    my $snap_names = [];
+    for my $snapshot (@snapshots) {
+       (my $snap_name = $snapshot) =~ s/^.*@//;
+       push $snap_names->@*, $snap_name;
+    }
+    return $snap_names;
+}
+
+sub status {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    my $total = 0;
+    my $free = 0;
+    my $used = 0;
+    my $active = 0;
+
+    eval {
+       ($free, $used) = $class->zfs_get_pool_stats($scfg);
+       $active = 1;
+       $total = $free + $used;
+    };
+    warn $@ if $@;
+
+    return ($total, $free, $used, $active);
+}
+
+sub volume_size_info {
+    my ($class, $scfg, $storeid, $volname, $timeout) = @_;
+
+    my (undef, $vname, undef, $parent, undef, undef, $format) =
+        $class->parse_volname($volname);
+
+    my $attr = $format eq 'subvol' ? 'refquota' : 'volsize';
+    my ($size, $used) = $class->zfs_get_properties($scfg, "$attr,usedbydataset", "$scfg->{pool}/$vname");
+
+    $used = ($used =~ /^(\d+)$/) ? $1 : 0;
+
+    if ($size =~ /^(\d+)$/) {
+       return wantarray ? ($1, $format, $used, $parent) : $1;
+    }
+
+    die "Could not get zfs volume size\n";
+}
+
+sub volume_snapshot {
+    my ($class, $scfg, $storeid, $volname, $snap) = @_;
+
+    my $vname = ($class->parse_volname($volname))[1];
+
+    $class->zfs_request($scfg, undef, 'snapshot', "$scfg->{pool}/$vname\@$snap");
+}
+
+sub volume_snapshot_delete {
+    my ($class, $scfg, $storeid, $volname, $snap, $running) = @_;
+
+    my $vname = ($class->parse_volname($volname))[1];
+
+    $class->deactivate_volume($storeid, $scfg, $vname, $snap, {});
+    $class->zfs_request($scfg, undef, 'destroy', "$scfg->{pool}/$vname\@$snap");
+}
+
+sub volume_snapshot_rollback {
+    my ($class, $scfg, $storeid, $volname, $snap) = @_;
+
+    my (undef, $vname, undef, undef, undef, undef, $format) = $class->parse_volname($volname);
+
+    my $msg = $class->zfs_request($scfg, undef, 'rollback', "$scfg->{pool}/$vname\@$snap");
+
+    # we have to unmount rollbacked subvols, to invalidate wrong kernel
+    # caches, they get mounted in activate volume again
+    # see zfs bug #10931 https://github.com/openzfs/zfs/issues/10931
+    if ($format eq 'subvol') {
+       eval { $class->zfs_request($scfg, undef, 'unmount', "$scfg->{pool}/$vname"); };
+       if (my $err = $@) {
+           die $err if $err !~ m/not currently mounted$/;
+       }
+    }
+
+    return $msg;
+}
+
+sub volume_rollback_is_possible {
+    my ($class, $scfg, $storeid, $volname, $snap, $blockers) = @_;
+
+    # can't use '-S creation', because zfs list won't reverse the order when the
+    # creation time is the same second, breaking at least our tests.
+    my $snapshots = $class->zfs_get_sorted_snapshot_list($scfg, $volname, ['-s', 'creation']);
+
+    my $found;
+    $blockers //= []; # not guaranteed to be set by caller
+    for my $snapshot ($snapshots->@*) {
+       if ($snapshot eq $snap) {
+           $found = 1;
+       } elsif ($found) {
+           push $blockers->@*, $snapshot;
+       }
+    }
+
+    my $volid = "${storeid}:${volname}";
+
+    die "can't rollback, snapshot '$snap' does not exist on '$volid'\n"
+       if !$found;
+
+    die "can't rollback, '$snap' is not most recent snapshot on '$volid'\n"
+       if scalar($blockers->@*) > 0;
+
+    return 1;
+}
+
+sub volume_snapshot_info {
+    my ($class, $scfg, $storeid, $volname) = @_;
+
+    my @params = ('-Hp', '-r', '-t', 'snapshot', '-o', 'name,guid,creation');
+
+    my $vname = ($class->parse_volname($volname))[1];
+    push @params, "$scfg->{pool}\/$vname";
+
+    my $text = $class->zfs_request($scfg, undef, 'list', @params);
+    my @lines = split(/\n/, $text);
+
+    my $info = {};
+    for my $line (@lines) {
+       my ($snapshot, $guid, $creation) = split(/\s+/, $line);
+       (my $snap_name = $snapshot) =~ s/^.*@//;
+
+       $info->{$snap_name} = {
+           id => $guid,
+           timestamp => $creation,
+       };
+    }
+    return $info;
+}
+
+my sub dataset_mounted_heuristic {
+    my ($dataset) = @_;
+
+    my $mounts = PVE::ProcFSTools::parse_proc_mounts();
+    for my $mp (@$mounts) {
+       my ($what, $dir, $fs) = $mp->@*;
+       next if $fs ne 'zfs';
+       # check for root-dataset or any child-dataset (root-dataset could have 'canmount=off')
+       # If any child is mounted heuristically assume that `zfs mount -a` was successful
+       next if $what !~ m!^$dataset(?:/|$)!;
+       return 1;
+    }
+    return 0;
+}
+
+sub activate_storage {
+    my ($class, $storeid, $scfg, $cache) = @_;
+
+    # Note: $scfg->{pool} can include dataset <pool>/<dataset>
+    my $dataset = $scfg->{pool};
+    my $pool = ($dataset =~ s!/.*$!!r);
+
+    return 1 if dataset_mounted_heuristic($dataset); # early return
+
+    my $pool_imported = sub {
+       my @param = ('-o', 'name', '-H', $pool);
+       my $res = eval { $class->zfs_request($scfg, undef, 'zpool_list', @param) };
+       warn "$@\n" if $@;
+
+       return defined($res) && $res =~ m/$pool/;
+    };
+
+    if (!$pool_imported->()) {
+       # import can only be done if not yet imported!
+       my @param = ('-d', '/dev/disk/by-id/', '-o', 'cachefile=none', $pool);
+       eval { $class->zfs_request($scfg, undef, 'zpool_import', @param) };
+       if (my $err = $@) {
+           # just could've raced with another import, so recheck if it is imported
+           die "could not activate storage '$storeid', $err\n" if !$pool_imported->();
+       }
+    }
+    eval { $class->zfs_request($scfg, undef, 'mount', '-a') };
+    die "could not activate storage '$storeid', $@\n" if $@;
+    return 1;
+}
+
+sub deactivate_storage {
+    my ($class, $storeid, $scfg, $cache) = @_;
+    return 1;
+}
+
+sub activate_volume {
+    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
+
+    return 1 if defined($snapname);
+
+    my (undef, $dataset, undef, undef, undef, undef, $format) = $class->parse_volname($volname);
+
+    if ($format eq 'raw') {
+       $class->zfs_wait_for_zvol_link($scfg, $volname);
+    } elsif ($format eq 'subvol') {
+       my $mounted = $class->zfs_get_properties($scfg, 'mounted', "$scfg->{pool}/$dataset");
+       if ($mounted !~ m/^yes$/) {
+           $class->zfs_request($scfg, undef, 'mount', "$scfg->{pool}/$dataset");
+       }
+    }
+
+    return 1;
+}
+
+sub deactivate_volume {
+    my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_;
+    return 1;
+}
+
+sub clone_image {
+    my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_;
+
+    $snap ||= '__base__';
+
+    my ($vtype, $basename, $basevmid, undef, undef, $isBase, $format) =
+        $class->parse_volname($volname);
+
+    die "clone_image only works on base images\n" if !$isBase;
+
+    my $name = $class->find_free_diskname($storeid, $scfg, $vmid, $format);
+
+    if ($format eq 'subvol') {
+       my $size = $class->zfs_request($scfg, undef, 'list', '-Hp', '-o', 'refquota', "$scfg->{pool}/$basename");
+       chomp($size);
+       $class->zfs_request($scfg, undef, 'clone', "$scfg->{pool}/$basename\@$snap", "$scfg->{pool}/$name", '-o', "refquota=$size");
+    } else {
+       $class->zfs_request($scfg, undef, 'clone', "$scfg->{pool}/$basename\@$snap", "$scfg->{pool}/$name");
+    }
+
+    return "$basename/$name";
+}
+
+sub create_base {
+    my ($class, $storeid, $scfg, $volname) = @_;
+
+    my $snap = '__base__';
+
+    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) =
+        $class->parse_volname($volname);
+
+    die "create_base not possible with base image\n" if $isBase;
+
+    my $newname = $name;
+    if ( $format eq 'subvol' ) {
+       $newname =~ s/^subvol-/basevol-/;
+    } else {
+       $newname =~ s/^vm-/base-/;
+    }
+    my $newvolname = $basename ? "$basename/$newname" : "$newname";
+
+    $class->zfs_request($scfg, undef, 'rename', "$scfg->{pool}/$name", "$scfg->{pool}/$newname");
+
+    my $running  = undef; #fixme : is create_base always offline ?
+
+    $class->volume_snapshot($scfg, $storeid, $newname, $snap, $running);
+
+    return $newvolname;
+}
+
+sub volume_resize {
+    my ($class, $scfg, $storeid, $volname, $size, $running) = @_;
+
+    my $new_size = int($size/1024);
+
+    my (undef, $vname, undef, undef, undef, undef, $format) =
+        $class->parse_volname($volname);
+
+    my $attr = $format eq 'subvol' ? 'refquota' : 'volsize';
+
+    # align size to 1M so we always have a valid multiple of the volume block size
+    if ($format eq 'raw') {
+       my $padding = (1024 - $new_size % 1024) % 1024;
+       $new_size = $new_size + $padding;
+    }
+
+    $class->zfs_request($scfg, undef, 'set', "$attr=${new_size}k", "$scfg->{pool}/$vname");
+
+    return $new_size;
+}
+
+sub storage_can_replicate {
+    my ($class, $scfg, $storeid, $format) = @_;
+
+    return 1 if $format eq 'raw' || $format eq 'subvol';
+
+    return 0;
+}
+
+sub volume_has_feature {
+    my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_;
+
+    my $features = {
+       snapshot => { current => 1, snap => 1},
+       clone => { base => 1},
+       template => { current => 1},
+       copy => { base => 1, current => 1},
+       sparseinit => { base => 1, current => 1},
+       replicate => { base => 1, current => 1},
+       rename => {current => 1},
+    };
+
+    my ($vtype, $name, $vmid, $basename, $basevmid, $isBase) =
+       $class->parse_volname($volname);
+
+    my $key = undef;
+
+    if ($snapname) {
+       $key = 'snap';
+    } else {
+       $key = $isBase ? 'base' : 'current';
+    }
+
+    return 1 if $features->{$feature}->{$key};
+
+    return undef;
+}
+
+sub volume_export {
+    my ($class, $scfg, $storeid, $fh, $volname, $format, $snapshot, $base_snapshot, $with_snapshots) = @_;
+
+    die "unsupported export stream format for $class: $format\n"
+       if $format ne 'zfs';
+
+    die "$class storage can only export snapshots\n"
+       if !defined($snapshot);
+
+    my $dataset = ($class->parse_volname($volname))[1];
+
+    my $fd = fileno($fh);
+    die "internal error: invalid file handle for volume_export\n"
+       if !defined($fd);
+    $fd = ">&$fd";
+
+    # For zfs we always create a replication stream (-R) which means the remote
+    # side will always delete non-existing source snapshots. This should work
+    # for all our use cases.
+    my $cmd = ['zfs', 'send', '-Rpv'];
+    if (defined($base_snapshot)) {
+       my $arg = $with_snapshots ? '-I' : '-i';
+       push @$cmd, $arg, $base_snapshot;
+    }
+    push @$cmd, '--', "$scfg->{pool}/$dataset\@$snapshot";
+
+    run_command($cmd, output => $fd);
+
+    return;
+}
+
+sub volume_export_formats {
+    my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
+
+    my @formats = ('zfs');
+    # TODOs:
+    # push @formats, 'fies' if $volname !~ /^(?:basevol|subvol)-/;
+    # push @formats, 'raw' if !$base_snapshot && !$with_snapshots;
+    return @formats;
+}
+
+sub volume_import {
+    my ($class, $scfg, $storeid, $fh, $volname, $format, $snapshot, $base_snapshot, $with_snapshots, $allow_rename) = @_;
+
+    die "unsupported import stream format for $class: $format\n"
+       if $format ne 'zfs';
+
+    my $fd = fileno($fh);
+    die "internal error: invalid file handle for volume_import\n"
+       if !defined($fd);
+
+    my (undef, $dataset, $vmid, undef, undef, undef, $volume_format) =
+       $class->parse_volname($volname);
+
+    my $zfspath = "$scfg->{pool}/$dataset";
+    my $suffix = defined($base_snapshot) ? "\@$base_snapshot" : '';
+    my $exists = 0 == run_command(['zfs', 'get', '-H', 'name', $zfspath.$suffix],
+                                 noerr => 1, quiet => 1);
+    if (defined($base_snapshot)) {
+       die "base snapshot '$zfspath\@$base_snapshot' doesn't exist\n" if !$exists;
+    } elsif ($exists) {
+       die "volume '$zfspath' already exists\n" if !$allow_rename;
+       warn "volume '$zfspath' already exists - importing with a different name\n";
+       $dataset = $class->find_free_diskname($storeid, $scfg, $vmid, $volume_format);
+       $zfspath = "$scfg->{pool}/$dataset";
+    }
+
+    eval { run_command(['zfs', 'recv', '-F', '--', $zfspath], input => "<&$fd") };
+    if (my $err = $@) {
+       if (defined($base_snapshot)) {
+           eval { run_command(['zfs', 'rollback', '-r', '--', "$zfspath\@$base_snapshot"]) };
+       } else {
+           eval { run_command(['zfs', 'destroy', '-r', '--', $zfspath]) };
+       }
+       die $err;
+    }
+
+    return "$storeid:$dataset";
+}
+
+sub volume_import_formats {
+    my ($class, $scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots) = @_;
+
+    return $class->volume_export_formats($scfg, $storeid, $volname, $snapshot, $base_snapshot, $with_snapshots);
+}
+
+sub rename_volume {
+    my ($class, $scfg, $storeid, $source_volname, $target_vmid, $target_volname) = @_;
+
+    my (
+       undef,
+       $source_image,
+       $source_vmid,
+       $base_name,
+       $base_vmid,
+       undef,
+       $format
+    ) = $class->parse_volname($source_volname);
+    $target_volname = $class->find_free_diskname($storeid, $scfg, $target_vmid, $format)
+       if !$target_volname;
+
+    my $pool = $scfg->{pool};
+    my $source_zfspath = "${pool}/${source_image}";
+    my $target_zfspath = "${pool}/${target_volname}";
+
+    my $exists = 0 == run_command(['zfs', 'get', '-H', 'name', $target_zfspath],
+                                 noerr => 1, quiet => 1);
+    die "target volume '${target_volname}' already exists\n" if $exists;
+
+    $class->zfs_request($scfg, 5, 'rename', ${source_zfspath}, ${target_zfspath});
+
+    $base_name = $base_name ? "${base_name}/" : '';
+
+    return "${storeid}:${base_name}${target_volname}";
+}
+
+1;
diff --git a/src/bin/Makefile b/src/bin/Makefile
new file mode 100644 (file)
index 0000000..45e133b
--- /dev/null
@@ -0,0 +1,36 @@
+DESTDIR=
+PREFIX=/usr
+SBINDIR=$(PREFIX)/sbin
+MANDIR=$(PREFIX)/share/man
+MAN1DIR=$(MANDIR)/man1/
+BASHCOMPLDIR=$(PREFIX)/share/bash-completion/completions/
+ZSHCOMPLDIR=$(PREFIX)/share/zsh/vendor-completions/
+
+export PERLDIR=$(PREFIX)/share/perl5
+
+PERL_DOC_INC_DIRS=..
+-include /usr/share/pve-doc-generator/pve-doc-generator.mk
+
+all:
+
+pvesm.bash-completion:
+       perl -I.. -T -e "use PVE::CLI::pvesm; PVE::CLI::pvesm->generate_bash_completions();" >$@.tmp
+       mv $@.tmp $@
+
+pvesm.zsh-completion:
+       perl -I.. -T -e "use PVE::CLI::pvesm; PVE::CLI::pvesm->generate_zsh_completions();" >$@.tmp
+       mv $@.tmp $@
+
+.PHONY: install
+install: pvesm.1 pvesm.bash-completion pvesm.zsh-completion
+       install -d $(DESTDIR)$(SBINDIR)
+       install -m 0755 pvesm $(DESTDIR)$(SBINDIR)
+       install -d $(DESTDIR)$(MAN1DIR)
+       install -m 0644 pvesm.1 $(DESTDIR)$(MAN1DIR)
+       gzip -9 -n $(DESTDIR)$(MAN1DIR)/pvesm.1
+       install -m 0644 -D pvesm.bash-completion $(DESTDIR)$(BASHCOMPLDIR)/pvesm
+       install -m 0644 -D pvesm.zsh-completion $(DESTDIR)$(ZSHCOMPLDIR)/_pvesm
+
+.PHONY: clean
+clean:
+       rm -f *.xml.tmp *.1 *.5 *.8 *{synopsis,opts}.adoc docinfo.xml
diff --git a/src/bin/pvesm b/src/bin/pvesm
new file mode 100755 (executable)
index 0000000..ece9be8
--- /dev/null
@@ -0,0 +1,8 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use PVE::CLI::pvesm;
+
+PVE::CLI::pvesm->run_cli_handler();
diff --git a/src/test/Makefile b/src/test/Makefile
new file mode 100644 (file)
index 0000000..c54b10f
--- /dev/null
@@ -0,0 +1,15 @@
+all: test
+
+test: test_zfspoolplugin test_disklist test_bwlimit test_plugin
+
+test_zfspoolplugin: run_test_zfspoolplugin.pl
+       ./run_test_zfspoolplugin.pl
+
+test_disklist: run_disk_tests.pl
+       ./run_disk_tests.pl
+
+test_bwlimit: run_bwlimit_tests.pl
+       ./run_bwlimit_tests.pl
+
+test_plugin: run_plugin_tests.pl
+       ./run_plugin_tests.pl
diff --git a/src/test/archive_info_test.pm b/src/test/archive_info_test.pm
new file mode 100644 (file)
index 0000000..cf03c3d
--- /dev/null
@@ -0,0 +1,198 @@
+package PVE::Storage::TestArchiveInfo;
+
+use strict;
+use warnings;
+
+use lib qw(..);
+
+use PVE::Storage;
+use Test::More;
+
+my $vmid = 16110;
+
+my $LOG_EXT = PVE::Storage::Plugin::LOG_EXT;
+my $NOTES_EXT = PVE::Storage::Plugin::NOTES_EXT;
+
+# an array of test cases, each test is comprised of the following keys:
+# description => to identify a single test
+# archive     => the input filename for archive_info
+# expected    => the hash that archive_info returns
+#
+# most of them are created further below
+my $tests = [
+    # backup archives
+    {
+       description => 'Backup archive, lxc, tgz, future millenium',
+       archive     => "backup/vzdump-lxc-$vmid-3070_01_01-00_00_00.tgz",
+       expected    => {
+           'filename'     => "vzdump-lxc-$vmid-3070_01_01-00_00_00.tgz",
+           'logfilename'  => "vzdump-lxc-$vmid-3070_01_01-00_00_00".$LOG_EXT,
+           'notesfilename'=> "vzdump-lxc-$vmid-3070_01_01-00_00_00.tgz".$NOTES_EXT,
+           'type'         => 'lxc',
+           'format'       => 'tar',
+           'decompressor' => ['tar', '-z'],
+           'compression'  => 'gz',
+           'vmid'         => $vmid,
+           'ctime'        => 60*60*24 * (365*1100 + 267),
+           'is_std_name'  => 1,
+       },
+    },
+    {
+       description => 'Backup archive, lxc, tgz, very old',
+       archive     => "backup/vzdump-lxc-$vmid-1970_01_01-02_00_30.tgz",
+       expected    => {
+           'filename'     => "vzdump-lxc-$vmid-1970_01_01-02_00_30.tgz",
+           'logfilename'  => "vzdump-lxc-$vmid-1970_01_01-02_00_30".$LOG_EXT,
+           'notesfilename'=> "vzdump-lxc-$vmid-1970_01_01-02_00_30.tgz".$NOTES_EXT,
+           'type'         => 'lxc',
+           'format'       => 'tar',
+           'decompressor' => ['tar', '-z'],
+           'compression'  => 'gz',
+           'vmid'         => $vmid,
+           'ctime'        => 60*60*2 + 30,
+           'is_std_name'  => 1,
+       },
+    },
+    {
+       description => 'Backup archive, lxc, tgz',
+       archive     => "backup/vzdump-lxc-$vmid-2020_03_30-21_39_30.tgz",
+       expected    => {
+           'filename'     => "vzdump-lxc-$vmid-2020_03_30-21_39_30.tgz",
+           'logfilename'  => "vzdump-lxc-$vmid-2020_03_30-21_39_30".$LOG_EXT,
+           'notesfilename'=> "vzdump-lxc-$vmid-2020_03_30-21_39_30.tgz".$NOTES_EXT,
+           'type'         => 'lxc',
+           'format'       => 'tar',
+           'decompressor' => ['tar', '-z'],
+           'compression'  => 'gz',
+           'vmid'         => $vmid,
+           'ctime'        => 1585604370,
+           'is_std_name'  => 1,
+       },
+    },
+    {
+       description => 'Backup archive, openvz, tgz',
+       archive     => "backup/vzdump-openvz-$vmid-2020_03_30-21_39_30.tgz",
+       expected    => {
+           'filename'     => "vzdump-openvz-$vmid-2020_03_30-21_39_30.tgz",
+           'logfilename'  => "vzdump-openvz-$vmid-2020_03_30-21_39_30".$LOG_EXT,
+           'notesfilename'=> "vzdump-openvz-$vmid-2020_03_30-21_39_30.tgz".$NOTES_EXT,
+           'type'         => 'openvz',
+           'format'       => 'tar',
+           'decompressor' => ['tar', '-z'],
+           'compression'  => 'gz',
+           'vmid'         => $vmid,
+           'ctime'        => 1585604370,
+           'is_std_name'  => 1,
+       },
+    },
+    {
+       description => 'Backup archive, custom dump directory, qemu, tgz',
+       archive     => "/here/be/Back-ups/vzdump-qemu-$vmid-2020_03_30-21_39_30.tgz",
+       expected    => {
+           'filename'     => "vzdump-qemu-$vmid-2020_03_30-21_39_30.tgz",
+           'logfilename'  => "vzdump-qemu-$vmid-2020_03_30-21_39_30".$LOG_EXT,
+           'notesfilename'=> "vzdump-qemu-$vmid-2020_03_30-21_39_30.tgz".$NOTES_EXT,
+           'type'         => 'qemu',
+           'format'       => 'tar',
+           'decompressor' => ['tar', '-z'],
+           'compression'  => 'gz',
+           'vmid'         => $vmid,
+           'ctime'        => 1585604370,
+           'is_std_name'  => 1,
+       },
+    },
+    {
+       description => 'Backup archive, none, tgz',
+       archive     => "backup/vzdump-qemu-$vmid-whatever-the-name_is_here.tgz",
+       expected    => {
+           'filename'     => "vzdump-qemu-$vmid-whatever-the-name_is_here.tgz",
+           'type'         => 'qemu',
+           'format'       => 'tar',
+           'decompressor' => ['tar', '-z'],
+           'compression'  => 'gz',
+           'is_std_name'  => 0,
+       },
+    },
+];
+
+# add new compression fromats to test
+my $decompressor = {
+    tar => {
+       gz  => ['tar', '-z'],
+       lzo => ['tar', '--lzop'],
+       zst => ['tar', '--zstd'],
+    },
+    vma => {
+       gz  => ['zcat'],
+       lzo => ['lzop', '-d', '-c'],
+       zst => ['zstd', '-q', '-d', '-c'],
+    },
+};
+
+my $bkp_suffix = {
+    qemu   => [ 'vma', $decompressor->{vma}, ],
+    lxc    => [ 'tar', $decompressor->{tar}, ],
+    openvz => [ 'tar', $decompressor->{tar}, ],
+};
+
+# create more test cases for backup files matches
+for my $virt (sort keys %$bkp_suffix) {
+    my ($format, $decomp) = $bkp_suffix->{$virt}->@*;
+    my $archive_name = "vzdump-$virt-$vmid-2020_03_30-21_12_40";
+
+    for my $suffix (sort keys %$decomp) {
+       push @$tests, {
+           description => "Backup archive, $virt, $format.$suffix",
+           archive     => "backup/$archive_name.$format.$suffix",
+           expected    => {
+               'filename'     => "$archive_name.$format.$suffix",
+               'logfilename'  => $archive_name.$LOG_EXT,
+               'notesfilename'=> "$archive_name.$format.$suffix".$NOTES_EXT,
+               'type'         => "$virt",
+               'format'       => "$format",
+               'decompressor' => $decomp->{$suffix},
+               'compression'  => "$suffix",
+               'vmid'         => $vmid,
+               'ctime'        => 1585602760,
+               'is_std_name'  => 1,
+           },
+       };
+    }
+}
+
+
+# add compression formats to test failed matches
+my $non_bkp_suffix = {
+    'openvz' => [ 'zip', 'tgz.lzo', 'tar.bz2', 'zip.gz', '', ],
+    'lxc'    => [ 'zip', 'tgz.lzo', 'tar.bz2', 'zip.gz', '', ],
+    'qemu'   => [ 'vma.xz', 'vms.gz', 'vmx.zst', '', ],
+    'none'   => [ 'tar.gz', ],
+};
+
+# create tests for failed matches
+for my $virt (sort keys %$non_bkp_suffix) {
+    my $suffix = $non_bkp_suffix->{$virt};
+    for my $s (@$suffix) {
+       my $archive = "backup/vzdump-$virt-$vmid-2020_03_30-21_12_40.$s";
+       push @$tests, {
+           description => "Failed match: Backup archive, $virt, $s",
+           archive     => $archive,
+           expected    => "ERROR: couldn't determine archive info from '$archive'\n",
+       };
+    }
+}
+
+
+plan tests => scalar @$tests;
+
+for my $tt (@$tests) {
+
+    my $got = eval { PVE::Storage::archive_info($tt->{archive}) };
+    $got = $@ if $@;
+
+    is_deeply($got, $tt->{expected}, $tt->{description}) || diag(explain($got));
+}
+
+done_testing();
+
+1;
diff --git a/src/test/disk_tests/cciss/cciss!c0d0/device/model b/src/test/disk_tests/cciss/cciss!c0d0/device/model
new file mode 100644 (file)
index 0000000..676b97d
--- /dev/null
@@ -0,0 +1 @@
+LOGICAL_VOLUME
diff --git a/src/test/disk_tests/cciss/cciss!c0d0/device/vendor b/src/test/disk_tests/cciss/cciss!c0d0/device/vendor
new file mode 100644 (file)
index 0000000..e8b2ad6
--- /dev/null
@@ -0,0 +1 @@
+HP
diff --git a/src/test/disk_tests/cciss/cciss!c0d0/queue/rotational b/src/test/disk_tests/cciss/cciss!c0d0/queue/rotational
new file mode 100644 (file)
index 0000000..d00491f
--- /dev/null
@@ -0,0 +1 @@
+1
diff --git a/src/test/disk_tests/cciss/cciss!c0d0/size b/src/test/disk_tests/cciss/cciss!c0d0/size
new file mode 100644 (file)
index 0000000..f599e28
--- /dev/null
@@ -0,0 +1 @@
+10
diff --git a/src/test/disk_tests/cciss/cciss!c0d0_udevadm b/src/test/disk_tests/cciss/cciss!c0d0_udevadm
new file mode 100644 (file)
index 0000000..21e8899
--- /dev/null
@@ -0,0 +1,32 @@
+P: /devices/pci0000:40/0000:40:13.0/0000:45:00.0/cciss0/c0d0/block/cciss!c0d0
+N: cciss/c0d0
+S: disk/by-id/cciss-SERIAL111
+S: disk/by-id/wwn-0x00000000000000000000000000000000
+S: disk/by-path/pci-0000:45:00.0-cciss-disk0
+E: DEVLINKS=/dev/disk/by-id/cciss-000000000000000000000000000000000 /dev/disk/by-id/wwn-0x000000000000000000000000000000000/dev/disk/by-path/pci-0000:45:00.0-cciss-disk0
+E: DEVNAME=/dev/cciss/c0d0
+E: DEVPATH=/devices/pci0000:40/0000:40:13.0/0000:45:00.0/cciss0/c0d0/block/cciss!c0d0
+E: DEVTYPE=disk
+E: ID_BUS=cciss
+E: ID_MODEL=LOGICAL_VOLUME
+E: ID_MODEL_ENC=LOGICAL\x20VOLUME\x20\x20
+E: ID_PART_TABLE_TYPE=gpt
+E: ID_PART_TABLE_UUID=cfe72deb-65d1-487c-bdfa-8af66dc1a969
+E: ID_PATH=pci-0000:45:00.0-cciss-disk0
+E: ID_PATH_TAG=pci-0000_45_00_0-cciss-disk0
+E: ID_REVISION=7.24
+E: ID_SCSI=1
+E: ID_SCSI_SERIAL=SERIAL1
+E: ID_SERIAL=SERIAL111
+E: ID_SERIAL_SHORT=SER111
+E: ID_TYPE=disk
+E: ID_VENDOR=HP
+E: ID_VENDOR_ENC=HP\x20\x20\x20\x20\x20\x20
+E: ID_WWN=0x0000000000000000
+E: ID_WWN_VENDOR_EXTENSION=0x0000000000000000
+E: ID_WWN_WITH_EXTENSION=0x00000000000000000000000000000000
+E: MAJOR=104
+E: MINOR=0
+E: SUBSYSTEM=block
+E: TAGS=:systemd:
+E: USEC_INITIALIZED=2247
diff --git a/src/test/disk_tests/cciss/disklist b/src/test/disk_tests/cciss/disklist
new file mode 100644 (file)
index 0000000..aa174aa
--- /dev/null
@@ -0,0 +1 @@
+cciss!c0d0
diff --git a/src/test/disk_tests/cciss/disklist_expected.json b/src/test/disk_tests/cciss/disklist_expected.json
new file mode 100644 (file)
index 0000000..eff58db
--- /dev/null
@@ -0,0 +1,16 @@
+{
+    "cciss/c0d0" : {
+       "wearout" : "N/A",
+       "vendor" : "HP",
+       "rpm" : -1,
+       "type" : "unknown",
+       "serial" : "SER111",
+       "osdid" : -1,
+       "health" : "UNKNOWN",
+       "model" : "LOGICAL_VOLUME",
+       "size" : 5120,
+       "wwn" : "0x0000000000000000",
+       "gpt" : 1,
+       "devpath" : "/dev/cciss/c0d0"
+    }
+}
diff --git a/src/test/disk_tests/hdd_smart/disklist b/src/test/disk_tests/hdd_smart/disklist
new file mode 100644 (file)
index 0000000..9f6776c
--- /dev/null
@@ -0,0 +1,2 @@
+sda
+sdb
diff --git a/src/test/disk_tests/hdd_smart/disklist_expected.json b/src/test/disk_tests/hdd_smart/disklist_expected.json
new file mode 100644 (file)
index 0000000..02a341e
--- /dev/null
@@ -0,0 +1,30 @@
+{
+    "sdb" : {
+       "devpath" : "/dev/sdb",
+       "size" : 1024000,
+       "gpt" : 1,
+       "osdid" : -1,
+       "rpm" : 7200,
+       "model" : "ST4000NM0033-9ZM170",
+       "vendor" : "ATA",
+       "health" : "PASSED",
+       "type" : "hdd",
+       "wwn" : "0x0000000000000000",
+       "wearout" : "N/A",
+       "serial" : "00000000"
+    },
+    "sda" : {
+       "osdid" : -1,
+       "size" : 1024000,
+       "gpt" : 1,
+       "devpath" : "/dev/sda",
+       "model" : "ST4000DM000-1F2168",
+       "rpm" : 5900,
+       "type" : "hdd",
+       "health" : "PASSED",
+       "vendor" : "ATA",
+       "serial" : "00000000",
+       "wearout" : "N/A",
+       "wwn" : "0x0000000000000000"
+    }
+}
diff --git a/src/test/disk_tests/hdd_smart/sda/device/vendor b/src/test/disk_tests/hdd_smart/sda/device/vendor
new file mode 100644 (file)
index 0000000..531030d
--- /dev/null
@@ -0,0 +1 @@
+ATA
diff --git a/src/test/disk_tests/hdd_smart/sda/queue/rotational b/src/test/disk_tests/hdd_smart/sda/queue/rotational
new file mode 100644 (file)
index 0000000..d00491f
--- /dev/null
@@ -0,0 +1 @@
+1
diff --git a/src/test/disk_tests/hdd_smart/sda/size b/src/test/disk_tests/hdd_smart/sda/size
new file mode 100644 (file)
index 0000000..8bd1af1
--- /dev/null
@@ -0,0 +1 @@
+2000
diff --git a/src/test/disk_tests/hdd_smart/sda_health b/src/test/disk_tests/hdd_smart/sda_health
new file mode 100644 (file)
index 0000000..faf4ce3
--- /dev/null
@@ -0,0 +1,5 @@
+smartctl 6.6 2016-05-31 r4324 [x86_64-linux-4.4.21-1-pve] (local build)
+Copyright (C) 2002-16, Bruce Allen, Christian Franke, www.smartmontools.org
+
+=== START OF READ SMART DATA SECTION ===
+SMART overall-health self-assessment test result: PASSED
diff --git a/src/test/disk_tests/hdd_smart/sda_smart b/src/test/disk_tests/hdd_smart/sda_smart
new file mode 100644 (file)
index 0000000..a3f8f0a
--- /dev/null
@@ -0,0 +1,40 @@
+smartctl 6.6 2016-05-31 r4324 [x86_64-linux-4.4.21-1-pve] (local build)
+Copyright (C) 2002-16, Bruce Allen, Christian Franke, www.smartmontools.org
+
+=== START OF READ SMART DATA SECTION ===
+SMART overall-health self-assessment test result: PASSED
+
+SMART Attributes Data Structure revision number: 10
+Vendor Specific SMART Attributes with Thresholds:
+ID# ATTRIBUTE_NAME          FLAGS    VALUE WORST THRESH FAIL RAW_VALUE
+  1 Raw_Read_Error_Rate     POSR--   117   099   006    -    158983656
+  3 Spin_Up_Time            PO----   092   091   000    -    0
+  4 Start_Stop_Count        -O--CK   100   100   020    -    189
+  5 Reallocated_Sector_Ct   PO--CK   100   100   010    -    0
+  7 Seek_Error_Rate         POSR--   075   060   030    -    30779387
+  9 Power_On_Hours          -O--CK   099   099   000    -    1250
+ 10 Spin_Retry_Count        PO--C-   100   100   097    -    0
+ 12 Power_Cycle_Count       -O--CK   100   100   020    -    190
+183 Runtime_Bad_Block       -O--CK   100   100   000    -    0
+184 End-to-End_Error        -O--CK   100   100   099    -    0
+187 Reported_Uncorrect      -O--CK   100   100   000    -    0
+188 Command_Timeout         -O--CK   100   100   000    -    0 0 0
+189 High_Fly_Writes         -O-RCK   100   100   000    -    0
+190 Airflow_Temperature_Cel -O---K   069   061   045    -    31 (Min/Max 20/33)
+191 G-Sense_Error_Rate      -O--CK   100   100   000    -    0
+192 Power-Off_Retract_Count -O--CK   100   100   000    -    43
+193 Load_Cycle_Count        -O--CK   100   100   000    -    201
+194 Temperature_Celsius     -O---K   031   040   000    -    31 (0 17 0 0 0)
+197 Current_Pending_Sector  -O--C-   100   100   000    -    0
+198 Offline_Uncorrectable   ----C-   100   100   000    -    0
+199 UDMA_CRC_Error_Count    -OSRCK   200   200   000    -    0
+240 Head_Flying_Hours       ------   100   253   000    -    1259h+06m+33.546s
+241 Total_LBAs_Written      ------   100   253   000    -    24013587236
+242 Total_LBAs_Read         ------   100   253   000    -    66916845706732
+                            ||||||_ K auto-keep
+                            |||||__ C event count
+                            ||||___ R error rate
+                            |||____ S speed/performance
+                            ||_____ O updated online
+                            |______ P prefailure warning
+
diff --git a/src/test/disk_tests/hdd_smart/sda_smart_expected.json b/src/test/disk_tests/hdd_smart/sda_smart_expected.json
new file mode 100644 (file)
index 0000000..73cd01c
--- /dev/null
@@ -0,0 +1,270 @@
+{
+    "attributes" : [
+       {
+           "threshold" : 6,
+           "fail" : "-",
+           "flags" : "POSR--",
+           "normalized" : 117,
+           "value" : 117,
+           "id" : "  1",
+           "raw" : "158983656",
+           "name" : "Raw_Read_Error_Rate",
+           "worst" : 99
+       },
+       {
+           "flags" : "PO----",
+           "normalized" : 92,
+           "value" : 92,
+           "raw" : "0",
+           "name" : "Spin_Up_Time",
+           "worst" : 91,
+           "id" : "  3",
+           "fail" : "-",
+           "threshold" : 0
+       },
+       {
+           "normalized" : 100,
+           "value" : 100,
+           "flags" : "-O--CK",
+           "id" : "  4",
+           "name" : "Start_Stop_Count",
+           "worst" : 100,
+           "raw" : "189",
+           "threshold" : 20,
+           "fail" : "-"
+       },
+       {
+           "normalized" : 100,
+           "value" : 100,
+           "flags" : "PO--CK",
+           "id" : "  5",
+           "name" : "Reallocated_Sector_Ct",
+           "worst" : 100,
+           "raw" : "0",
+           "threshold" : 10,
+           "fail" : "-"
+       },
+       {
+           "flags" : "POSR--",
+           "normalized" : 75,
+           "value" : 75,
+           "raw" : "30779387",
+           "worst" : 60,
+           "name" : "Seek_Error_Rate",
+           "id" : "  7",
+           "fail" : "-",
+           "threshold" : 30
+       },
+       {
+           "raw" : "1250",
+           "worst" : 99,
+           "name" : "Power_On_Hours",
+           "id" : "  9",
+           "flags" : "-O--CK",
+           "normalized" : 99,
+           "value" : 99,
+           "fail" : "-",
+           "threshold" : 0
+       },
+       {
+           "normalized" : 100,
+           "value" : 100,
+           "flags" : "PO--C-",
+           "id" : " 10",
+           "name" : "Spin_Retry_Count",
+           "worst" : 100,
+           "raw" : "0",
+           "threshold" : 97,
+           "fail" : "-"
+       },
+       {
+           "flags" : "-O--CK",
+           "normalized" : 100,
+           "value" : 100,
+           "id" : " 12",
+           "raw" : "190",
+           "worst" : 100,
+           "name" : "Power_Cycle_Count",
+           "threshold" : 20,
+           "fail" : "-"
+       },
+       {
+           "threshold" : 0,
+           "fail" : "-",
+           "flags" : "-O--CK",
+           "normalized" : 100,
+           "value" : 100,
+           "id" : "183",
+           "raw" : "0",
+           "worst" : 100,
+           "name" : "Runtime_Bad_Block"
+       },
+       {
+           "fail" : "-",
+           "threshold" : 99,
+           "flags" : "-O--CK",
+           "normalized" : 100,
+           "value" : 100,
+           "raw" : "0",
+           "worst" : 100,
+           "name" : "End-to-End_Error",
+           "id" : "184"
+       },
+       {
+           "worst" : 100,
+           "name" : "Reported_Uncorrect",
+           "raw" : "0",
+           "id" : "187",
+           "normalized" : 100,
+           "value" : 100,
+           "flags" : "-O--CK",
+           "fail" : "-",
+           "threshold" : 0
+       },
+       {
+           "flags" : "-O--CK",
+           "normalized" : 100,
+           "value" : 100,
+           "raw" : "0 0 0",
+           "worst" : 100,
+           "name" : "Command_Timeout",
+           "id" : "188",
+           "fail" : "-",
+           "threshold" : 0
+       },
+       {
+           "threshold" : 0,
+           "fail" : "-",
+           "flags" : "-O-RCK",
+           "normalized" : 100,
+           "value" : 100,
+           "id" : "189",
+           "raw" : "0",
+           "name" : "High_Fly_Writes",
+           "worst" : 100
+       },
+       {
+           "worst" : 61,
+           "name" : "Airflow_Temperature_Cel",
+           "raw" : "31 (Min/Max 20/33)",
+           "id" : "190",
+           "normalized" : 69,
+           "value" : 69,
+           "flags" : "-O---K",
+           "fail" : "-",
+           "threshold" : 45
+       },
+       {
+           "fail" : "-",
+           "threshold" : 0,
+           "worst" : 100,
+           "name" : "G-Sense_Error_Rate",
+           "raw" : "0",
+           "id" : "191",
+           "normalized" : 100,
+           "value" : 100,
+           "flags" : "-O--CK"
+       },
+       {
+           "id" : "192",
+           "raw" : "43",
+           "name" : "Power-Off_Retract_Count",
+           "worst" : 100,
+           "flags" : "-O--CK",
+           "normalized" : 100,
+           "value" : 100,
+           "threshold" : 0,
+           "fail" : "-"
+       },
+       {
+           "flags" : "-O--CK",
+           "normalized" : 100,
+           "value" : 100,
+           "raw" : "201",
+           "worst" : 100,
+           "name" : "Load_Cycle_Count",
+           "id" : "193",
+           "fail" : "-",
+           "threshold" : 0
+       },
+       {
+           "fail" : "-",
+           "threshold" : 0,
+           "normalized" : 31,
+           "value" : 31,
+           "flags" : "-O---K",
+           "name" : "Temperature_Celsius",
+           "worst" : 40,
+           "raw" : "31 (0 17 0 0 0)",
+           "id" : "194"
+       },
+       {
+           "normalized" : 100,
+           "value" : 100,
+           "flags" : "-O--C-",
+           "id" : "197",
+           "worst" : 100,
+           "name" : "Current_Pending_Sector",
+           "raw" : "0",
+           "threshold" : 0,
+           "fail" : "-"
+       },
+       {
+           "worst" : 100,
+           "name" : "Offline_Uncorrectable",
+           "raw" : "0",
+           "id" : "198",
+           "normalized" : 100,
+           "value" : 100,
+           "flags" : "----C-",
+           "fail" : "-",
+           "threshold" : 0
+       },
+       {
+           "threshold" : 0,
+           "fail" : "-",
+           "normalized" : 200,
+           "value" : 200,
+           "flags" : "-OSRCK",
+           "id" : "199",
+           "worst" : 200,
+           "name" : "UDMA_CRC_Error_Count",
+           "raw" : "0"
+       },
+       {
+           "raw" : "1259h+06m+33.546s",
+           "name" : "Head_Flying_Hours",
+           "worst" : 253,
+           "id" : "240",
+           "flags" : "------",
+           "normalized" : 100,
+           "value" : 100,
+           "fail" : "-",
+           "threshold" : 0
+       },
+       {
+           "fail" : "-",
+           "threshold" : 0,
+           "name" : "Total_LBAs_Written",
+           "worst" : 253,
+           "raw" : "24013587236",
+           "id" : "241",
+           "normalized" : 100,
+           "value" : 100,
+           "flags" : "------"
+       },
+       {
+           "flags" : "------",
+           "normalized" : 100,
+           "value" : 100,
+           "id" : "242",
+           "raw" : "66916845706732",
+           "worst" : 253,
+           "name" : "Total_LBAs_Read",
+           "threshold" : 0,
+           "fail" : "-"
+       }
+    ],
+    "health" : "PASSED",
+    "type" : "ata"
+}
diff --git a/src/test/disk_tests/hdd_smart/sda_udevadm b/src/test/disk_tests/hdd_smart/sda_udevadm
new file mode 100644 (file)
index 0000000..d9b5497
--- /dev/null
@@ -0,0 +1,11 @@
+E: DEVNAME=/dev/sda
+E: DEVTYPE=disk
+E: ID_ATA_ROTATION_RATE_RPM=5900
+E: ID_BUS=ata
+E: ID_MODEL=ST4000DM000-1F2168
+E: ID_PART_TABLE_TYPE=gpt
+E: ID_PART_TABLE_UUID=4f2e07a6-5437-2b4e-b6e8-9cba98639324
+E: ID_SERIAL_SHORT=00000000
+E: ID_TYPE=disk
+E: ID_WWN=0x0000000000000000
+E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/src/test/disk_tests/hdd_smart/sdb/device/vendor b/src/test/disk_tests/hdd_smart/sdb/device/vendor
new file mode 100644 (file)
index 0000000..531030d
--- /dev/null
@@ -0,0 +1 @@
+ATA
diff --git a/src/test/disk_tests/hdd_smart/sdb/queue/rotational b/src/test/disk_tests/hdd_smart/sdb/queue/rotational
new file mode 100644 (file)
index 0000000..d00491f
--- /dev/null
@@ -0,0 +1 @@
+1
diff --git a/src/test/disk_tests/hdd_smart/sdb/size b/src/test/disk_tests/hdd_smart/sdb/size
new file mode 100644 (file)
index 0000000..8bd1af1
--- /dev/null
@@ -0,0 +1 @@
+2000
diff --git a/src/test/disk_tests/hdd_smart/sdb_health b/src/test/disk_tests/hdd_smart/sdb_health
new file mode 100644 (file)
index 0000000..faf4ce3
--- /dev/null
@@ -0,0 +1,5 @@
+smartctl 6.6 2016-05-31 r4324 [x86_64-linux-4.4.21-1-pve] (local build)
+Copyright (C) 2002-16, Bruce Allen, Christian Franke, www.smartmontools.org
+
+=== START OF READ SMART DATA SECTION ===
+SMART overall-health self-assessment test result: PASSED
diff --git a/src/test/disk_tests/hdd_smart/sdb_smart b/src/test/disk_tests/hdd_smart/sdb_smart
new file mode 100644 (file)
index 0000000..ce52bea
--- /dev/null
@@ -0,0 +1,36 @@
+smartctl 6.4 2014-10-07 r4002 [x86_64-linux-4.4.10-1-pve] (local build)
+Copyright (C) 2002-14, Bruce Allen, Christian Franke, www.smartmontools.org
+
+=== START OF READ SMART DATA SECTION ===
+SMART overall-health self-assessment test result: PASSED
+
+SMART Attributes Data Structure revision number: 10
+Vendor Specific SMART Attributes with Thresholds:
+ID# ATTRIBUTE_NAME          FLAG     VALUE WORST THRESH FAIL RAW_VALUE
+  1 Raw_Read_Error_Rate     POSR--   083   063   044    -    215697022
+  3 Spin_Up_Time            PO----   092   091   000    -    0
+  4 Start_Stop_Count        -O--CK   100   100   020    -    265
+  5 Reallocated_Sector_Ct   PO--CK   100   100   010    -    0
+  7 Seek_Error_Rate         POSR--   091   060   030    -    1572375006
+  9 Power_On_Hours          -O--CK   089   089   000    -    9885
+ 10 Spin_Retry_Count        PO--C-   100   100   097    -    0
+ 12 Power_Cycle_Count       -O--CK   100   100   020    -    265
+184 End-to-End_Error        -O--CK   100   100   099    -    0
+187 Reported_Uncorrect      -O--CK   100   100   000    -    0
+188 Command_Timeout         -O--CK   100   100   000    -    0
+189 High_Fly_Writes         -O--CK   100   100   000    -    0
+190 Airflow_Temperature_Cel -O-RCK   045   036   045    NOW  55 (147 229 55 24 0)
+191 G-Sense_Error_Rate      -O---K   100   100   000    -    0
+192 Power-Off_Retract_Count -O--CK   100   100   000    -    57
+193 Load_Cycle_Count        -O--CK   100   100   000    -    265
+194 Temperature_Celsius     -O--CK   055   064   000    -    55 (0 16 0 0 0)
+195 Hardware_ECC_Recovered  -O---K   023   013   000    -    215697022
+197 Current_Pending_Sector  -O--C-   100   100   000    -    0
+198 Offline_Uncorrectable   ----C-   100   100   000    -    0
+199 UDMA_CRC_Error_Count    -OSRCK   200   200   000    -    0
+                            ||||||_ K auto-keep
+                            |||||__ C event count
+                            ||||___ R error rate
+                            |||____ S speed/performance
+                            ||_____ O updated online
+                            |______ P prefailure warning
diff --git a/src/test/disk_tests/hdd_smart/sdb_smart_expected.json b/src/test/disk_tests/hdd_smart/sdb_smart_expected.json
new file mode 100644 (file)
index 0000000..ae0f014
--- /dev/null
@@ -0,0 +1,237 @@
+{
+    "attributes" : [
+       {
+           "threshold" : 44,
+           "fail" : "-",
+           "id" : "  1",
+           "name" : "Raw_Read_Error_Rate",
+           "worst" : 63,
+           "raw" : "215697022",
+           "normalized" : 83,
+           "value" : 83,
+           "flags" : "POSR--"
+       },
+       {
+           "flags" : "PO----",
+           "normalized" : 92,
+           "value" : 92,
+           "id" : "  3",
+           "raw" : "0",
+           "worst" : 91,
+           "name" : "Spin_Up_Time",
+           "threshold" : 0,
+           "fail" : "-"
+       },
+       {
+           "fail" : "-",
+           "threshold" : 20,
+           "normalized" : 100,
+           "value" : 100,
+           "flags" : "-O--CK",
+           "worst" : 100,
+           "name" : "Start_Stop_Count",
+           "raw" : "265",
+           "id" : "  4"
+       },
+       {
+           "flags" : "PO--CK",
+           "normalized" : 100,
+           "value" : 100,
+           "id" : "  5",
+           "raw" : "0",
+           "name" : "Reallocated_Sector_Ct",
+           "worst" : 100,
+           "threshold" : 10,
+           "fail" : "-"
+       },
+       {
+           "id" : "  7",
+           "raw" : "1572375006",
+           "name" : "Seek_Error_Rate",
+           "worst" : 60,
+           "flags" : "POSR--",
+           "normalized" : 91,
+           "value" : 91,
+           "threshold" : 30,
+           "fail" : "-"
+       },
+       {
+           "raw" : "9885",
+           "name" : "Power_On_Hours",
+           "worst" : 89,
+           "id" : "  9",
+           "flags" : "-O--CK",
+           "normalized" : 89,
+           "value" : 89,
+           "fail" : "-",
+           "threshold" : 0
+       },
+       {
+           "fail" : "-",
+           "threshold" : 97,
+           "normalized" : 100,
+           "value" : 100,
+           "flags" : "PO--C-",
+           "worst" : 100,
+           "name" : "Spin_Retry_Count",
+           "raw" : "0",
+           "id" : " 10"
+       },
+       {
+           "threshold" : 20,
+           "fail" : "-",
+           "id" : " 12",
+           "raw" : "265",
+           "name" : "Power_Cycle_Count",
+           "worst" : 100,
+           "flags" : "-O--CK",
+           "normalized" : 100,
+           "value" : 100
+       },
+       {
+           "name" : "End-to-End_Error",
+           "worst" : 100,
+           "raw" : "0",
+           "id" : "184",
+           "normalized" : 100,
+           "value" : 100,
+           "flags" : "-O--CK",
+           "fail" : "-",
+           "threshold" : 99
+       },
+       {
+           "threshold" : 0,
+           "fail" : "-",
+           "id" : "187",
+           "raw" : "0",
+           "name" : "Reported_Uncorrect",
+           "worst" : 100,
+           "flags" : "-O--CK",
+           "normalized" : 100,
+           "value" : 100
+       },
+       {
+           "threshold" : 0,
+           "fail" : "-",
+           "id" : "188",
+           "raw" : "0",
+           "name" : "Command_Timeout",
+           "worst" : 100,
+           "flags" : "-O--CK",
+           "normalized" : 100,
+           "value" : 100
+       },
+       {
+           "fail" : "-",
+           "threshold" : 0,
+           "raw" : "0",
+           "worst" : 100,
+           "name" : "High_Fly_Writes",
+           "id" : "189",
+           "flags" : "-O--CK",
+           "normalized" : 100,
+           "value" : 100
+       },
+       {
+           "flags" : "-O-RCK",
+           "normalized" : 45,
+           "value" : 45,
+           "raw" : "55 (147 229 55 24 0)",
+           "worst" : 36,
+           "name" : "Airflow_Temperature_Cel",
+           "id" : "190",
+           "fail" : "NOW",
+           "threshold" : 45
+       },
+       {
+           "fail" : "-",
+           "threshold" : 0,
+           "raw" : "0",
+           "worst" : 100,
+           "name" : "G-Sense_Error_Rate",
+           "id" : "191",
+           "flags" : "-O---K",
+           "normalized" : 100,
+           "value" : 100
+       },
+       {
+           "threshold" : 0,
+           "fail" : "-",
+           "id" : "192",
+           "raw" : "57",
+           "worst" : 100,
+           "name" : "Power-Off_Retract_Count",
+           "flags" : "-O--CK",
+           "normalized" : 100,
+           "value" : 100
+       },
+       {
+           "fail" : "-",
+           "threshold" : 0,
+           "normalized" : 100,
+           "value" : 100,
+           "flags" : "-O--CK",
+           "name" : "Load_Cycle_Count",
+           "worst" : 100,
+           "raw" : "265",
+           "id" : "193"
+       },
+       {
+           "threshold" : 0,
+           "fail" : "-",
+           "flags" : "-O--CK",
+           "normalized" : 55,
+           "value" : 55,
+           "id" : "194",
+           "raw" : "55 (0 16 0 0 0)",
+           "name" : "Temperature_Celsius",
+           "worst" : 64
+       },
+       {
+           "threshold" : 0,
+           "fail" : "-",
+           "id" : "195",
+           "name" : "Hardware_ECC_Recovered",
+           "worst" : 13,
+           "raw" : "215697022",
+           "normalized" : 23,
+           "value" : 23,
+           "flags" : "-O---K"
+       },
+       {
+           "worst" : 100,
+           "name" : "Current_Pending_Sector",
+           "raw" : "0",
+           "id" : "197",
+           "normalized" : 100,
+           "value" : 100,
+           "flags" : "-O--C-",
+           "fail" : "-",
+           "threshold" : 0
+       },
+       {
+           "worst" : 100,
+           "name" : "Offline_Uncorrectable",
+           "raw" : "0",
+           "id" : "198",
+           "normalized" : 100,
+           "value" : 100,
+           "flags" : "----C-",
+           "fail" : "-",
+           "threshold" : 0
+       },
+       {
+           "fail" : "-",
+           "threshold" : 0,
+           "normalized" : 200,
+           "value" : 200,
+           "flags" : "-OSRCK",
+           "worst" : 200,
+           "name" : "UDMA_CRC_Error_Count",
+           "raw" : "0",
+           "id" : "199"
+       }
+    ],
+    "type" : "ata",
+    "health" : "PASSED"
+}
diff --git a/src/test/disk_tests/hdd_smart/sdb_udevadm b/src/test/disk_tests/hdd_smart/sdb_udevadm
new file mode 100644 (file)
index 0000000..3bc3a57
--- /dev/null
@@ -0,0 +1,11 @@
+E: DEVNAME=/dev/sdb
+E: DEVTYPE=disk
+E: ID_ATA_ROTATION_RATE_RPM=7200
+E: ID_BUS=ata
+E: ID_MODEL=ST4000NM0033-9ZM170
+E: ID_PART_TABLE_TYPE=gpt
+E: ID_PART_TABLE_UUID=4f2e07a6-5437-2b4e-b6e8-9cba98639324
+E: ID_SERIAL_SHORT=00000000
+E: ID_TYPE=disk
+E: ID_WWN=0x0000000000000000
+E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/src/test/disk_tests/nvme_smart/disklist b/src/test/disk_tests/nvme_smart/disklist
new file mode 100644 (file)
index 0000000..d00b90e
--- /dev/null
@@ -0,0 +1 @@
+nvme0n1
diff --git a/src/test/disk_tests/nvme_smart/disklist_expected.json b/src/test/disk_tests/nvme_smart/disklist_expected.json
new file mode 100644 (file)
index 0000000..4d1c92f
--- /dev/null
@@ -0,0 +1,16 @@
+{
+    "nvme0n1" : {
+       "wearout" : 69,
+       "vendor" : "unknown",
+       "size" : 512000,
+       "health" : "PASSED",
+       "serial" : "unknown",
+       "model" : "NVME MODEL 1",
+       "rpm" : 0,
+       "osdid" : -1,
+       "devpath" : "/dev/nvme0n1",
+       "gpt" : 0,
+       "wwn" : "unknown",
+       "type" : "nvme"
+    }
+}
diff --git a/src/test/disk_tests/nvme_smart/nvme0n1/device/model b/src/test/disk_tests/nvme_smart/nvme0n1/device/model
new file mode 100644 (file)
index 0000000..9bd6eba
--- /dev/null
@@ -0,0 +1 @@
+NVME MODEL 1
diff --git a/src/test/disk_tests/nvme_smart/nvme0n1/queue/rotational b/src/test/disk_tests/nvme_smart/nvme0n1/queue/rotational
new file mode 100644 (file)
index 0000000..573541a
--- /dev/null
@@ -0,0 +1 @@
+0
diff --git a/src/test/disk_tests/nvme_smart/nvme0n1/size b/src/test/disk_tests/nvme_smart/nvme0n1/size
new file mode 100644 (file)
index 0000000..83b33d2
--- /dev/null
@@ -0,0 +1 @@
+1000
diff --git a/src/test/disk_tests/nvme_smart/nvme0n1_smart b/src/test/disk_tests/nvme_smart/nvme0n1_smart
new file mode 100644 (file)
index 0000000..f371d5b
--- /dev/null
@@ -0,0 +1,22 @@
+smartctl 6.6 2016-05-31 r4324 [x86_64-linux-4.4.19-1-pve] (local build)
+Copyright (C) 2002-16, Bruce Allen, Christian Franke, www.smartmontools.org
+
+=== START OF SMART DATA SECTION ===
+SMART overall-health self-assessment test result: PASSED
+
+SMART/Health Information (NVMe Log 0x02, NSID 0xffffffff)
+Critical Warning:                   0x00
+Temperature:                        32 Celsius
+Available Spare:                    100%
+Available Spare Threshold:          10%
+Percentage Used:                    31%
+Data Units Read:                    1,299,288 [665 GB]
+Data Units Written:                 5,592,478 [2.86 TB]
+Host Read Commands:                 30,360,807
+Host Write Commands:                470,356,196
+Controller Busy Time:               12
+Power Cycles:                       98
+Power On Hours:                     687
+Unsafe Shutdowns:                   21
+Media and Data Integrity Errors:    0
+Error Information Log Entries:      0
diff --git a/src/test/disk_tests/nvme_smart/nvme0n1_smart_expected.json b/src/test/disk_tests/nvme_smart/nvme0n1_smart_expected.json
new file mode 100644 (file)
index 0000000..af8eade
--- /dev/null
@@ -0,0 +1,6 @@
+{
+    "text" : "\nSMART/Health Information (NVMe Log 0x02, NSID 0xffffffff)\nCritical Warning:                   0x00\nTemperature:                        32 Celsius\nAvailable Spare:                    100%\nAvailable Spare Threshold:          10%\nPercentage Used:                    31%\nData Units Read:                    1,299,288 [665 GB]\nData Units Written:                 5,592,478 [2.86 TB]\nHost Read Commands:                 30,360,807\nHost Write Commands:                470,356,196\nController Busy Time:               12\nPower Cycles:                       98\nPower On Hours:                     687\nUnsafe Shutdowns:                   21\nMedia and Data Integrity Errors:    0\nError Information Log Entries:      0\n",
+    "health" : "PASSED",
+    "type" : "text",
+    "wearout": 69
+}
diff --git a/src/test/disk_tests/nvme_smart/nvme0n1_udevadm b/src/test/disk_tests/nvme_smart/nvme0n1_udevadm
new file mode 100644 (file)
index 0000000..36c78ce
--- /dev/null
@@ -0,0 +1,18 @@
+
+P: /devices/pci0000:00/0000:00:01.1/0000:02:00.0/nvme/nvme0/nvme0n1
+N: nvme0n1
+S: disk/by-id/lvm-pv-uuid-Py4eod-qfzj-i8Q3-Dxu6-xf0Q-H3Wr-w5Fo8V
+E: DEVLINKS=/dev/disk/by-id/lvm-pv-uuid-Py4eod-qfzj-i8Q3-Dxu6-xf0Q-H3Wr-w5Fo8V
+E: DEVNAME=/dev/nvme0n1
+E: DEVPATH=/devices/pci0000:00/0000:00:01.1/0000:02:00.0/nvme/nvme0/nvme0n1
+E: DEVTYPE=disk
+E: ID_FS_TYPE=LVM2_member
+E: ID_FS_USAGE=raid
+E: ID_FS_UUID=Py4eod-qfzj-i8Q3-Dxu6-xf0Q-H3Wr-w5Fo8V
+E: ID_FS_UUID_ENC=Py4eod-qfzj-i8Q3-Dxu6-xf0Q-H3Wr-w5Fo8V
+E: ID_FS_VERSION=LVM2 001
+E: MAJOR=259
+E: MINOR=0
+E: SUBSYSTEM=block
+E: TAGS=:systemd:
+E: USEC_INITIALIZED=3842
diff --git a/src/test/disk_tests/sas/disklist b/src/test/disk_tests/sas/disklist
new file mode 100644 (file)
index 0000000..9191c61
--- /dev/null
@@ -0,0 +1 @@
+sda
diff --git a/src/test/disk_tests/sas/disklist_expected.json b/src/test/disk_tests/sas/disklist_expected.json
new file mode 100644 (file)
index 0000000..39b14a5
--- /dev/null
@@ -0,0 +1,17 @@
+{
+    "sda" : {
+       "gpt" : 1,
+       "devpath" : "/dev/sda",
+       "type" : "unknown",
+       "model" : "MODEL1",
+       "health" : "UNKNOWN",
+       "osdid" : -1,
+       "wwn" : "0x0000000000000000",
+       "vendor" : "VENDOR1",
+       "rpm" : -1,
+       "size" : 5120000,
+       "serial" : "SER2",
+       "wearout" : "N/A",
+       "by_id_link" : "/dev/disk/by-id/scsi-00000000000000000"
+    }
+}
diff --git a/src/test/disk_tests/sas/sda/device/model b/src/test/disk_tests/sas/sda/device/model
new file mode 100644 (file)
index 0000000..6b69674
--- /dev/null
@@ -0,0 +1 @@
+MODEL1
diff --git a/src/test/disk_tests/sas/sda/device/vendor b/src/test/disk_tests/sas/sda/device/vendor
new file mode 100644 (file)
index 0000000..a7894eb
--- /dev/null
@@ -0,0 +1 @@
+VENDOR1
diff --git a/src/test/disk_tests/sas/sda/queue/rotational b/src/test/disk_tests/sas/sda/queue/rotational
new file mode 100644 (file)
index 0000000..d00491f
--- /dev/null
@@ -0,0 +1 @@
+1
diff --git a/src/test/disk_tests/sas/sda/size b/src/test/disk_tests/sas/sda/size
new file mode 100644 (file)
index 0000000..5caff40
--- /dev/null
@@ -0,0 +1 @@
+10000
diff --git a/src/test/disk_tests/sas/sda_smart b/src/test/disk_tests/sas/sda_smart
new file mode 100644 (file)
index 0000000..856af39
--- /dev/null
@@ -0,0 +1,20 @@
+=== START OF READ SMART DATA SECTION ===
+SMART Health Status: OK
+
+Percentage used endurance indicator: 0%
+Current Drive Temperature:     20 C
+Drive Trip Temperature:        70 C
+
+Manufactured in week 47 of year 2012
+Specified cycle count over device lifetime:  0
+Accumulated start-stop cycles:  0
+Specified load-unload count over device lifetime:  0
+Accumulated load-unload cycles:  0
+Elements in grown defect list: 0
+
+Vendor (Seagate) cache information
+  Blocks sent to initiator = 1286675833552896
+
+Vendor (Seagate/Hitachi) factory information
+  number of hours powered up = 7127.12
+  number of minutes until next internal SMART test = 0
diff --git a/src/test/disk_tests/sas/sda_smart_expected.json b/src/test/disk_tests/sas/sda_smart_expected.json
new file mode 100644 (file)
index 0000000..2964372
--- /dev/null
@@ -0,0 +1,6 @@
+{
+    "health" : "OK",
+    "text" : "\nPercentage used endurance indicator: 0%\nCurrent Drive Temperature:     20 C\nDrive Trip Temperature:        70 C\n\nManufactured in week 47 of year 2012\nSpecified cycle count over device lifetime:  0\nAccumulated start-stop cycles:  0\nSpecified load-unload count over device lifetime:  0\nAccumulated load-unload cycles:  0\nElements in grown defect list: 0\n\nVendor (Seagate) cache information\n  Blocks sent to initiator = 1286675833552896\n\nVendor (Seagate/Hitachi) factory information\n  number of hours powered up = 7127.12\n  number of minutes until next internal SMART test = 0\n",
+    "type" : "text",
+    "wearout": 100
+}
diff --git a/src/test/disk_tests/sas/sda_udevadm b/src/test/disk_tests/sas/sda_udevadm
new file mode 100644 (file)
index 0000000..ac0744d
--- /dev/null
@@ -0,0 +1,31 @@
+P: /devices/pci0000:00/0000:00:03.0/0000:02:00.0/host4/port-4:0/end_device-4:0/target4:0:0/4:0:0:0/block/sda
+N: sda
+S: disk/by-id/scsi-00000000000000000
+S: disk/by-id/wwn-0x0000000000000000
+S: disk/by-path/pci-0000:02:00.0-sas-0x0000000000000000-lun-0
+E: DEVLINKS=/dev/disk/by-id/scsi-00000000000000000 /dev/disk/by-id/wwn-0x0000000000000000 /dev/disk/by-path/pci-0000:02:00.0-sas-0x0000000000000000-lun-0
+E: DEVNAME=/dev/sda
+E: DEVPATH=/devices/pci0000:00/0000:00:03.0/0000:02:00.0/host4/port-4:0/end_device-4:0/target4:0:0/4:0:0:0/block/sda
+E: DEVTYPE=disk
+E: ID_BUS=scsi
+E: ID_MODEL=MODEL1
+E: ID_MODEL_ENC=MODEL1\x20\x20\x20\x20\x20\x20
+E: ID_PART_TABLE_TYPE=gpt
+E: ID_PART_TABLE_UUID=605740f0-44a1-4dc5-9fea-bde166df963e
+E: ID_PATH=pci-0000:02:00.0-sas-0x0000000000000000-lun-0
+E: ID_PATH_TAG=pci-0000_02_00_0-sas-0x0000000000000000-lun-0
+E: ID_REVISION=ES64
+E: ID_SCSI=1
+E: ID_SCSI_SERIAL=SERIAL
+E: ID_SERIAL=SERIAL2
+E: ID_SERIAL_SHORT=SER2
+E: ID_TYPE=disk
+E: ID_VENDOR=VENDOR1
+E: ID_VENDOR_ENC=VENDOR1\x20
+E: ID_WWN=0x0000000000000000
+E: ID_WWN_WITH_EXTENSION=0x0000000000000000
+E: MAJOR=8
+E: MINOR=0
+E: SUBSYSTEM=block
+E: TAGS=:systemd:
+E: USEC_INITIALIZED=667541
diff --git a/src/test/disk_tests/sas_ssd/disklist b/src/test/disk_tests/sas_ssd/disklist
new file mode 100644 (file)
index 0000000..9191c61
--- /dev/null
@@ -0,0 +1 @@
+sda
diff --git a/src/test/disk_tests/sas_ssd/disklist_expected.json b/src/test/disk_tests/sas_ssd/disklist_expected.json
new file mode 100644 (file)
index 0000000..dd9b748
--- /dev/null
@@ -0,0 +1,17 @@
+{
+    "sda" : {
+       "gpt" : 1,
+       "devpath" : "/dev/sda",
+       "type" : "ssd",
+       "model" : "MODEL1",
+       "health" : "OK",
+       "osdid" : -1,
+       "wwn" : "0x0000000000000000",
+       "vendor" : "VENDOR1",
+       "rpm" : 0,
+       "size" : 5120000,
+       "serial" : "SER2",
+       "wearout" : 100,
+       "by_id_link" : "/dev/disk/by-id/scsi-00000000000000000"
+    }
+}
diff --git a/src/test/disk_tests/sas_ssd/sda/device/model b/src/test/disk_tests/sas_ssd/sda/device/model
new file mode 100644 (file)
index 0000000..6b69674
--- /dev/null
@@ -0,0 +1 @@
+MODEL1
diff --git a/src/test/disk_tests/sas_ssd/sda/device/vendor b/src/test/disk_tests/sas_ssd/sda/device/vendor
new file mode 100644 (file)
index 0000000..a7894eb
--- /dev/null
@@ -0,0 +1 @@
+VENDOR1
diff --git a/src/test/disk_tests/sas_ssd/sda/queue/rotational b/src/test/disk_tests/sas_ssd/sda/queue/rotational
new file mode 100644 (file)
index 0000000..573541a
--- /dev/null
@@ -0,0 +1 @@
+0
diff --git a/src/test/disk_tests/sas_ssd/sda/size b/src/test/disk_tests/sas_ssd/sda/size
new file mode 100644 (file)
index 0000000..5caff40
--- /dev/null
@@ -0,0 +1 @@
+10000
diff --git a/src/test/disk_tests/sas_ssd/sda_smart b/src/test/disk_tests/sas_ssd/sda_smart
new file mode 100644 (file)
index 0000000..856af39
--- /dev/null
@@ -0,0 +1,20 @@
+=== START OF READ SMART DATA SECTION ===
+SMART Health Status: OK
+
+Percentage used endurance indicator: 0%
+Current Drive Temperature:     20 C
+Drive Trip Temperature:        70 C
+
+Manufactured in week 47 of year 2012
+Specified cycle count over device lifetime:  0
+Accumulated start-stop cycles:  0
+Specified load-unload count over device lifetime:  0
+Accumulated load-unload cycles:  0
+Elements in grown defect list: 0
+
+Vendor (Seagate) cache information
+  Blocks sent to initiator = 1286675833552896
+
+Vendor (Seagate/Hitachi) factory information
+  number of hours powered up = 7127.12
+  number of minutes until next internal SMART test = 0
diff --git a/src/test/disk_tests/sas_ssd/sda_smart_expected.json b/src/test/disk_tests/sas_ssd/sda_smart_expected.json
new file mode 100644 (file)
index 0000000..2964372
--- /dev/null
@@ -0,0 +1,6 @@
+{
+    "health" : "OK",
+    "text" : "\nPercentage used endurance indicator: 0%\nCurrent Drive Temperature:     20 C\nDrive Trip Temperature:        70 C\n\nManufactured in week 47 of year 2012\nSpecified cycle count over device lifetime:  0\nAccumulated start-stop cycles:  0\nSpecified load-unload count over device lifetime:  0\nAccumulated load-unload cycles:  0\nElements in grown defect list: 0\n\nVendor (Seagate) cache information\n  Blocks sent to initiator = 1286675833552896\n\nVendor (Seagate/Hitachi) factory information\n  number of hours powered up = 7127.12\n  number of minutes until next internal SMART test = 0\n",
+    "type" : "text",
+    "wearout": 100
+}
diff --git a/src/test/disk_tests/sas_ssd/sda_udevadm b/src/test/disk_tests/sas_ssd/sda_udevadm
new file mode 100644 (file)
index 0000000..ac0744d
--- /dev/null
@@ -0,0 +1,31 @@
+P: /devices/pci0000:00/0000:00:03.0/0000:02:00.0/host4/port-4:0/end_device-4:0/target4:0:0/4:0:0:0/block/sda
+N: sda
+S: disk/by-id/scsi-00000000000000000
+S: disk/by-id/wwn-0x0000000000000000
+S: disk/by-path/pci-0000:02:00.0-sas-0x0000000000000000-lun-0
+E: DEVLINKS=/dev/disk/by-id/scsi-00000000000000000 /dev/disk/by-id/wwn-0x0000000000000000 /dev/disk/by-path/pci-0000:02:00.0-sas-0x0000000000000000-lun-0
+E: DEVNAME=/dev/sda
+E: DEVPATH=/devices/pci0000:00/0000:00:03.0/0000:02:00.0/host4/port-4:0/end_device-4:0/target4:0:0/4:0:0:0/block/sda
+E: DEVTYPE=disk
+E: ID_BUS=scsi
+E: ID_MODEL=MODEL1
+E: ID_MODEL_ENC=MODEL1\x20\x20\x20\x20\x20\x20
+E: ID_PART_TABLE_TYPE=gpt
+E: ID_PART_TABLE_UUID=605740f0-44a1-4dc5-9fea-bde166df963e
+E: ID_PATH=pci-0000:02:00.0-sas-0x0000000000000000-lun-0
+E: ID_PATH_TAG=pci-0000_02_00_0-sas-0x0000000000000000-lun-0
+E: ID_REVISION=ES64
+E: ID_SCSI=1
+E: ID_SCSI_SERIAL=SERIAL
+E: ID_SERIAL=SERIAL2
+E: ID_SERIAL_SHORT=SER2
+E: ID_TYPE=disk
+E: ID_VENDOR=VENDOR1
+E: ID_VENDOR_ENC=VENDOR1\x20
+E: ID_WWN=0x0000000000000000
+E: ID_WWN_WITH_EXTENSION=0x0000000000000000
+E: MAJOR=8
+E: MINOR=0
+E: SUBSYSTEM=block
+E: TAGS=:systemd:
+E: USEC_INITIALIZED=667541
diff --git a/src/test/disk_tests/ssd_smart/disklist b/src/test/disk_tests/ssd_smart/disklist
new file mode 100644 (file)
index 0000000..18048d5
--- /dev/null
@@ -0,0 +1,5 @@
+sda
+sdb
+sdc
+sdd
+sde
diff --git a/src/test/disk_tests/ssd_smart/disklist_expected.json b/src/test/disk_tests/ssd_smart/disklist_expected.json
new file mode 100644 (file)
index 0000000..d84b9dc
--- /dev/null
@@ -0,0 +1,72 @@
+{
+    "sda" : {
+       "serial" : "000000000000",
+       "vendor" : "ATA",
+       "rpm" : 0,
+       "gpt" : 1,
+       "health" : "PASSED",
+       "wearout" : "100",
+       "osdid" : -1,
+       "size" : 512000,
+       "type" : "ssd",
+       "devpath" : "/dev/sda",
+       "model" : "Crucial_CT500MX200SSD1",
+       "wwn" : "0x0000000000000000"
+    },
+    "sdb" : {
+       "model" : "INTEL_SSDSC2BB080G6",
+       "devpath" : "/dev/sdb",
+       "osdid" : -1,
+       "type" : "ssd",
+       "size" : 512000,
+       "wwn" : "0x0000000000000000",
+       "gpt" : 1,
+       "rpm" : 0,
+       "vendor" : "ATA",
+       "serial" : "000000000000000000",
+       "wearout" : "97",
+       "health" : "PASSED"
+    },
+    "sdc" : {
+       "wwn" : "0x0000000000000000",
+       "devpath" : "/dev/sdc",
+       "model" : "Samsung SSD 850 PRO 512GB",
+       "osdid" : -1,
+       "type" : "ssd",
+       "size" : 512000,
+       "wearout" : "99",
+       "health" : "PASSED",
+       "gpt" : 1,
+       "rpm" : 0,
+       "vendor" : "ATA",
+       "serial" : "000000000000"
+    },
+    "sdd" : {
+       "rpm" : 0,
+       "gpt" : 1,
+       "serial" : "000000000000",
+       "vendor" : "ATA",
+       "wearout" : "100",
+       "health" : "PASSED",
+       "devpath" : "/dev/sdd",
+       "model" : "SanDisk SD8SB8U1T001122",
+       "size" : 512000,
+       "osdid" : -1,
+       "type" : "ssd",
+       "wwn" : "0x0000000000000000"
+    },
+    "sde" : {
+       "type" : "ssd",
+       "osdid" : -1,
+       "size" : 512000,
+       "model" : "KINGSTON SHFS37A120G",
+       "devpath" : "/dev/sde",
+       "wwn" : "0x0000000000000000",
+       "vendor" : "ATA",
+       "serial" : "000000000000",
+       "gpt" : 1,
+       "rpm" : 0,
+       "health" : "PASSED",
+       "wearout" : "91"
+    }
+}
diff --git a/src/test/disk_tests/ssd_smart/sda/device/vendor b/src/test/disk_tests/ssd_smart/sda/device/vendor
new file mode 100644 (file)
index 0000000..531030d
--- /dev/null
@@ -0,0 +1 @@
+ATA
diff --git a/src/test/disk_tests/ssd_smart/sda/queue/rotational b/src/test/disk_tests/ssd_smart/sda/queue/rotational
new file mode 100644 (file)
index 0000000..573541a
--- /dev/null
@@ -0,0 +1 @@
+0
diff --git a/src/test/disk_tests/ssd_smart/sda/size b/src/test/disk_tests/ssd_smart/sda/size
new file mode 100644 (file)
index 0000000..83b33d2
--- /dev/null
@@ -0,0 +1 @@
+1000
diff --git a/src/test/disk_tests/ssd_smart/sda_smart b/src/test/disk_tests/ssd_smart/sda_smart
new file mode 100644 (file)
index 0000000..2857588
--- /dev/null
@@ -0,0 +1,39 @@
+smartctl 6.6 2016-05-31 r4324 [x86_64-linux-4.4.21-1-pve] (local build)
+Copyright (C) 2002-16, Bruce Allen, Christian Franke, www.smartmontools.org
+
+=== START OF READ SMART DATA SECTION ===
+SMART overall-health self-assessment test result: PASSED
+
+SMART Attributes Data Structure revision number: 16
+Vendor Specific SMART Attributes with Thresholds:
+ID# ATTRIBUTE_NAME          FLAGS    VALUE WORST THRESH FAIL RAW_VALUE
+  1 Raw_Read_Error_Rate     POSR-K   100   100   000    -    0
+  5 Reallocate_NAND_Blk_Cnt -O--CK   100   100   010    -    0
+  9 Power_On_Hours          -O--CK   100   100   000    -    1309
+ 12 Power_Cycle_Count       -O--CK   100   100   000    -    200
+171 Program_Fail_Count      -O--CK   100   100   000    -    0
+172 Erase_Fail_Count        -O--CK   100   100   000    -    0
+173 Ave_Block-Erase_Count   -O--CK   100   100   000    -    12
+174 Unexpect_Power_Loss_Ct  -O--CK   100   100   000    -    53
+180 Unused_Reserve_NAND_Blk PO--CK   000   000   000    -    5565
+183 SATA_Interfac_Downshift -O--CK   100   100   000    -    0
+184 Error_Correction_Count  -O--CK   100   100   000    -    0
+187 Reported_Uncorrect      -O--CK   100   100   000    -    0
+194 Temperature_Celsius     -O---K   068   054   000    -    32 (Min/Max 22/46)
+196 Reallocated_Event_Count -O--CK   100   100   000    -    0
+197 Current_Pending_Sector  -O--CK   100   100   000    -    0
+198 Offline_Uncorrectable   ----CK   100   100   000    -    0
+199 UDMA_CRC_Error_Count    -O--CK   100   100   000    -    0
+202 Percent_Lifetime_Used   ----CK   100   100   001    -    0
+206 Write_Error_Rate        -OSR--   100   100   000    -    0
+210 Success_RAIN_Recov_Cnt  -O--CK   100   100   000    -    0
+246 Total_Host_Sector_Write -O--CK   100   100   000    -    6751830403
+247 Host_Program_Page_Count -O--CK   100   100   000    -    211228065
+248 Bckgnd_Program_Page_Cnt -O--CK   100   100   000    -    253276904
+                            ||||||_ K auto-keep
+                            |||||__ C event count
+                            ||||___ R error rate
+                            |||____ S speed/performance
+                            ||_____ O updated online
+                            |______ P prefailure warning
+
diff --git a/src/test/disk_tests/ssd_smart/sda_smart_expected.json b/src/test/disk_tests/ssd_smart/sda_smart_expected.json
new file mode 100644 (file)
index 0000000..2b42cf8
--- /dev/null
@@ -0,0 +1,259 @@
+{
+    "type" : "ata",
+    "health" : "PASSED",
+    "attributes" : [
+       {
+           "worst" : 100,
+           "threshold" : 0,
+           "name" : "Raw_Read_Error_Rate",
+           "id" : "  1",
+           "flags" : "POSR-K",
+           "raw" : "0",
+           "fail" : "-",
+           "normalized" : 100,
+           "value" : 100
+       },
+       {
+           "worst" : 100,
+           "threshold" : 10,
+           "name" : "Reallocate_NAND_Blk_Cnt",
+           "id" : "  5",
+           "flags" : "-O--CK",
+           "raw" : "0",
+           "fail" : "-",
+           "normalized" : 100,
+           "value" : 100
+       },
+       {
+           "name" : "Power_On_Hours",
+           "threshold" : 0,
+           "worst" : 100,
+           "normalized" : 100,
+           "value" : 100,
+           "fail" : "-",
+           "raw" : "1309",
+           "flags" : "-O--CK",
+           "id" : "  9"
+       },
+       {
+           "threshold" : 0,
+           "name" : "Power_Cycle_Count",
+           "worst" : 100,
+           "raw" : "200",
+           "flags" : "-O--CK",
+           "fail" : "-",
+           "id" : " 12",
+           "normalized" : 100,
+           "value" : 100
+       },
+       {
+           "normalized" : 100,
+           "value" : 100,
+           "raw" : "0",
+           "flags" : "-O--CK",
+           "fail" : "-",
+           "id" : "171",
+           "threshold" : 0,
+           "name" : "Program_Fail_Count",
+           "worst" : 100
+       },
+       {
+           "normalized" : 100,
+           "value" : 100,
+           "id" : "172",
+           "raw" : "0",
+           "flags" : "-O--CK",
+           "fail" : "-",
+           "worst" : 100,
+           "threshold" : 0,
+           "name" : "Erase_Fail_Count"
+       },
+       {
+           "threshold" : 0,
+           "name" : "Ave_Block-Erase_Count",
+           "worst" : 100,
+           "raw" : "12",
+           "flags" : "-O--CK",
+           "fail" : "-",
+           "id" : "173",
+           "normalized" : 100,
+           "value" : 100
+       },
+       {
+           "name" : "Unexpect_Power_Loss_Ct",
+           "threshold" : 0,
+           "worst" : 100,
+           "normalized" : 100,
+           "value" : 100,
+           "fail" : "-",
+           "raw" : "53",
+           "flags" : "-O--CK",
+           "id" : "174"
+       },
+       {
+           "raw" : "5565",
+           "flags" : "PO--CK",
+           "fail" : "-",
+           "id" : "180",
+           "normalized" : 0,
+           "value" : 0,
+           "threshold" : 0,
+           "name" : "Unused_Reserve_NAND_Blk",
+           "worst" : 0
+       },
+       {
+           "name" : "SATA_Interfac_Downshift",
+           "threshold" : 0,
+           "worst" : 100,
+           "fail" : "-",
+           "raw" : "0",
+           "flags" : "-O--CK",
+           "id" : "183",
+           "normalized" : 100,
+           "value" : 100
+       },
+       {
+           "worst" : 100,
+           "threshold" : 0,
+           "name" : "Error_Correction_Count",
+           "id" : "184",
+           "flags" : "-O--CK",
+           "raw" : "0",
+           "fail" : "-",
+           "normalized" : 100,
+           "value" : 100
+       },
+       {
+           "name" : "Reported_Uncorrect",
+           "threshold" : 0,
+           "worst" : 100,
+           "normalized" : 100,
+           "value" : 100,
+           "fail" : "-",
+           "flags" : "-O--CK",
+           "raw" : "0",
+           "id" : "187"
+       },
+       {
+           "worst" : 54,
+           "name" : "Temperature_Celsius",
+           "threshold" : 0,
+           "id" : "194",
+           "fail" : "-",
+           "flags" : "-O---K",
+           "raw" : "32 (Min/Max 22/46)",
+           "normalized" : 68,
+           "value" : 68
+       },
+       {
+           "raw" : "0",
+           "flags" : "-O--CK",
+           "fail" : "-",
+           "id" : "196",
+           "normalized" : 100,
+           "value" : 100,
+           "threshold" : 0,
+           "name" : "Reallocated_Event_Count",
+           "worst" : 100
+       },
+       {
+           "id" : "197",
+           "fail" : "-",
+           "flags" : "-O--CK",
+           "raw" : "0",
+           "normalized" : 100,
+           "value" : 100,
+           "worst" : 100,
+           "name" : "Current_Pending_Sector",
+           "threshold" : 0
+       },
+       {
+           "threshold" : 0,
+           "name" : "Offline_Uncorrectable",
+           "worst" : 100,
+           "flags" : "----CK",
+           "raw" : "0",
+           "fail" : "-",
+           "id" : "198",
+           "normalized" : 100,
+           "value" : 100
+       },
+       {
+           "worst" : 100,
+           "name" : "UDMA_CRC_Error_Count",
+           "threshold" : 0,
+           "id" : "199",
+           "fail" : "-",
+           "raw" : "0",
+           "flags" : "-O--CK",
+           "normalized" : 100,
+           "value" : 100
+       },
+       {
+           "normalized" : 100,
+           "value" : 100,
+           "id" : "202",
+           "flags" : "----CK",
+           "raw" : "0",
+           "fail" : "-",
+           "worst" : 100,
+           "threshold" : 1,
+           "name" : "Percent_Lifetime_Used"
+       },
+       {
+           "name" : "Write_Error_Rate",
+           "threshold" : 0,
+           "worst" : 100,
+           "normalized" : 100,
+           "value" : 100,
+           "fail" : "-",
+           "raw" : "0",
+           "flags" : "-OSR--",
+           "id" : "206"
+       },
+       {
+           "name" : "Success_RAIN_Recov_Cnt",
+           "threshold" : 0,
+           "worst" : 100,
+           "fail" : "-",
+           "raw" : "0",
+           "flags" : "-O--CK",
+           "id" : "210",
+           "normalized" : 100,
+           "value" : 100
+       },
+       {
+           "normalized" : 100,
+           "value" : 100,
+           "id" : "246",
+           "fail" : "-",
+           "raw" : "6751830403",
+           "flags" : "-O--CK",
+           "worst" : 100,
+           "name" : "Total_Host_Sector_Write",
+           "threshold" : 0
+       },
+       {
+           "name" : "Host_Program_Page_Count",
+           "threshold" : 0,
+           "worst" : 100,
+           "fail" : "-",
+           "raw" : "211228065",
+           "flags" : "-O--CK",
+           "id" : "247",
+           "normalized" : 100,
+           "value" : 100
+       },
+       {
+           "normalized" : 100,
+           "value" : 100,
+           "fail" : "-",
+           "raw" : "253276904",
+           "flags" : "-O--CK",
+           "id" : "248",
+           "name" : "Bckgnd_Program_Page_Cnt",
+           "threshold" : 0,
+           "worst" : 100
+       }
+    ]
+}
diff --git a/src/test/disk_tests/ssd_smart/sda_udevadm b/src/test/disk_tests/ssd_smart/sda_udevadm
new file mode 100644 (file)
index 0000000..3290a13
--- /dev/null
@@ -0,0 +1,11 @@
+E: DEVNAME=/dev/sda
+E: DEVTYPE=disk
+E: ID_ATA_ROTATION_RATE_RPM=0
+E: ID_BUS=ata
+E: ID_MODEL=Crucial_CT500MX200SSD1
+E: ID_PART_TABLE_TYPE=gpt
+E: ID_SERIAL=Crucial_CT500MX200SSD1_000000000000
+E: ID_SERIAL_SHORT=000000000000
+E: ID_TYPE=disk
+E: ID_WWN=0x0000000000000000
+
diff --git a/src/test/disk_tests/ssd_smart/sdb/device/vendor b/src/test/disk_tests/ssd_smart/sdb/device/vendor
new file mode 100644 (file)
index 0000000..531030d
--- /dev/null
@@ -0,0 +1 @@
+ATA
diff --git a/src/test/disk_tests/ssd_smart/sdb/queue/rotational b/src/test/disk_tests/ssd_smart/sdb/queue/rotational
new file mode 100644 (file)
index 0000000..573541a
--- /dev/null
@@ -0,0 +1 @@
+0
diff --git a/src/test/disk_tests/ssd_smart/sdb/size b/src/test/disk_tests/ssd_smart/sdb/size
new file mode 100644 (file)
index 0000000..83b33d2
--- /dev/null
@@ -0,0 +1 @@
+1000
diff --git a/src/test/disk_tests/ssd_smart/sdb_smart b/src/test/disk_tests/ssd_smart/sdb_smart
new file mode 100644 (file)
index 0000000..79eea84
--- /dev/null
@@ -0,0 +1,41 @@
+smartctl 6.6 2016-05-31 r4324 [x86_64-linux-4.4.21-1-pve] (local build)
+Copyright (C) 2002-16, Bruce Allen, Christian Franke, www.smartmontools.org
+
+=== START OF READ SMART DATA SECTION ===
+SMART overall-health self-assessment test result: PASSED
+
+SMART Attributes Data Structure revision number: 1
+Vendor Specific SMART Attributes with Thresholds:
+ID# ATTRIBUTE_NAME          FLAGS    VALUE WORST THRESH FAIL RAW_VALUE
+  5 Reallocated_Sector_Ct   -O--CK   100   100   000    -    0
+  9 Power_On_Hours          -O--CK   100   100   000    -    1259
+ 12 Power_Cycle_Count       -O--CK   100   100   000    -    191
+170 Available_Reservd_Space PO--CK   100   100   010    -    0
+171 Program_Fail_Count      -O--CK   100   100   000    -    0
+172 Erase_Fail_Count        -O--CK   100   100   000    -    0
+174 Unsafe_Shutdown_Count   -O--CK   100   100   000    -    164
+175 Power_Loss_Cap_Test     PO--CK   100   100   010    -    5670 (1 343)
+183 SATA_Downshift_Count    -O--CK   100   100   000    -    0
+184 End-to-End_Error        PO--CK   100   100   090    -    0
+187 Reported_Uncorrect      -O--CK   100   100   000    -    0
+190 Temperature_Case        -O---K   072   071   000    -    28 (Min/Max 21/30)
+192 Unsafe_Shutdown_Count   -O--CK   100   100   000    -    164
+194 Temperature_Internal    -O---K   100   100   000    -    28
+197 Current_Pending_Sector  -O--C-   100   100   000    -    0
+199 CRC_Error_Count         -OSRCK   100   100   000    -    0
+225 Host_Writes_32MiB       -O--CK   100   100   000    -    296600
+226 Workld_Media_Wear_Indic -O--CK   100   100   000    -    3747
+227 Workld_Host_Reads_Perc  -O--CK   100   100   000    -    0
+228 Workload_Minutes        -O--CK   100   100   000    -    75111
+232 Available_Reservd_Space PO--CK   100   100   010    -    0
+233 Media_Wearout_Indicator -O--CK   097   097   000    -    0
+234 Thermal_Throttle        -O--CK   100   100   000    -    0/0
+241 Host_Writes_32MiB       -O--CK   100   100   000    -    296600
+242 Host_Reads_32MiB        -O--CK   100   100   000    -    1207
+                            ||||||_ K auto-keep
+                            |||||__ C event count
+                            ||||___ R error rate
+                            |||____ S speed/performance
+                            ||_____ O updated online
+                            |______ P prefailure warning
+
diff --git a/src/test/disk_tests/ssd_smart/sdb_smart_expected.json b/src/test/disk_tests/ssd_smart/sdb_smart_expected.json
new file mode 100644 (file)
index 0000000..0614754
--- /dev/null
@@ -0,0 +1,281 @@
+{
+    "health" : "PASSED",
+    "type" : "ata",
+    "attributes" : [
+       {
+           "name" : "Reallocated_Sector_Ct",
+           "threshold" : 0,
+           "worst" : 100,
+           "fail" : "-",
+           "flags" : "-O--CK",
+           "raw" : "0",
+           "id" : "  5",
+           "normalized" : 100,
+           "value" : 100
+       },
+       {
+           "worst" : 100,
+           "name" : "Power_On_Hours",
+           "threshold" : 0,
+           "id" : "  9",
+           "fail" : "-",
+           "raw" : "1259",
+           "flags" : "-O--CK",
+           "normalized" : 100,
+           "value" : 100
+       },
+       {
+           "normalized" : 100,
+           "value" : 100,
+           "id" : " 12",
+           "raw" : "191",
+           "flags" : "-O--CK",
+           "fail" : "-",
+           "worst" : 100,
+           "threshold" : 0,
+           "name" : "Power_Cycle_Count"
+       },
+       {
+           "id" : "170",
+           "raw" : "0",
+           "flags" : "PO--CK",
+           "fail" : "-",
+           "normalized" : 100,
+           "value" : 100,
+           "worst" : 100,
+           "threshold" : 10,
+           "name" : "Available_Reservd_Space"
+       },
+       {
+           "threshold" : 0,
+           "name" : "Program_Fail_Count",
+           "worst" : 100,
+           "normalized" : 100,
+           "value" : 100,
+           "raw" : "0",
+           "flags" : "-O--CK",
+           "fail" : "-",
+           "id" : "171"
+       },
+       {
+           "worst" : 100,
+           "name" : "Erase_Fail_Count",
+           "threshold" : 0,
+           "id" : "172",
+           "fail" : "-",
+           "flags" : "-O--CK",
+           "raw" : "0",
+           "normalized" : 100,
+           "value" : 100
+       },
+       {
+           "worst" : 100,
+           "threshold" : 0,
+           "name" : "Unsafe_Shutdown_Count",
+           "normalized" : 100,
+           "value" : 100,
+           "id" : "174",
+           "raw" : "164",
+           "flags" : "-O--CK",
+           "fail" : "-"
+       },
+       {
+           "normalized" : 100,
+           "value" : 100,
+           "fail" : "-",
+           "flags" : "PO--CK",
+           "raw" : "5670 (1 343)",
+           "id" : "175",
+           "name" : "Power_Loss_Cap_Test",
+           "threshold" : 10,
+           "worst" : 100
+       },
+       {
+           "worst" : 100,
+           "threshold" : 0,
+           "name" : "SATA_Downshift_Count",
+           "normalized" : 100,
+           "value" : 100,
+           "id" : "183",
+           "raw" : "0",
+           "flags" : "-O--CK",
+           "fail" : "-"
+       },
+       {
+           "worst" : 100,
+           "name" : "End-to-End_Error",
+           "threshold" : 90,
+           "normalized" : 100,
+           "value" : 100,
+           "id" : "184",
+           "fail" : "-",
+           "raw" : "0",
+           "flags" : "PO--CK"
+       },
+       {
+           "worst" : 100,
+           "name" : "Reported_Uncorrect",
+           "threshold" : 0,
+           "normalized" : 100,
+           "value" : 100,
+           "id" : "187",
+           "fail" : "-",
+           "raw" : "0",
+           "flags" : "-O--CK"
+       },
+       {
+           "worst" : 71,
+           "name" : "Temperature_Case",
+           "threshold" : 0,
+           "id" : "190",
+           "fail" : "-",
+           "raw" : "28 (Min/Max 21/30)",
+           "flags" : "-O---K",
+           "normalized" : 72,
+           "value" : 72
+       },
+       {
+           "worst" : 100,
+           "threshold" : 0,
+           "name" : "Unsafe_Shutdown_Count",
+           "normalized" : 100,
+           "value" : 100,
+           "id" : "192",
+           "raw" : "164",
+           "flags" : "-O--CK",
+           "fail" : "-"
+       },
+       {
+           "id" : "194",
+           "fail" : "-",
+           "raw" : "28",
+           "flags" : "-O---K",
+           "normalized" : 100,
+           "value" : 100,
+           "worst" : 100,
+           "name" : "Temperature_Internal",
+           "threshold" : 0
+       },
+       {
+           "worst" : 100,
+           "threshold" : 0,
+           "name" : "Current_Pending_Sector",
+           "normalized" : 100,
+           "value" : 100,
+           "id" : "197",
+           "raw" : "0",
+           "flags" : "-O--C-",
+           "fail" : "-"
+       },
+       {
+           "worst" : 100,
+           "threshold" : 0,
+           "name" : "CRC_Error_Count",
+           "normalized" : 100,
+           "value" : 100,
+           "id" : "199",
+           "flags" : "-OSRCK",
+           "raw" : "0",
+           "fail" : "-"
+       },
+       {
+           "worst" : 100,
+           "name" : "Host_Writes_32MiB",
+           "threshold" : 0,
+           "id" : "225",
+           "fail" : "-",
+           "flags" : "-O--CK",
+           "raw" : "296600",
+           "normalized" : 100,
+           "value" : 100
+       },
+       {
+           "normalized" : 100,
+           "value" : 100,
+           "id" : "226",
+           "raw" : "3747",
+           "flags" : "-O--CK",
+           "fail" : "-",
+           "worst" : 100,
+           "threshold" : 0,
+           "name" : "Workld_Media_Wear_Indic"
+       },
+       {
+           "worst" : 100,
+           "threshold" : 0,
+           "name" : "Workld_Host_Reads_Perc",
+           "id" : "227",
+           "raw" : "0",
+           "flags" : "-O--CK",
+           "fail" : "-",
+           "normalized" : 100,
+           "value" : 100
+       },
+       {
+           "threshold" : 0,
+           "name" : "Workload_Minutes",
+           "worst" : 100,
+           "raw" : "75111",
+           "flags" : "-O--CK",
+           "fail" : "-",
+           "id" : "228",
+           "normalized" : 100,
+           "value" : 100
+       },
+       {
+           "fail" : "-",
+           "raw" : "0",
+           "flags" : "PO--CK",
+           "id" : "232",
+           "normalized" : 100,
+           "value" : 100,
+           "name" : "Available_Reservd_Space",
+           "threshold" : 10,
+           "worst" : 100
+       },
+       {
+           "normalized" : 97,
+           "value" : 97,
+           "fail" : "-",
+           "flags" : "-O--CK",
+           "raw" : "0",
+           "id" : "233",
+           "name" : "Media_Wearout_Indicator",
+           "threshold" : 0,
+           "worst" : 97
+       },
+       {
+           "normalized" : 100,
+           "value" : 100,
+           "fail" : "-",
+           "raw" : "0/0",
+           "flags" : "-O--CK",
+           "id" : "234",
+           "name" : "Thermal_Throttle",
+           "threshold" : 0,
+           "worst" : 100
+       },
+       {
+           "worst" : 100,
+           "name" : "Host_Writes_32MiB",
+           "threshold" : 0,
+           "id" : "241",
+           "fail" : "-",
+           "raw" : "296600",
+           "flags" : "-O--CK",
+           "normalized" : 100,
+           "value" : 100
+       },
+       {
+           "normalized" : 100,
+           "value" : 100,
+           "id" : "242",
+           "fail" : "-",
+           "raw" : "1207",
+           "flags" : "-O--CK",
+           "worst" : 100,
+           "name" : "Host_Reads_32MiB",
+           "threshold" : 0
+       }
+    ]
+}
diff --git a/src/test/disk_tests/ssd_smart/sdb_udevadm b/src/test/disk_tests/ssd_smart/sdb_udevadm
new file mode 100644 (file)
index 0000000..bb723c8
--- /dev/null
@@ -0,0 +1,12 @@
+E: DEVNAME=/dev/sdb
+E: DEVTYPE=disk
+E: ID_ATA_ROTATION_RATE_RPM=0
+E: ID_BUS=ata
+E: ID_MODEL=INTEL_SSDSC2BB080G6
+E: ID_PART_TABLE_TYPE=gpt
+E: ID_PART_TABLE_UUID=21c18951-8568-488c-a2a8-24441eb4b165
+E: ID_SERIAL=INTEL_SSDSC2BB080G6_000000000000000000
+E: ID_SERIAL_SHORT=000000000000000000
+E: ID_TYPE=disk
+E: ID_WWN=0x0000000000000000
+E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/src/test/disk_tests/ssd_smart/sdc/device/vendor b/src/test/disk_tests/ssd_smart/sdc/device/vendor
new file mode 100644 (file)
index 0000000..531030d
--- /dev/null
@@ -0,0 +1 @@
+ATA
diff --git a/src/test/disk_tests/ssd_smart/sdc/queue/rotational b/src/test/disk_tests/ssd_smart/sdc/queue/rotational
new file mode 100644 (file)
index 0000000..573541a
--- /dev/null
@@ -0,0 +1 @@
+0
diff --git a/src/test/disk_tests/ssd_smart/sdc/size b/src/test/disk_tests/ssd_smart/sdc/size
new file mode 100644 (file)
index 0000000..83b33d2
--- /dev/null
@@ -0,0 +1 @@
+1000
diff --git a/src/test/disk_tests/ssd_smart/sdc_smart b/src/test/disk_tests/ssd_smart/sdc_smart
new file mode 100644 (file)
index 0000000..927f6d2
--- /dev/null
@@ -0,0 +1,16 @@
+smartctl 6.4 2014-10-07 r4002 [x86_64-linux-4.4.8-1-pve] (local build)
+Copyright (C) 2002-14, Bruce Allen, Christian Franke, www.smartmontools.org
+
+=== START OF READ SMART DATA SECTION ===
+SMART overall-health self-assessment test result: PASSED
+
+SMART Attributes Data Structure revision number: 1
+Vendor Specific SMART Attributes with Thresholds:
+ID# ATTRIBUTE_NAME          FLAGS    VALUE WORST THRESH FAIL RAW_VALUE
+177 Wear_Leveling_Count     PO--CK   099   099   000    -    34
+                            ||||||_ K auto-keep
+                            |||||__ C event count
+                            ||||___ R error rate
+                            |||____ S speed/performance
+                            ||_____ O updated onlinie
+                            |______ P prefailure warning
diff --git a/src/test/disk_tests/ssd_smart/sdc_smart_expected.json b/src/test/disk_tests/ssd_smart/sdc_smart_expected.json
new file mode 100644 (file)
index 0000000..fad095b
--- /dev/null
@@ -0,0 +1,17 @@
+{
+    "type" : "ata",
+    "health" : "PASSED",
+    "attributes" : [
+       {
+           "normalized" : 99,
+           "value" : 99,
+           "fail" : "-",
+           "raw" : "34",
+           "flags" : "PO--CK",
+           "id" : "177",
+           "name" : "Wear_Leveling_Count",
+           "threshold" : 0,
+           "worst" : 99
+       }
+    ]
+}
diff --git a/src/test/disk_tests/ssd_smart/sdc_udevadm b/src/test/disk_tests/ssd_smart/sdc_udevadm
new file mode 100644 (file)
index 0000000..5adb3ef
--- /dev/null
@@ -0,0 +1,11 @@
+E: DEVNAME=/dev/sdc
+E: DEVTYPE=disk
+E: ID_ATA_ROTATION_RATE_RPM=0
+E: ID_BUS=ata
+E: ID_MODEL=Samsung SSD 850 PRO 512GB
+E: ID_PART_TABLE_TYPE=gpt
+E: ID_SERIAL=SAMSUNG_00000000
+E: ID_SERIAL_SHORT=000000000000
+E: ID_TYPE=disk
+E: ID_WWN=0x0000000000000000
+
diff --git a/src/test/disk_tests/ssd_smart/sdd/device/vendor b/src/test/disk_tests/ssd_smart/sdd/device/vendor
new file mode 100644 (file)
index 0000000..531030d
--- /dev/null
@@ -0,0 +1 @@
+ATA
diff --git a/src/test/disk_tests/ssd_smart/sdd/queue/rotational b/src/test/disk_tests/ssd_smart/sdd/queue/rotational
new file mode 100644 (file)
index 0000000..573541a
--- /dev/null
@@ -0,0 +1 @@
+0
diff --git a/src/test/disk_tests/ssd_smart/sdd/size b/src/test/disk_tests/ssd_smart/sdd/size
new file mode 100644 (file)
index 0000000..83b33d2
--- /dev/null
@@ -0,0 +1 @@
+1000
diff --git a/src/test/disk_tests/ssd_smart/sdd_smart b/src/test/disk_tests/ssd_smart/sdd_smart
new file mode 100644 (file)
index 0000000..2ad16af
--- /dev/null
@@ -0,0 +1,40 @@
+smartctl 6.4 2014-10-07 r4002 [x86_64-linux-4.4.19-1-pve] (local build)
+Copyright (C) 2002-14, Bruce Allen, Christian Franke, www.smartmontools.org
+
+=== START OF READ SMART DATA SECTION ===
+SMART overall-health self-assessment test result: PASSED
+
+SMART Attributes Data Structure revision number: 4
+Vendor Specific SMART Attributes with Thresholds:
+ID# ATTRIBUTE_NAME          FLAGS    VALUE WORST THRESH FAIL RAW_VALUE
+  5 Reallocated_Sector_Ct   -O--CK   100   100   ---    -    0
+  9 Power_On_Hours          -O--CK   100   100   ---    -    799
+ 12 Power_Cycle_Count       -O--CK   100   100   ---    -    92
+165 Unknown_Attribute       -O--CK   100   100   ---    -    9699447
+166 Unknown_Attribute       -O--CK   100   100   ---    -    1
+167 Unknown_Attribute       -O--CK   100   100   ---    -    46
+168 Unknown_Attribute       -O--CK   100   100   ---    -    5
+169 Unknown_Attribute       -O--CK   100   100   ---    -    1079
+170 Unknown_Attribute       -O--CK   100   100   ---    -    0
+171 Unknown_Attribute       -O--CK   100   100   ---    -    0
+172 Unknown_Attribute       -O--CK   100   100   ---    -    0
+173 Unknown_Attribute       -O--CK   100   100   ---    -    1
+174 Unknown_Attribute       -O--CK   100   100   ---    -    22
+184 End-to-End_Error        -O--CK   100   100   ---    -    0
+187 Reported_Uncorrect      -O--CK   100   100   ---    -    0
+188 Command_Timeout         -O--CK   100   100   ---    -    0
+194 Temperature_Celsius     -O---K   073   064   ---    -    27 (Min/Max 23/64)
+199 UDMA_CRC_Error_Count    -O--CK   100   100   ---    -    0
+230 Unknown_SSD_Attribute   -O--CK   100   100   ---    -    146029805602
+232 Available_Reservd_Space PO--CK   100   100   004    -    100
+233 Media_Wearout_Indicator -O--CK   100   100   ---    -    1574
+234 Unknown_Attribute       -O--CK   100   100   ---    -    2303
+241 Total_LBAs_Written      ----CK   253   253   ---    -    2111
+242 Total_LBAs_Read         ----CK   253   253   ---    -    1542
+244 Unknown_Attribute       -O--CK   000   100   ---    -    0
+                            ||||||_ K auto-keep
+                            |||||__ C event count
+                            ||||___ R error rate
+                            |||____ S speed/performance
+                            ||_____ O updated online
+                            |______ P prefailure warning
diff --git a/src/test/disk_tests/ssd_smart/sdd_smart_expected.json b/src/test/disk_tests/ssd_smart/sdd_smart_expected.json
new file mode 100644 (file)
index 0000000..8e0bebf
--- /dev/null
@@ -0,0 +1,281 @@
+{
+    "attributes" : [
+       {
+           "id" : "  5",
+           "flags" : "-O--CK",
+           "fail" : "-",
+           "worst" : 100,
+           "raw" : "0",
+           "threshold" : 0,
+           "normalized" : 100,
+           "value" : 100,
+           "name" : "Reallocated_Sector_Ct"
+       },
+       {
+           "worst" : 100,
+           "fail" : "-",
+           "flags" : "-O--CK",
+           "id" : "  9",
+           "name" : "Power_On_Hours",
+           "threshold" : 0,
+           "normalized" : 100,
+           "value" : 100,
+           "raw" : "799"
+       },
+       {
+           "name" : "Power_Cycle_Count",
+           "raw" : "92",
+           "threshold" : 0,
+           "normalized" : 100,
+           "value" : 100,
+           "fail" : "-",
+           "worst" : 100,
+           "id" : " 12",
+           "flags" : "-O--CK"
+       },
+       {
+           "worst" : 100,
+           "fail" : "-",
+           "flags" : "-O--CK",
+           "id" : "165",
+           "name" : "Unknown_Attribute",
+           "threshold" : 0,
+           "normalized" : 100,
+           "value" : 100,
+           "raw" : "9699447"
+       },
+       {
+           "normalized" : 100,
+           "value" : 100,
+           "threshold" : 0,
+           "raw" : "1",
+           "name" : "Unknown_Attribute",
+           "flags" : "-O--CK",
+           "id" : "166",
+           "worst" : 100,
+           "fail" : "-"
+       },
+       {
+           "id" : "167",
+           "flags" : "-O--CK",
+           "fail" : "-",
+           "worst" : 100,
+           "raw" : "46",
+           "normalized" : 100,
+           "value" : 100,
+           "threshold" : 0,
+           "name" : "Unknown_Attribute"
+       },
+       {
+           "name" : "Unknown_Attribute",
+           "raw" : "5",
+           "normalized" : 100,
+           "value" : 100,
+           "threshold" : 0,
+           "fail" : "-",
+           "worst" : 100,
+           "id" : "168",
+           "flags" : "-O--CK"
+       },
+       {
+           "flags" : "-O--CK",
+           "id" : "169",
+           "worst" : 100,
+           "fail" : "-",
+           "normalized" : 100,
+           "value" : 100,
+           "threshold" : 0,
+           "raw" : "1079",
+           "name" : "Unknown_Attribute"
+       },
+       {
+           "raw" : "0",
+           "threshold" : 0,
+           "normalized" : 100,
+           "value" : 100,
+           "name" : "Unknown_Attribute",
+           "id" : "170",
+           "flags" : "-O--CK",
+           "fail" : "-",
+           "worst" : 100
+       },
+       {
+           "fail" : "-",
+           "worst" : 100,
+           "id" : "171",
+           "flags" : "-O--CK",
+           "name" : "Unknown_Attribute",
+           "raw" : "0",
+           "normalized" : 100,
+           "value" : 100,
+           "threshold" : 0
+       },
+       {
+           "name" : "Unknown_Attribute",
+           "raw" : "0",
+           "normalized" : 100,
+           "value" : 100,
+           "threshold" : 0,
+           "fail" : "-",
+           "worst" : 100,
+           "id" : "172",
+           "flags" : "-O--CK"
+       },
+       {
+           "name" : "Unknown_Attribute",
+           "threshold" : 0,
+           "normalized" : 100,
+           "value" : 100,
+           "raw" : "1",
+           "worst" : 100,
+           "fail" : "-",
+           "flags" : "-O--CK",
+           "id" : "173"
+       },
+       {
+           "name" : "Unknown_Attribute",
+           "normalized" : 100,
+           "value" : 100,
+           "threshold" : 0,
+           "raw" : "22",
+           "worst" : 100,
+           "fail" : "-",
+           "flags" : "-O--CK",
+           "id" : "174"
+       },
+       {
+           "worst" : 100,
+           "fail" : "-",
+           "flags" : "-O--CK",
+           "id" : "184",
+           "name" : "End-to-End_Error",
+           "normalized" : 100,
+           "value" : 100,
+           "threshold" : 0,
+           "raw" : "0"
+       },
+       {
+           "name" : "Reported_Uncorrect",
+           "normalized" : 100,
+           "value" : 100,
+           "threshold" : 0,
+           "raw" : "0",
+           "worst" : 100,
+           "fail" : "-",
+           "flags" : "-O--CK",
+           "id" : "187"
+       },
+       {
+           "name" : "Command_Timeout",
+           "raw" : "0",
+           "normalized" : 100,
+           "value" : 100,
+           "threshold" : 0,
+           "fail" : "-",
+           "worst" : 100,
+           "id" : "188",
+           "flags" : "-O--CK"
+       },
+       {
+           "threshold" : 0,
+           "normalized" : 73,
+           "value" : 73,
+           "raw" : "27 (Min/Max 23/64)",
+           "name" : "Temperature_Celsius",
+           "flags" : "-O---K",
+           "id" : "194",
+           "worst" : 64,
+           "fail" : "-"
+       },
+       {
+           "worst" : 100,
+           "fail" : "-",
+           "flags" : "-O--CK",
+           "id" : "199",
+           "name" : "UDMA_CRC_Error_Count",
+           "normalized" : 100,
+           "value" : 100,
+           "threshold" : 0,
+           "raw" : "0"
+       },
+       {
+           "name" : "Unknown_SSD_Attribute",
+           "threshold" : 0,
+           "normalized" : 100,
+           "value" : 100,
+           "raw" : "146029805602",
+           "worst" : 100,
+           "fail" : "-",
+           "flags" : "-O--CK",
+           "id" : "230"
+       },
+       {
+           "raw" : "100",
+           "normalized" : 100,
+           "value" : 100,
+           "threshold" : 4,
+           "name" : "Available_Reservd_Space",
+           "id" : "232",
+           "flags" : "PO--CK",
+           "fail" : "-",
+           "worst" : 100
+       },
+       {
+           "threshold" : 0,
+           "normalized" : 100,
+           "value" : 100,
+           "raw" : "1574",
+           "name" : "Media_Wearout_Indicator",
+           "flags" : "-O--CK",
+           "id" : "233",
+           "worst" : 100,
+           "fail" : "-"
+       },
+       {
+           "id" : "234",
+           "flags" : "-O--CK",
+           "fail" : "-",
+           "worst" : 100,
+           "raw" : "2303",
+           "normalized" : 100,
+           "value" : 100,
+           "threshold" : 0,
+           "name" : "Unknown_Attribute"
+       },
+       {
+           "fail" : "-",
+           "worst" : 253,
+           "id" : "241",
+           "flags" : "----CK",
+           "name" : "Total_LBAs_Written",
+           "raw" : "2111",
+           "normalized" : 253,
+           "value" : 253,
+           "threshold" : 0
+       },
+       {
+           "worst" : 253,
+           "fail" : "-",
+           "flags" : "----CK",
+           "id" : "242",
+           "name" : "Total_LBAs_Read",
+           "threshold" : 0,
+           "normalized" : 253,
+           "value" : 253,
+           "raw" : "1542"
+       },
+       {
+           "name" : "Unknown_Attribute",
+           "normalized" : 0,
+           "value" : 0,
+           "threshold" : 0,
+           "raw" : "0",
+           "worst" : 100,
+           "fail" : "-",
+           "flags" : "-O--CK",
+           "id" : "244"
+       }
+    ],
+    "type" : "ata",
+    "health" : "PASSED"
+}
diff --git a/src/test/disk_tests/ssd_smart/sdd_udevadm b/src/test/disk_tests/ssd_smart/sdd_udevadm
new file mode 100644 (file)
index 0000000..d46a7d4
--- /dev/null
@@ -0,0 +1,11 @@
+E: DEVNAME=/dev/sdd
+E: DEVTYPE=disk
+E: ID_ATA_ROTATION_RATE_RPM=0
+E: ID_BUS=ata
+E: ID_MODEL=SanDisk SD8SB8U1T001122
+E: ID_PART_TABLE_TYPE=gpt
+E: ID_SERIAL=SANDISK_00000000000
+E: ID_SERIAL_SHORT=000000000000
+E: ID_TYPE=disk
+E: ID_WWN=0x0000000000000000
+
diff --git a/src/test/disk_tests/ssd_smart/sde/device/vendor b/src/test/disk_tests/ssd_smart/sde/device/vendor
new file mode 100644 (file)
index 0000000..531030d
--- /dev/null
@@ -0,0 +1 @@
+ATA
diff --git a/src/test/disk_tests/ssd_smart/sde/queue/rotational b/src/test/disk_tests/ssd_smart/sde/queue/rotational
new file mode 100644 (file)
index 0000000..573541a
--- /dev/null
@@ -0,0 +1 @@
+0
diff --git a/src/test/disk_tests/ssd_smart/sde/size b/src/test/disk_tests/ssd_smart/sde/size
new file mode 100644 (file)
index 0000000..83b33d2
--- /dev/null
@@ -0,0 +1 @@
+1000
diff --git a/src/test/disk_tests/ssd_smart/sde_smart b/src/test/disk_tests/ssd_smart/sde_smart
new file mode 100644 (file)
index 0000000..f6f01d6
--- /dev/null
@@ -0,0 +1,40 @@
+smartctl 7.1 2019-12-30 r5022 [x86_64-linux-5.4.65-1-pve] (local build)
+Copyright (C) 2002-19, Bruce Allen, Christian Franke, www.smartmontools.org
+
+=== START OF READ SMART DATA SECTION ===
+SMART overall-health self-assessment test result: PASSED
+
+SMART Attributes Data Structure revision number: 10
+Vendor Specific SMART Attributes with Thresholds:
+ID# ATTRIBUTE_NAME          FLAGS    VALUE WORST THRESH FAIL RAW_VALUE
+  1 Raw_Read_Error_Rate     -O--CK   120   120   050    -    0/0
+  5 Retired_Block_Count     PO--CK   100   100   003    -    0
+  9 Power_On_Hours_and_Msec -O--CK   091   091   000    -    7963h+07m+54.620s
+ 12 Power_Cycle_Count       -O--CK   099   099   000    -    1153
+171 Program_Fail_Count      -O-R--   100   100   000    -    0
+172 Erase_Fail_Count        -O--CK   100   100   000    -    0
+174 Unexpect_Power_Loss_Ct  ----CK   000   000   000    -    113
+177 Wear_Range_Delta        ------   000   000   000    -    1
+181 Program_Fail_Count      -O-R--   100   100   000    -    0
+182 Erase_Fail_Count        -O--CK   100   100   000    -    0
+187 Reported_Uncorrect      -O--C-   100   100   000    -    0
+189 Airflow_Temperature_Cel ------   027   049   000    -    27 (Min/Max 2/49)
+194 Temperature_Celsius     -O---K   027   049   000    -    27 (Min/Max 2/49)
+195 ECC_Uncorr_Error_Count  --SRC-   120   120   000    -    0/0
+196 Reallocated_Event_Count PO--CK   100   100   003    -    0
+201 Unc_Soft_Read_Err_Rate  --SRC-   120   120   000    -    0/0
+204 Soft_ECC_Correct_Rate   --SRC-   120   120   000    -    0/0
+230 Life_Curve_Status       PO--C-   100   100   000    -    100
+231 SSD_Life_Left           ------   091   091   011    -    4294967296
+233 SandForce_Internal      -O--CK   000   000   000    -    6317
+234 SandForce_Internal      -O--CK   000   000   000    -    4252
+241 Lifetime_Writes_GiB     -O--CK   000   000   000    -    4252
+242 Lifetime_Reads_GiB      -O--CK   000   000   000    -    34599
+244 Unknown_Attribute       ------   099   099   010    -    4063273
+                            ||||||_ K auto-keep
+                            |||||__ C event count
+                            ||||___ R error rate
+                            |||____ S speed/performance
+                            ||_____ O updated online
+                            |______ P prefailure warning
+
diff --git a/src/test/disk_tests/ssd_smart/sde_smart_expected.json b/src/test/disk_tests/ssd_smart/sde_smart_expected.json
new file mode 100644 (file)
index 0000000..0fa1214
--- /dev/null
@@ -0,0 +1,270 @@
+{
+    "health": "PASSED",
+    "type": "ata",
+    "attributes": [
+       {
+           "fail": "-",
+           "id": "  1",
+           "raw": "0/0",
+           "flags": "-O--CK",
+           "name": "Raw_Read_Error_Rate",
+           "threshold": 50,
+           "normalized": 120,
+           "value": 120,
+           "worst": 120
+       },
+       {
+           "id": "  5",
+           "fail": "-",
+           "normalized": 100,
+           "value": 100,
+           "worst": 100,
+           "threshold": 3,
+           "name": "Retired_Block_Count",
+           "flags": "PO--CK",
+           "raw": "0"
+       },
+       {
+           "fail": "-",
+           "id": "  9",
+           "raw": "7963h+07m+54.620s",
+           "flags": "-O--CK",
+           "worst": 91,
+           "normalized": 91,
+           "value": 91,
+           "name": "Power_On_Hours_and_Msec",
+           "threshold": 0
+       },
+       {
+           "id": " 12",
+           "fail": "-",
+           "threshold": 0,
+           "name": "Power_Cycle_Count",
+           "worst": 99,
+           "normalized": 99,
+           "value": 99,
+           "flags": "-O--CK",
+           "raw": "1153"
+       },
+       {
+           "flags": "-O-R--",
+           "raw": "0",
+           "worst": 100,
+           "normalized": 100,
+           "value": 100,
+           "threshold": 0,
+           "name": "Program_Fail_Count",
+           "fail": "-",
+           "id": "171"
+       },
+       {
+           "fail": "-",
+           "id": "172",
+           "flags": "-O--CK",
+           "raw": "0",
+           "name": "Erase_Fail_Count",
+           "threshold": 0,
+           "worst": 100,
+           "normalized": 100,
+           "value": 100
+       },
+       {
+           "fail": "-",
+           "id": "174",
+           "raw": "113",
+           "flags": "----CK",
+           "normalized": 0,
+           "value": 0,
+           "worst": 0,
+           "threshold": 0,
+           "name": "Unexpect_Power_Loss_Ct"
+       },
+       {
+           "id": "177",
+           "fail": "-",
+           "normalized": 0,
+           "value": 0,
+           "worst": 0,
+           "name": "Wear_Range_Delta",
+           "threshold": 0,
+           "flags": "------",
+           "raw": "1"
+       },
+       {
+           "flags": "-O-R--",
+           "raw": "0",
+           "threshold": 0,
+           "name": "Program_Fail_Count",
+           "worst": 100,
+           "normalized": 100,
+           "value": 100,
+           "fail": "-",
+           "id": "181"
+       },
+       {
+           "threshold": 0,
+           "name": "Erase_Fail_Count",
+           "normalized": 100,
+           "value": 100,
+           "worst": 100,
+           "flags": "-O--CK",
+           "raw": "0",
+           "id": "182",
+           "fail": "-"
+       },
+       {
+           "flags": "-O--C-",
+           "raw": "0",
+           "normalized": 100,
+           "value": 100,
+           "worst": 100,
+           "threshold": 0,
+           "name": "Reported_Uncorrect",
+           "fail": "-",
+           "id": "187"
+       },
+       {
+           "normalized": 27,
+           "value": 27,
+           "worst": 49,
+           "name": "Airflow_Temperature_Cel",
+           "threshold": 0,
+           "flags": "------",
+           "raw": "27 (Min/Max 2/49)",
+           "id": "189",
+           "fail": "-"
+       },
+       {
+           "threshold": 0,
+           "name": "Temperature_Celsius",
+           "worst": 49,
+           "normalized": 27,
+           "value": 27,
+           "flags": "-O---K",
+           "raw": "27 (Min/Max 2/49)",
+           "id": "194",
+           "fail": "-"
+       },
+       {
+           "id": "195",
+           "fail": "-",
+           "worst": 120,
+           "normalized": 120,
+           "value": 120,
+           "threshold": 0,
+           "name": "ECC_Uncorr_Error_Count",
+           "raw": "0/0",
+           "flags": "--SRC-"
+       },
+       {
+           "fail": "-",
+           "id": "196",
+           "raw": "0",
+           "flags": "PO--CK",
+           "threshold": 3,
+           "name": "Reallocated_Event_Count",
+           "normalized": 100,
+           "value": 100,
+           "worst": 100
+       },
+       {
+           "normalized": 120,
+           "value": 120,
+           "worst": 120,
+           "threshold": 0,
+           "name": "Unc_Soft_Read_Err_Rate",
+           "flags": "--SRC-",
+           "raw": "0/0",
+           "id": "201",
+           "fail": "-"
+       },
+       {
+           "raw": "0/0",
+           "flags": "--SRC-",
+           "normalized": 120,
+           "value": 120,
+           "worst": 120,
+           "threshold": 0,
+           "name": "Soft_ECC_Correct_Rate",
+           "fail": "-",
+           "id": "204"
+       },
+       {
+           "normalized": 100,
+           "value": 100,
+           "worst": 100,
+           "threshold": 0,
+           "name": "Life_Curve_Status",
+           "raw": "100",
+           "flags": "PO--C-",
+           "id": "230",
+           "fail": "-"
+       },
+       {
+           "id": "231",
+           "fail": "-",
+           "worst": 91,
+           "normalized": 91,
+           "value": 91,
+           "name": "SSD_Life_Left",
+           "threshold": 11,
+           "flags": "------",
+           "raw": "4294967296"
+       },
+       {
+           "raw": "6317",
+           "flags": "-O--CK",
+           "name": "SandForce_Internal",
+           "threshold": 0,
+           "normalized": 0,
+           "value": 0,
+           "worst": 0,
+           "fail": "-",
+           "id": "233"
+       },
+       {
+           "normalized": 0,
+           "value": 0,
+           "worst": 0,
+           "name": "SandForce_Internal",
+           "threshold": 0,
+           "flags": "-O--CK",
+           "raw": "4252",
+           "id": "234",
+           "fail": "-"
+       },
+       {
+           "worst": 0,
+           "normalized": 0,
+           "value": 0,
+           "name": "Lifetime_Writes_GiB",
+           "threshold": 0,
+           "flags": "-O--CK",
+           "raw": "4252",
+           "id": "241",
+           "fail": "-"
+       },
+       {
+           "flags": "-O--CK",
+           "raw": "34599",
+           "normalized": 0,
+           "value": 0,
+           "worst": 0,
+           "threshold": 0,
+           "name": "Lifetime_Reads_GiB",
+           "fail": "-",
+           "id": "242"
+       },
+       {
+           "threshold": 10,
+           "name": "Unknown_Attribute",
+           "worst": 99,
+           "normalized": 99,
+           "value": 99,
+           "flags": "------",
+           "raw": "4063273",
+           "id": "244",
+           "fail": "-"
+       }
+    ]
+}
diff --git a/src/test/disk_tests/ssd_smart/sde_udevadm b/src/test/disk_tests/ssd_smart/sde_udevadm
new file mode 100644 (file)
index 0000000..440c33a
--- /dev/null
@@ -0,0 +1,11 @@
+E: DEVNAME=/dev/sde
+E: DEVTYPE=disk
+E: ID_ATA_ROTATION_RATE_RPM=0
+E: ID_BUS=ata
+E: ID_MODEL=KINGSTON SHFS37A120G
+E: ID_PART_TABLE_TYPE=gpt
+E: ID_SERIAL=KINGSTON_00000000000
+E: ID_SERIAL_SHORT=000000000000
+E: ID_TYPE=disk
+E: ID_WWN=0x0000000000000000
+
diff --git a/src/test/disk_tests/usages/disklist b/src/test/disk_tests/usages/disklist
new file mode 100644 (file)
index 0000000..648bea5
--- /dev/null
@@ -0,0 +1,14 @@
+sda
+sdb
+sdc
+sdd
+sde
+sdf
+sdg
+sdh
+sdi
+sdj
+sdk
+sdl
+sdm
+sdn
diff --git a/src/test/disk_tests/usages/disklist_expected.json b/src/test/disk_tests/usages/disklist_expected.json
new file mode 100644 (file)
index 0000000..e9c5e5d
--- /dev/null
@@ -0,0 +1,223 @@
+{
+    "sdf" : {
+       "gpt" : 1,
+       "rpm" : 0,
+       "size" : 1536000,
+       "type" : "hdd",
+       "osdencrypted": 0,
+       "osdid" : "444",
+       "bluestore" : "0",
+       "health" : "UNKNOWN",
+       "model" : "MODEL1",
+       "used" : "mounted",
+       "wearout" : "N/A",
+       "wwn" : "0x0000000000000000",
+       "devpath" : "/dev/sdf",
+       "vendor" : "ATA",
+       "serial" : "SERIAL1"
+    },
+    "sde" : {
+       "wwn" : "0x0000000000000000",
+       "devpath" : "/dev/sde",
+       "serial" : "SERIAL1",
+       "vendor" : "ATA",
+       "health" : "UNKNOWN",
+       "rpm" : 0,
+       "size" : 1536000,
+       "gpt" : 1,
+       "osdid" : -1,
+       "type" : "hdd",
+       "model" : "MODEL1",
+       "used" : "Device Mapper",
+       "wearout" : "N/A"
+    },
+    "sdb" : {
+       "serial" : "SERIAL1",
+       "vendor" : "ATA",
+       "wwn" : "0x0000000000000000",
+       "devpath" : "/dev/sdb",
+       "model" : "MODEL1",
+       "used" : "LVM",
+       "wearout" : "N/A",
+       "health" : "UNKNOWN",
+       "gpt" : 1,
+       "size" : 1536000,
+       "rpm" : 0,
+       "type" : "hdd",
+       "osdid" : -1
+    },
+    "sda" : {
+       "model" : "MODEL1",
+       "used" : "mounted",
+       "mounted": 1,
+       "wearout" : "N/A",
+       "health" : "UNKNOWN",
+       "gpt" : 1,
+       "size" : 1536000,
+       "rpm" : 0,
+       "type" : "hdd",
+       "osdid" : -1,
+       "serial" : "SERIAL1",
+       "vendor" : "ATA",
+       "wwn" : "0x0000000000000000",
+       "devpath" : "/dev/sda"
+    },
+    "sdc" : {
+       "serial" : "SERIAL1",
+       "vendor" : "ATA",
+       "devpath" : "/dev/sdc",
+       "wwn" : "0x0000000000000000",
+       "used" : "ZFS",
+       "wearout" : "N/A",
+       "model" : "MODEL1",
+       "health" : "UNKNOWN",
+       "type" : "hdd",
+       "osdid" : -1,
+       "gpt" : 1,
+       "rpm" : 0,
+       "size" : 1536000
+    },
+    "sdd" : {
+       "model" : "MODEL1",
+       "wearout" : "N/A",
+       "health" : "UNKNOWN",
+       "size" : 1536000,
+       "rpm" : 0,
+       "gpt" : 1,
+       "type" : "hdd",
+       "osdid" : -1,
+       "serial" : "SERIAL1",
+       "used": "ZFS",
+       "vendor" : "ATA",
+       "wwn" : "0x0000000000000000",
+       "devpath" : "/dev/sdd"
+    },
+    "sdg" : {
+       "serial" : "SERIAL1",
+       "vendor" : "ATA",
+       "wwn" : "0x0000000000000000",
+       "devpath" : "/dev/sdg",
+       "model" : "MODEL1",
+       "used" : "LVM",
+       "wearout" : "N/A",
+       "health" : "UNKNOWN",
+       "gpt" : 1,
+       "size" : 1536000,
+       "rpm" : 0,
+       "type" : "hdd",
+       "bluestore": 1,
+       "osdencrypted": 0,
+       "osdid" : 1
+    },
+    "sdh" : {
+       "serial" : "SERIAL1",
+       "vendor" : "ATA",
+       "wwn" : "0x0000000000000000",
+       "devpath" : "/dev/sdh",
+       "model" : "MODEL1",
+       "used" : "LVM",
+       "wearout" : "N/A",
+       "health" : "UNKNOWN",
+       "gpt" : 1,
+       "journals": 1,
+       "size" : 1536000,
+       "rpm" : 0,
+       "type" : "hdd",
+       "osdid" : -1
+    },
+    "sdi" : {
+       "serial" : "SERIAL1",
+       "vendor" : "ATA",
+       "wwn" : "0x0000000000000000",
+       "devpath" : "/dev/sdi",
+       "model" : "MODEL1",
+       "used" : "LVM",
+       "wearout" : "N/A",
+       "health" : "UNKNOWN",
+       "gpt" : 1,
+       "size" : 1536000,
+       "rpm" : 0,
+       "type" : "hdd",
+       "db": 1,
+       "osdid" : -1
+    },
+    "sdj" : {
+       "serial" : "SERIAL1",
+       "vendor" : "ATA",
+       "wwn" : "0x0000000000000000",
+       "devpath" : "/dev/sdj",
+       "model" : "MODEL1",
+       "used" : "LVM",
+       "wearout" : "N/A",
+       "health" : "UNKNOWN",
+       "gpt" : 1,
+       "size" : 1536000,
+       "rpm" : 0,
+       "bluestore": 0,
+       "type" : "hdd",
+       "osdencrypted": 1,
+       "osdid" : 0
+    },
+    "sdk" : {
+       "serial" : "SERIAL1",
+       "vendor" : "ATA",
+       "wwn" : "0x0000000000000000",
+       "devpath" : "/dev/sdk",
+       "model" : "MODEL1",
+       "used" : "LVM",
+       "wearout" : "N/A",
+       "health" : "UNKNOWN",
+       "gpt" : 1,
+       "size" : 1536000,
+       "rpm" : 0,
+       "bluestore": 0,
+       "type" : "hdd",
+       "osdencrypted": 0,
+       "osdid" : 230
+    },
+    "sdl" : {
+       "serial" : "SERIAL1",
+       "vendor" : "ATA",
+       "wwn" : "0x0000000000000000",
+       "devpath" : "/dev/sdl",
+       "model" : "MODEL1",
+       "used" : "LVM",
+       "wearout" : "N/A",
+       "health" : "UNKNOWN",
+       "gpt" : 1,
+       "size" : 1536000,
+       "rpm" : 0,
+       "type" : "hdd",
+       "osdid" : -1
+    },
+    "sdm" : {
+       "serial" : "SERIAL1",
+       "vendor" : "ATA",
+       "wwn" : "0x0000000000000000",
+       "devpath" : "/dev/sdm",
+       "model" : "MODEL1",
+       "used" : "ZFS",
+       "wearout" : "N/A",
+       "health" : "UNKNOWN",
+       "gpt" : 1,
+       "size" : 1536000,
+       "rpm" : 0,
+       "type" : "hdd",
+       "osdid" : -1
+    },
+    "sdn" : {
+       "serial" : "SERIAL1",
+       "vendor" : "ATA",
+       "wwn" : "0x0000000000000000",
+       "devpath" : "/dev/sdn",
+       "model" : "MODEL1",
+       "used" : "xfs",
+       "wearout" : "N/A",
+       "health" : "UNKNOWN",
+       "gpt" : 0,
+       "size" : 1536000,
+       "rpm" : 0,
+       "type" : "hdd",
+       "osdid" : -1
+    }
+}
diff --git a/src/test/disk_tests/usages/lsblk b/src/test/disk_tests/usages/lsblk
new file mode 100644 (file)
index 0000000..6a725ab
--- /dev/null
@@ -0,0 +1,8 @@
+{
+   "blockdevices": [
+      {"path":"/dev/sdm", "parttype":null, "fstype":null},
+      {"path":"/dev/sdm1", "parttype":"6a898cc3-1dd2-11b2-99a6-080020736631", "fstype":"zfs_member"},
+      {"path":"/dev/sdm9", "parttype":"6a945a3b-1dd2-11b2-99a6-080020736631", "fstype":null},
+      {"path":"/dev/sdn", "parttype":null, "fstype":"xfs"}
+   ]
+}
diff --git a/src/test/disk_tests/usages/lvs b/src/test/disk_tests/usages/lvs
new file mode 100644 (file)
index 0000000..3720a80
--- /dev/null
@@ -0,0 +1,6 @@
+/dev/sdg(0);osd-block-01234;ceph.osd_id=1
+/dev/sdh(0);osd-journal-01234;ceph.osd_id=1
+/dev/sdi(0);osd-db-01234;ceph.osd_id=1,dasdf
+/dev/sdj(0);osd-data-01234;ceph.osd_id=0,asfd,ceph.encrypted=1
+/dev/sdk(0);osd-data-231231;ceph.osd_id=230,ceph.fsid=test
+/dev/sdl(0);osd-data-234132;ceph.osd_id=,bar
diff --git a/src/test/disk_tests/usages/mounts b/src/test/disk_tests/usages/mounts
new file mode 100644 (file)
index 0000000..f8c1cd3
--- /dev/null
@@ -0,0 +1,2 @@
+/dev/sda /mountpoint1
+/dev/sdf1 /var/lib/ceph/osd/ceph-444
diff --git a/src/test/disk_tests/usages/partlist b/src/test/disk_tests/usages/partlist
new file mode 100644 (file)
index 0000000..43c1f68
--- /dev/null
@@ -0,0 +1,6 @@
+sdd1
+sdd2
+sde1
+sdf1
+sdm1
+sdm9
diff --git a/src/test/disk_tests/usages/pvs b/src/test/disk_tests/usages/pvs
new file mode 100644 (file)
index 0000000..86ec3d4
--- /dev/null
@@ -0,0 +1,7 @@
+  /dev/sdb
+  /dev/sdg
+  /dev/sdh
+  /dev/sdi
+  /dev/sdj
+  /dev/sdk
+  /dev/sdl
diff --git a/src/test/disk_tests/usages/sda/device/vendor b/src/test/disk_tests/usages/sda/device/vendor
new file mode 100644 (file)
index 0000000..531030d
--- /dev/null
@@ -0,0 +1 @@
+ATA
diff --git a/src/test/disk_tests/usages/sda/queue/rotational b/src/test/disk_tests/usages/sda/queue/rotational
new file mode 100644 (file)
index 0000000..d00491f
--- /dev/null
@@ -0,0 +1 @@
+1
diff --git a/src/test/disk_tests/usages/sda/size b/src/test/disk_tests/usages/sda/size
new file mode 100644 (file)
index 0000000..13de30f
--- /dev/null
@@ -0,0 +1 @@
+3000
diff --git a/src/test/disk_tests/usages/sda_udevadm b/src/test/disk_tests/usages/sda_udevadm
new file mode 100644 (file)
index 0000000..ab04390
--- /dev/null
@@ -0,0 +1,12 @@
+E: DEVNAME=/dev/sda
+E: DEVTYPE=disk
+E: ID_ATA_ROTATION_RATE_RPM=0
+E: ID_BUS=ata
+E: ID_MODEL=MODEL1
+E: ID_PART_TABLE_TYPE=gpt
+E: ID_PART_TABLE_UUID=8417b93f-eff9-4e8f-8d84-dc2e77fc07a2
+E: ID_SERIAL=SERIAL1
+E: ID_SERIAL_SHORT=SERIAL1
+E: ID_TYPE=disk
+E: ID_WWN=0x0000000000000000
+E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/src/test/disk_tests/usages/sdb/device/vendor b/src/test/disk_tests/usages/sdb/device/vendor
new file mode 100644 (file)
index 0000000..531030d
--- /dev/null
@@ -0,0 +1 @@
+ATA
diff --git a/src/test/disk_tests/usages/sdb/queue/rotational b/src/test/disk_tests/usages/sdb/queue/rotational
new file mode 100644 (file)
index 0000000..d00491f
--- /dev/null
@@ -0,0 +1 @@
+1
diff --git a/src/test/disk_tests/usages/sdb/size b/src/test/disk_tests/usages/sdb/size
new file mode 100644 (file)
index 0000000..13de30f
--- /dev/null
@@ -0,0 +1 @@
+3000
diff --git a/src/test/disk_tests/usages/sdb_udevadm b/src/test/disk_tests/usages/sdb_udevadm
new file mode 100644 (file)
index 0000000..ada1ca8
--- /dev/null
@@ -0,0 +1,12 @@
+E: DEVNAME=/dev/sdb
+E: DEVTYPE=disk
+E: ID_ATA_ROTATION_RATE_RPM=0
+E: ID_BUS=ata
+E: ID_MODEL=MODEL1
+E: ID_PART_TABLE_TYPE=gpt
+E: ID_PART_TABLE_UUID=8417b93f-eff9-4e8f-8d84-dc2e77fc07a2
+E: ID_SERIAL=SERIAL1
+E: ID_SERIAL_SHORT=SERIAL1
+E: ID_TYPE=disk
+E: ID_WWN=0x0000000000000000
+E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/src/test/disk_tests/usages/sdc/device/vendor b/src/test/disk_tests/usages/sdc/device/vendor
new file mode 100644 (file)
index 0000000..531030d
--- /dev/null
@@ -0,0 +1 @@
+ATA
diff --git a/src/test/disk_tests/usages/sdc/queue/rotational b/src/test/disk_tests/usages/sdc/queue/rotational
new file mode 100644 (file)
index 0000000..d00491f
--- /dev/null
@@ -0,0 +1 @@
+1
diff --git a/src/test/disk_tests/usages/sdc/size b/src/test/disk_tests/usages/sdc/size
new file mode 100644 (file)
index 0000000..13de30f
--- /dev/null
@@ -0,0 +1 @@
+3000
diff --git a/src/test/disk_tests/usages/sdc_udevadm b/src/test/disk_tests/usages/sdc_udevadm
new file mode 100644 (file)
index 0000000..42845f3
--- /dev/null
@@ -0,0 +1,12 @@
+E: DEVNAME=/dev/sdc
+E: DEVTYPE=disk
+E: ID_ATA_ROTATION_RATE_RPM=0
+E: ID_BUS=ata
+E: ID_MODEL=MODEL1
+E: ID_PART_TABLE_TYPE=gpt
+E: ID_PART_TABLE_UUID=8417b93f-eff9-4e8f-8d84-dc2e77fc07a2
+E: ID_SERIAL=SERIAL1
+E: ID_SERIAL_SHORT=SERIAL1
+E: ID_TYPE=disk
+E: ID_WWN=0x0000000000000000
+E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/src/test/disk_tests/usages/sdd/device/vendor b/src/test/disk_tests/usages/sdd/device/vendor
new file mode 100644 (file)
index 0000000..531030d
--- /dev/null
@@ -0,0 +1 @@
+ATA
diff --git a/src/test/disk_tests/usages/sdd/queue/rotational b/src/test/disk_tests/usages/sdd/queue/rotational
new file mode 100644 (file)
index 0000000..d00491f
--- /dev/null
@@ -0,0 +1 @@
+1
diff --git a/src/test/disk_tests/usages/sdd/sdd1/size b/src/test/disk_tests/usages/sdd/sdd1/size
new file mode 100644 (file)
index 0000000..83b33d2
--- /dev/null
@@ -0,0 +1 @@
+1000
diff --git a/src/test/disk_tests/usages/sdd/sdd2/size b/src/test/disk_tests/usages/sdd/sdd2/size
new file mode 100644 (file)
index 0000000..8bd1af1
--- /dev/null
@@ -0,0 +1 @@
+2000
diff --git a/src/test/disk_tests/usages/sdd/size b/src/test/disk_tests/usages/sdd/size
new file mode 100644 (file)
index 0000000..13de30f
--- /dev/null
@@ -0,0 +1 @@
+3000
diff --git a/src/test/disk_tests/usages/sdd_udevadm b/src/test/disk_tests/usages/sdd_udevadm
new file mode 100644 (file)
index 0000000..65e880f
--- /dev/null
@@ -0,0 +1,12 @@
+E: DEVNAME=/dev/sdd
+E: DEVTYPE=disk
+E: ID_ATA_ROTATION_RATE_RPM=0
+E: ID_BUS=ata
+E: ID_MODEL=MODEL1
+E: ID_PART_TABLE_TYPE=gpt
+E: ID_PART_TABLE_UUID=8417b93f-eff9-4e8f-8d84-dc2e77fc07a2
+E: ID_SERIAL=SERIAL1
+E: ID_SERIAL_SHORT=SERIAL1
+E: ID_TYPE=disk
+E: ID_WWN=0x0000000000000000
+E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/src/test/disk_tests/usages/sde/device/vendor b/src/test/disk_tests/usages/sde/device/vendor
new file mode 100644 (file)
index 0000000..531030d
--- /dev/null
@@ -0,0 +1 @@
+ATA
diff --git a/src/test/disk_tests/usages/sde/queue/rotational b/src/test/disk_tests/usages/sde/queue/rotational
new file mode 100644 (file)
index 0000000..d00491f
--- /dev/null
@@ -0,0 +1 @@
+1
diff --git a/src/test/disk_tests/usages/sde/sde1/size b/src/test/disk_tests/usages/sde/sde1/size
new file mode 100644 (file)
index 0000000..13de30f
--- /dev/null
@@ -0,0 +1 @@
+3000
diff --git a/src/test/disk_tests/usages/sde/size b/src/test/disk_tests/usages/sde/size
new file mode 100644 (file)
index 0000000..13de30f
--- /dev/null
@@ -0,0 +1 @@
+3000
diff --git a/src/test/disk_tests/usages/sde_udevadm b/src/test/disk_tests/usages/sde_udevadm
new file mode 100644 (file)
index 0000000..db77725
--- /dev/null
@@ -0,0 +1,12 @@
+E: DEVNAME=/dev/sde
+E: DEVTYPE=disk
+E: ID_ATA_ROTATION_RATE_RPM=0
+E: ID_BUS=ata
+E: ID_MODEL=MODEL1
+E: ID_PART_TABLE_TYPE=gpt
+E: ID_PART_TABLE_UUID=8417b93f-eff9-4e8f-8d84-dc2e77fc07a2
+E: ID_SERIAL=SERIAL1
+E: ID_SERIAL_SHORT=SERIAL1
+E: ID_TYPE=disk
+E: ID_WWN=0x0000000000000000
+E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/src/test/disk_tests/usages/sdf/device/vendor b/src/test/disk_tests/usages/sdf/device/vendor
new file mode 100644 (file)
index 0000000..531030d
--- /dev/null
@@ -0,0 +1 @@
+ATA
diff --git a/src/test/disk_tests/usages/sdf/queue/rotational b/src/test/disk_tests/usages/sdf/queue/rotational
new file mode 100644 (file)
index 0000000..d00491f
--- /dev/null
@@ -0,0 +1 @@
+1
diff --git a/src/test/disk_tests/usages/sdf/sdf1/size b/src/test/disk_tests/usages/sdf/sdf1/size
new file mode 100644 (file)
index 0000000..13de30f
--- /dev/null
@@ -0,0 +1 @@
+3000
diff --git a/src/test/disk_tests/usages/sdf/size b/src/test/disk_tests/usages/sdf/size
new file mode 100644 (file)
index 0000000..13de30f
--- /dev/null
@@ -0,0 +1 @@
+3000
diff --git a/src/test/disk_tests/usages/sdf_udevadm b/src/test/disk_tests/usages/sdf_udevadm
new file mode 100644 (file)
index 0000000..ad49acf
--- /dev/null
@@ -0,0 +1,12 @@
+E: DEVNAME=/dev/sdf
+E: DEVTYPE=disk
+E: ID_ATA_ROTATION_RATE_RPM=0
+E: ID_BUS=ata
+E: ID_MODEL=MODEL1
+E: ID_PART_TABLE_TYPE=gpt
+E: ID_PART_TABLE_UUID=8417b93f-eff9-4e8f-8d84-dc2e77fc07a2
+E: ID_SERIAL=SERIAL1
+E: ID_SERIAL_SHORT=SERIAL1
+E: ID_TYPE=disk
+E: ID_WWN=0x0000000000000000
+E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/src/test/disk_tests/usages/sdg/device/vendor b/src/test/disk_tests/usages/sdg/device/vendor
new file mode 100644 (file)
index 0000000..531030d
--- /dev/null
@@ -0,0 +1 @@
+ATA
diff --git a/src/test/disk_tests/usages/sdg/queue/rotational b/src/test/disk_tests/usages/sdg/queue/rotational
new file mode 100644 (file)
index 0000000..d00491f
--- /dev/null
@@ -0,0 +1 @@
+1
diff --git a/src/test/disk_tests/usages/sdg/size b/src/test/disk_tests/usages/sdg/size
new file mode 100644 (file)
index 0000000..13de30f
--- /dev/null
@@ -0,0 +1 @@
+3000
diff --git a/src/test/disk_tests/usages/sdg_udevadm b/src/test/disk_tests/usages/sdg_udevadm
new file mode 100644 (file)
index 0000000..6d40d35
--- /dev/null
@@ -0,0 +1,12 @@
+E: DEVNAME=/dev/sdg
+E: DEVTYPE=disk
+E: ID_ATA_ROTATION_RATE_RPM=0
+E: ID_BUS=ata
+E: ID_MODEL=MODEL1
+E: ID_PART_TABLE_TYPE=gpt
+E: ID_PART_TABLE_UUID=8417b93f-eff9-4e8f-8d84-dc2e77fc07a2
+E: ID_SERIAL=SERIAL1
+E: ID_SERIAL_SHORT=SERIAL1
+E: ID_TYPE=disk
+E: ID_WWN=0x0000000000000000
+E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/src/test/disk_tests/usages/sdh/device/vendor b/src/test/disk_tests/usages/sdh/device/vendor
new file mode 100644 (file)
index 0000000..531030d
--- /dev/null
@@ -0,0 +1 @@
+ATA
diff --git a/src/test/disk_tests/usages/sdh/queue/rotational b/src/test/disk_tests/usages/sdh/queue/rotational
new file mode 100644 (file)
index 0000000..d00491f
--- /dev/null
@@ -0,0 +1 @@
+1
diff --git a/src/test/disk_tests/usages/sdh/size b/src/test/disk_tests/usages/sdh/size
new file mode 100644 (file)
index 0000000..13de30f
--- /dev/null
@@ -0,0 +1 @@
+3000
diff --git a/src/test/disk_tests/usages/sdh_udevadm b/src/test/disk_tests/usages/sdh_udevadm
new file mode 100644 (file)
index 0000000..3ff1a9e
--- /dev/null
@@ -0,0 +1,12 @@
+E: DEVNAME=/dev/sdh
+E: DEVTYPE=disk
+E: ID_ATA_ROTATION_RATE_RPM=0
+E: ID_BUS=ata
+E: ID_MODEL=MODEL1
+E: ID_PART_TABLE_TYPE=gpt
+E: ID_PART_TABLE_UUID=8417b93f-eff9-4e8f-8d84-dc2e77fc07a2
+E: ID_SERIAL=SERIAL1
+E: ID_SERIAL_SHORT=SERIAL1
+E: ID_TYPE=disk
+E: ID_WWN=0x0000000000000000
+E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/src/test/disk_tests/usages/sdi/device/vendor b/src/test/disk_tests/usages/sdi/device/vendor
new file mode 100644 (file)
index 0000000..531030d
--- /dev/null
@@ -0,0 +1 @@
+ATA
diff --git a/src/test/disk_tests/usages/sdi/queue/rotational b/src/test/disk_tests/usages/sdi/queue/rotational
new file mode 100644 (file)
index 0000000..d00491f
--- /dev/null
@@ -0,0 +1 @@
+1
diff --git a/src/test/disk_tests/usages/sdi/size b/src/test/disk_tests/usages/sdi/size
new file mode 100644 (file)
index 0000000..13de30f
--- /dev/null
@@ -0,0 +1 @@
+3000
diff --git a/src/test/disk_tests/usages/sdi_udevadm b/src/test/disk_tests/usages/sdi_udevadm
new file mode 100644 (file)
index 0000000..a9eae5e
--- /dev/null
@@ -0,0 +1,12 @@
+E: DEVNAME=/dev/sdi
+E: DEVTYPE=disk
+E: ID_ATA_ROTATION_RATE_RPM=0
+E: ID_BUS=ata
+E: ID_MODEL=MODEL1
+E: ID_PART_TABLE_TYPE=gpt
+E: ID_PART_TABLE_UUID=8417b93f-eff9-4e8f-8d84-dc2e77fc07a2
+E: ID_SERIAL=SERIAL1
+E: ID_SERIAL_SHORT=SERIAL1
+E: ID_TYPE=disk
+E: ID_WWN=0x0000000000000000
+E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/src/test/disk_tests/usages/sdj/device/vendor b/src/test/disk_tests/usages/sdj/device/vendor
new file mode 100644 (file)
index 0000000..531030d
--- /dev/null
@@ -0,0 +1 @@
+ATA
diff --git a/src/test/disk_tests/usages/sdj/queue/rotational b/src/test/disk_tests/usages/sdj/queue/rotational
new file mode 100644 (file)
index 0000000..d00491f
--- /dev/null
@@ -0,0 +1 @@
+1
diff --git a/src/test/disk_tests/usages/sdj/size b/src/test/disk_tests/usages/sdj/size
new file mode 100644 (file)
index 0000000..13de30f
--- /dev/null
@@ -0,0 +1 @@
+3000
diff --git a/src/test/disk_tests/usages/sdj_udevadm b/src/test/disk_tests/usages/sdj_udevadm
new file mode 100644 (file)
index 0000000..39d0cf3
--- /dev/null
@@ -0,0 +1,12 @@
+E: DEVNAME=/dev/sdj
+E: DEVTYPE=disk
+E: ID_ATA_ROTATION_RATE_RPM=0
+E: ID_BUS=ata
+E: ID_MODEL=MODEL1
+E: ID_PART_TABLE_TYPE=gpt
+E: ID_PART_TABLE_UUID=8417b93f-eff9-4e8f-8d84-dc2e77fc07a2
+E: ID_SERIAL=SERIAL1
+E: ID_SERIAL_SHORT=SERIAL1
+E: ID_TYPE=disk
+E: ID_WWN=0x0000000000000000
+E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/src/test/disk_tests/usages/sdk/device/vendor b/src/test/disk_tests/usages/sdk/device/vendor
new file mode 100644 (file)
index 0000000..531030d
--- /dev/null
@@ -0,0 +1 @@
+ATA
diff --git a/src/test/disk_tests/usages/sdk/queue/rotational b/src/test/disk_tests/usages/sdk/queue/rotational
new file mode 100644 (file)
index 0000000..d00491f
--- /dev/null
@@ -0,0 +1 @@
+1
diff --git a/src/test/disk_tests/usages/sdk/size b/src/test/disk_tests/usages/sdk/size
new file mode 100644 (file)
index 0000000..13de30f
--- /dev/null
@@ -0,0 +1 @@
+3000
diff --git a/src/test/disk_tests/usages/sdk_udevadm b/src/test/disk_tests/usages/sdk_udevadm
new file mode 100644 (file)
index 0000000..3baef2f
--- /dev/null
@@ -0,0 +1,12 @@
+E: DEVNAME=/dev/sdk
+E: DEVTYPE=disk
+E: ID_ATA_ROTATION_RATE_RPM=0
+E: ID_BUS=ata
+E: ID_MODEL=MODEL1
+E: ID_PART_TABLE_TYPE=gpt
+E: ID_PART_TABLE_UUID=8417b93f-eff9-4e8f-8d84-dc2e77fc07a2
+E: ID_SERIAL=SERIAL1
+E: ID_SERIAL_SHORT=SERIAL1
+E: ID_TYPE=disk
+E: ID_WWN=0x0000000000000000
+E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/src/test/disk_tests/usages/sdl/device/vendor b/src/test/disk_tests/usages/sdl/device/vendor
new file mode 100644 (file)
index 0000000..531030d
--- /dev/null
@@ -0,0 +1 @@
+ATA
diff --git a/src/test/disk_tests/usages/sdl/queue/rotational b/src/test/disk_tests/usages/sdl/queue/rotational
new file mode 100644 (file)
index 0000000..d00491f
--- /dev/null
@@ -0,0 +1 @@
+1
diff --git a/src/test/disk_tests/usages/sdl/size b/src/test/disk_tests/usages/sdl/size
new file mode 100644 (file)
index 0000000..13de30f
--- /dev/null
@@ -0,0 +1 @@
+3000
diff --git a/src/test/disk_tests/usages/sdl_udevadm b/src/test/disk_tests/usages/sdl_udevadm
new file mode 100644 (file)
index 0000000..ead0622
--- /dev/null
@@ -0,0 +1,12 @@
+E: DEVNAME=/dev/sdl
+E: DEVTYPE=disk
+E: ID_ATA_ROTATION_RATE_RPM=0
+E: ID_BUS=ata
+E: ID_MODEL=MODEL1
+E: ID_PART_TABLE_TYPE=gpt
+E: ID_PART_TABLE_UUID=8417b93f-eff9-4e8f-8d84-dc2e77fc07a2
+E: ID_SERIAL=SERIAL1
+E: ID_SERIAL_SHORT=SERIAL1
+E: ID_TYPE=disk
+E: ID_WWN=0x0000000000000000
+E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/src/test/disk_tests/usages/sdm/device/vendor b/src/test/disk_tests/usages/sdm/device/vendor
new file mode 100644 (file)
index 0000000..531030d
--- /dev/null
@@ -0,0 +1 @@
+ATA
diff --git a/src/test/disk_tests/usages/sdm/queue/rotational b/src/test/disk_tests/usages/sdm/queue/rotational
new file mode 100644 (file)
index 0000000..d00491f
--- /dev/null
@@ -0,0 +1 @@
+1
diff --git a/src/test/disk_tests/usages/sdm/sdm1/size b/src/test/disk_tests/usages/sdm/sdm1/size
new file mode 100644 (file)
index 0000000..83b33d2
--- /dev/null
@@ -0,0 +1 @@
+1000
diff --git a/src/test/disk_tests/usages/sdm/sdm9/size b/src/test/disk_tests/usages/sdm/sdm9/size
new file mode 100644 (file)
index 0000000..8bd1af1
--- /dev/null
@@ -0,0 +1 @@
+2000
diff --git a/src/test/disk_tests/usages/sdm/size b/src/test/disk_tests/usages/sdm/size
new file mode 100644 (file)
index 0000000..13de30f
--- /dev/null
@@ -0,0 +1 @@
+3000
diff --git a/src/test/disk_tests/usages/sdm_udevadm b/src/test/disk_tests/usages/sdm_udevadm
new file mode 100644 (file)
index 0000000..9317b9f
--- /dev/null
@@ -0,0 +1,12 @@
+E: DEVNAME=/dev/sdm
+E: DEVTYPE=disk
+E: ID_ATA_ROTATION_RATE_RPM=0
+E: ID_BUS=ata
+E: ID_MODEL=MODEL1
+E: ID_PART_TABLE_TYPE=gpt
+E: ID_PART_TABLE_UUID=8417b93f-eff9-4e8f-8d84-dc2e77fc07a2
+E: ID_SERIAL=SERIAL1
+E: ID_SERIAL_SHORT=SERIAL1
+E: ID_TYPE=disk
+E: ID_WWN=0x0000000000000000
+E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/src/test/disk_tests/usages/sdn/device/vendor b/src/test/disk_tests/usages/sdn/device/vendor
new file mode 100644 (file)
index 0000000..531030d
--- /dev/null
@@ -0,0 +1 @@
+ATA
diff --git a/src/test/disk_tests/usages/sdn/queue/rotational b/src/test/disk_tests/usages/sdn/queue/rotational
new file mode 100644 (file)
index 0000000..d00491f
--- /dev/null
@@ -0,0 +1 @@
+1
diff --git a/src/test/disk_tests/usages/sdn/size b/src/test/disk_tests/usages/sdn/size
new file mode 100644 (file)
index 0000000..13de30f
--- /dev/null
@@ -0,0 +1 @@
+3000
diff --git a/src/test/disk_tests/usages/sdn_udevadm b/src/test/disk_tests/usages/sdn_udevadm
new file mode 100644 (file)
index 0000000..5ec4a92
--- /dev/null
@@ -0,0 +1,14 @@
+E: DEVNAME=/dev/sdn
+E: DEVTYPE=disk
+E: ID_ATA_ROTATION_RATE_RPM=0
+E: ID_BUS=ata
+E: ID_MODEL=MODEL1
+E: ID_SERIAL=SERIAL1
+E: ID_SERIAL_SHORT=SERIAL1
+E: ID_TYPE=disk
+E: ID_FS_UUID=ab54fba8-48fe-4d37-bbe7-b403f94d3bed
+E: ID_FS_UUID_ENC=ab54fba8-48fe-4d37-bbe7-b403f94d3bed
+E: ID_FS_TYPE=xfs
+E: ID_FS_USAGE=filesystem
+E: ID_WWN=0x0000000000000000
+E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/src/test/disk_tests/usages/zpool b/src/test/disk_tests/usages/zpool
new file mode 100644 (file)
index 0000000..3431792
--- /dev/null
@@ -0,0 +1,6 @@
+testpool       2.00T   1.00T   2.60T   -       5%      28%     1.00x   ONLINE  -
+       /dev/sdc        3.62T   1.02T   2.60T   -       5%      28%
+log      -      -      -         -      -      -
+       /dev/sdd1       15.9G   2.79M   15.9G   -       82%     0%
+cache      -      -      -         -      -      -
+       /dev/sdd2       42.5G   36.2G   6.36G   -       0%      85%
diff --git a/src/test/disklist_test.pm b/src/test/disklist_test.pm
new file mode 100644 (file)
index 0000000..97f11fc
--- /dev/null
@@ -0,0 +1,265 @@
+package PVE::Diskmanage::Test;
+
+use strict;
+use warnings;
+
+use lib qw(..);
+
+use PVE::Diskmanage;
+use PVE::Tools;
+
+use Test::MockModule;
+use Test::More;
+use JSON;
+use Data::Dumper;
+
+my $testcasedir; # current case directory
+my $testcount = 0; # testcount for TAP::Harness
+my $diskmanage_module; # mockmodule for PVE::Diskmanage
+my $print = 0;
+
+sub mocked_run_command {
+    my ($cmd, %param) = @_;
+
+    my $outputlines = [];
+    if (my $ref = ref($cmd)) {
+       if ($cmd->[0] =~ m/udevadm/i) {
+           # simulate udevadm output
+           my $dev = $cmd->[3];
+           $dev =~ s|/sys/block/||;
+           @$outputlines = split(/\n/, read_test_file("${dev}_udevadm"));
+
+       } elsif ($cmd->[0] =~ m/smartctl/i) {
+           # simulate smartctl output
+           my $dev;
+           my $type;
+           if (@$cmd > 3) {
+               $dev = $cmd->[5];
+               $type = 'smart';
+           } else {
+               $dev = $cmd->[2];
+               $type = 'health';
+           }
+           $dev =~ s|/dev/||;
+           @$outputlines = split(/\n/, read_test_file("${dev}_${type}"));
+       } elsif ($cmd->[0] =~ m/sgdisk/i) {
+           # simulate sgdisk
+           die "implement me: @$cmd\n";
+       } elsif ($cmd->[0] =~ m/zpool/i) {
+           # simulate zpool output
+           @$outputlines = split(/\n/, read_test_file('zpool'));
+
+       } elsif ($cmd->[0] =~ m/pvs/i) {
+           # simulate lvs output
+           @$outputlines = split(/\n/, read_test_file('pvs'));
+       } elsif ($cmd->[0] =~ m/lvs/i) {
+           @$outputlines = split(/\n/, read_test_file('lvs'));
+       } elsif ($cmd->[0] =~ m/lsblk/i) {
+           my $content = read_test_file('lsblk');
+           if ($content eq '') {
+               $content = '{}';
+           }
+           @$outputlines = split(/\n/, $content);
+       } else {
+           die "unexpected run_command call: '@$cmd', aborting\n";
+       }
+    } else {
+       print "unexpected run_command call: '@$cmd', aborting\n";
+       die;
+    }
+
+    my $outfunc;
+    if ($param{outfunc}) {
+       $outfunc = $param{outfunc};
+       map { &$outfunc(($_)) } @$outputlines;
+
+       return 0;
+    }
+}
+
+sub mocked_get_sysdir_info {
+    my ($param) = @_;
+
+    my $originalsub = $diskmanage_module->original('get_sysdir_info');
+
+    $param =~ s|/sys/block|disk_tests/$testcasedir|;
+
+    return &$originalsub($param);
+}
+
+sub mocked_get_sysdir_size {
+    my ($param) = @_;
+
+    my $originalsub = $diskmanage_module->original('get_sysdir_size');
+
+    $param =~ s|/sys/block|disk_tests/$testcasedir|;
+
+    return &$originalsub($param);
+}
+
+sub mocked_is_iscsi {
+    return 0;
+}
+
+sub mocked_dir_glob_foreach {
+    my ($dir, $regex, $sub) = @_;
+
+    my $lines = [];
+
+    # read lines in from file
+    if ($dir =~ m{^/sys/block$} ) {
+       @$lines = split(/\n/, read_test_file('disklist'));
+    } elsif ($dir =~ m{^/sys/block/([^/]+)}) {
+       @$lines = split(/\n/, read_test_file('partlist'));
+    }
+
+    foreach my $line (@$lines) {
+       if ($line =~ m/$regex/) {
+           &$sub($line);
+       }
+    }
+}
+
+sub mocked_parse_proc_mounts {
+    my $text = read_test_file('mounts');
+
+    my $mounts = [];
+
+    foreach my $line(split(/\n/, $text)) {
+       push @$mounts, [split(/\s+/, $line)];
+    }
+
+    return $mounts;
+}
+
+sub read_test_file {
+    my ($filename) = @_;
+
+    if (!-f  "disk_tests/$testcasedir/$filename") {
+       print "file '$testcasedir/$filename' not found\n";
+       return '';
+    }
+    open (my $fh, '<', "disk_tests/$testcasedir/$filename")
+       or die "Cannot open disk_tests/$testcasedir/$filename: $!";
+
+    my $output = <$fh> // '';
+    chomp $output if $output;
+    while (my $line = <$fh>) {
+       chomp $line;
+       $output .= "\n$line";
+    }
+
+    return $output;
+}
+
+
+sub test_disk_list {
+    my ($testdir) = @_;
+    subtest "Test '$testdir'" => sub {
+       my $testcount = 0;
+       $testcasedir = $testdir;
+
+       my $disks;
+       my $expected_disk_list;
+       eval {
+           $disks = PVE::Diskmanage::get_disks();
+       };
+       warn $@ if $@;
+       $expected_disk_list = decode_json(read_test_file('disklist_expected.json'));
+
+       print Dumper($disks) if $print;
+       $testcount++;
+       is_deeply($disks, $expected_disk_list, 'disk list should be the same');
+
+       foreach my $disk (sort keys %$disks) {
+           my $smart;
+           my $expected_smart;
+           eval {
+               $smart = PVE::Diskmanage::get_smart_data("/dev/$disk");
+               print Dumper($smart) if $print;
+               $expected_smart = decode_json(read_test_file("${disk}_smart_expected.json"));
+           };
+
+           if ($smart && $expected_smart) {
+               $testcount++;
+               is_deeply($smart, $expected_smart, "smart data for '$disk' should be the same");
+           } elsif ($smart && -f  "disk_tests/$testcasedir/${disk}_smart_expected.json") {
+               $testcount++;
+               ok(0,  "could not parse expected smart for '$disk'\n");
+           }
+           my $disk_tmp = {};
+
+           # test single disk parameter
+           $disk_tmp = PVE::Diskmanage::get_disks($disk);
+           warn $@ if $@;
+           $testcount++;
+           print Dumper $disk_tmp if $print;
+           is_deeply($disk_tmp->{$disk}, $expected_disk_list->{$disk}, "disk $disk should be the same");
+
+
+           # test wrong parameter
+           eval {
+               PVE::Diskmanage::get_disks( { test => 1 } );
+           };
+           my $err = $@;
+           $testcount++;
+           is_deeply($err, "disks is not a string or array reference\n", "error message should be the same");
+
+       }
+           # test multi disk parameter
+           $disks = PVE::Diskmanage::get_disks( [ keys %$disks ] );
+           $testcount++;
+           is_deeply($disks, $expected_disk_list, 'disk list should be the same');
+
+       done_testing($testcount);
+    };
+}
+
+# start reading tests:
+
+if (@ARGV && $ARGV[0] eq 'print') {
+    $print = 1;
+}
+
+print("Setting up Mocking\n");
+$diskmanage_module = Test::MockModule->new('PVE::Diskmanage', no_auto => 1);
+$diskmanage_module->mock('run_command' => \&mocked_run_command);
+print("\tMocked run_command\n");
+$diskmanage_module->mock('dir_glob_foreach' => \&mocked_dir_glob_foreach);
+print("\tMocked dir_glob_foreach\n");
+$diskmanage_module->mock('get_sysdir_info' => \&mocked_get_sysdir_info);
+print("\tMocked get_sysdir_info\n");
+$diskmanage_module->mock('get_sysdir_size' => \&mocked_get_sysdir_size);
+print("\tMocked get_sysdir_size\n");
+$diskmanage_module->mock('is_iscsi' => \&mocked_is_iscsi);
+print("\tMocked is_iscsi\n");
+$diskmanage_module->mock('assert_blockdev' => sub { return 1; });
+print("\tMocked assert_blockdev\n");
+$diskmanage_module->mock('dir_is_empty' => sub {
+       # all partitions have a holder dir
+       my $val = shift;
+       if ($val =~ m|^/sys/block/.+/.+/|) {
+           return 0;
+       }
+       return 1;
+    });
+print("\tMocked dir_is_empty\n");
+$diskmanage_module->mock('check_bin' => sub { return 1; });
+print("\tMocked check_bin\n");
+my $tools_module= Test::MockModule->new('PVE::ProcFSTools', no_auto => 1);
+$tools_module->mock('parse_proc_mounts' => \&mocked_parse_proc_mounts);
+print("\tMocked parse_proc_mounts\n");
+print("Done Setting up Mocking\n\n");
+
+print("Beginning Tests:\n\n");
+opendir (my $dh, 'disk_tests')
+    or die "Cannot open disk_tests: $!";
+
+while (readdir $dh) {
+    my $dir = $_;
+    next if $dir eq '.' or $dir eq '..';
+    $testcount++;
+    test_disk_list($dir);
+}
+
+done_testing($testcount);
diff --git a/src/test/filesystem_path_test.pm b/src/test/filesystem_path_test.pm
new file mode 100644 (file)
index 0000000..c1b6d90
--- /dev/null
@@ -0,0 +1,91 @@
+package PVE::Storage::TestFilesystemPath;
+
+use strict;
+use warnings;
+
+use lib qw(..);
+
+use PVE::Storage;
+use Test::More;
+
+my $path = '/some/path';
+
+# each array entry is a test that consists of the following keys:
+# volname  => image name that is passed to parse_volname
+# snapname => to test the die condition
+# expected => the array of return values; or the die message
+my $tests = [
+    {
+       volname  => '1234/vm-1234-disk-0.raw',
+       snapname => undef,
+       expected => [
+           "$path/images/1234/vm-1234-disk-0.raw",
+           '1234',
+           'images'
+       ],
+    },
+    {
+       volname  => '1234/vm-1234-disk-0.raw',
+       snapname => 'my_snap',
+       expected => "can't snapshot this image format\n"
+    },
+    {
+       volname  => '1234/vm-1234-disk-0.qcow2',
+       snapname => undef,
+       expected => [
+           "$path/images/1234/vm-1234-disk-0.qcow2",
+           '1234',
+           'images'
+       ],
+    },
+    {
+       volname  => '1234/vm-1234-disk-0.qcow2',
+       snapname => 'my_snap',
+       expected => [
+           "$path/images/1234/vm-1234-disk-0.qcow2",
+           '1234',
+           'images'
+       ],
+    },
+    {
+       volname  => 'iso/my-awesome-proxmox.iso',
+       snapname => undef,
+       expected => [
+           "$path/template/iso/my-awesome-proxmox.iso",
+           undef,
+           'iso'
+       ],
+    },
+    {
+       volname  => "backup/vzdump-qemu-1234-2020_03_30-21_12_40.vma",
+       snapname => undef,
+       expected => [
+           "$path/dump/vzdump-qemu-1234-2020_03_30-21_12_40.vma",
+           1234,
+           'backup'
+       ],
+    },
+];
+
+plan tests => scalar @$tests;
+
+foreach my $tt (@$tests) {
+    my $volname = $tt->{volname};
+    my $snapname = $tt->{snapname};
+    my $expected = $tt->{expected};
+    my $scfg = { path => $path };
+    my $got;
+
+    eval {
+       $got = [ PVE::Storage::Plugin->filesystem_path($scfg, $volname, $snapname) ];
+    };
+    $got = $@ if $@;
+
+    is_deeply($got, $expected, "wantarray: filesystem_path for $volname")
+    || diag(explain($got));
+
+}
+
+done_testing();
+
+1;
diff --git a/src/test/get_subdir_test.pm b/src/test/get_subdir_test.pm
new file mode 100644 (file)
index 0000000..b9d61d5
--- /dev/null
@@ -0,0 +1,51 @@
+package PVE::Storage::TestGetSubdir;
+
+use strict;
+use warnings;
+
+use lib qw(..);
+
+use PVE::Storage::Plugin;
+use Test::More;
+
+my $scfg_with_path = { path => '/some/path' };
+my $vtype_subdirs = PVE::Storage::Plugin::get_vtype_subdirs();
+
+# each test is comprised of the following array keys:
+# [0] => storage config; positive with path key
+# [1] => storage type;  see $vtype_subdirs
+# [2] => expected return from get_subdir
+my $tests = [
+    # failed matches
+    [ $scfg_with_path, 'none', "unknown vtype 'none'\n" ],
+    [ {}, 'iso', "storage definition has no path\n" ],
+];
+
+# creates additional positive tests
+foreach my $type (keys %$vtype_subdirs) {
+    my $path = "$scfg_with_path->{path}/$vtype_subdirs->{$type}";
+    push @$tests, [ $scfg_with_path, $type, $path ];
+}
+
+# creates additional tests for overrides
+foreach my $type (keys %$vtype_subdirs) {
+    my $override = "${type}_override";
+    my $scfg_with_override = { path => '/some/path', 'content-dirs' => { $type => $override } };
+    push @$tests, [ $scfg_with_override, $type, "$scfg_with_override->{path}/$scfg_with_override->{'content-dirs'}->{$type}" ];
+}
+
+plan tests => scalar @$tests;
+
+foreach my $tt (@$tests) {
+    my ($scfg, $type, $expected) = @$tt;
+
+    my $got;
+    eval { $got = PVE::Storage::Plugin->get_subdir($scfg, $type) };
+    $got = $@ if $@;
+
+    is ($got, $expected, "get_subdir for $type") || diag(explain($got));
+}
+
+done_testing();
+
+1;
diff --git a/src/test/list_volumes_test.pm b/src/test/list_volumes_test.pm
new file mode 100644 (file)
index 0000000..d155cb9
--- /dev/null
@@ -0,0 +1,548 @@
+package PVE::Storage::TestListVolumes;
+
+use strict;
+use warnings;
+
+use lib qw(..);
+
+use PVE::Storage;
+use PVE::Cluster;
+use PVE::Tools qw(run_command);
+
+use Test::More;
+use Test::MockModule;
+
+use Cwd;
+use File::Basename;
+use File::Path qw(make_path remove_tree);
+use File::stat qw();
+use File::Temp;
+use Storable qw(dclone);
+
+use constant DEFAULT_SIZE => 131072; # 128 kiB
+use constant DEFAULT_USED => 262144; # 256 kiB
+use constant DEFAULT_CTIME => 1234567890;
+
+# get_vmlist() return values
+my $mocked_vmlist = {
+    'version' => 1,
+    'ids' => {
+       '16110' => {
+           'node'    => 'x42',
+           'type'    => 'qemu',
+           'version' => 4,
+       },
+       '16112' => {
+           'node'    => 'x42',
+           'type'    => 'lxc',
+           'version' => 7,
+       },
+       '16114' => {
+           'node'    => 'x42',
+           'type'    => 'qemu',
+           'version' => 2,
+       },
+       '16113' => {
+           'node'    => 'x42',
+           'type'    => 'qemu',
+           'version' => 5,
+       },
+       '16115' => {
+           'node'    => 'x42',
+           'type'    => 'qemu',
+           'version' => 1,
+       },
+       '9004' => {
+           'node'    => 'x42',
+           'type'    => 'qemu',
+           'version' => 6,
+       }
+    }
+};
+
+my $storage_dir = File::Temp->newdir();
+my $scfg = {
+    'type'     => 'dir',
+    'maxfiles' => 0,
+    'path'     => $storage_dir,
+    'shared'   => 0,
+    'content'  => {
+       'iso'      => 1,
+       'rootdir'  => 1,
+       'vztmpl'   => 1,
+       'images'   => 1,
+       'snippets' => 1,
+       'backup'   => 1,
+    },
+};
+
+# The test cases are comprised of an arry of hashes with the following keys:
+# description => displayed on error by Test::More
+# vmid        => used for image matches by list_volume
+# files       => array of files for qemu-img to create
+# expected    => returned result hash
+#                (content, ctime, format, parent, size, used, vimd, volid)
+my @tests = (
+    {
+       description => 'VMID: 16110, VM, qcow2, backup, snippets',
+       vmid => '16110',
+       files => [
+           "$storage_dir/images/16110/vm-16110-disk-0.qcow2",
+           "$storage_dir/images/16110/vm-16110-disk-1.raw",
+           "$storage_dir/images/16110/vm-16110-disk-2.vmdk",
+           "$storage_dir/dump/vzdump-qemu-16110-2020_03_30-21_11_40.vma.gz",
+           "$storage_dir/dump/vzdump-qemu-16110-2020_03_30-21_12_45.vma.lzo",
+           "$storage_dir/dump/vzdump-qemu-16110-2020_03_30-21_13_55.vma",
+           "$storage_dir/dump/vzdump-qemu-16110-2020_03_30-21_13_55.vma.zst",
+           "$storage_dir/snippets/userconfig.yaml",
+           "$storage_dir/snippets/hookscript.pl",
+       ],
+       expected => [
+           {
+               'content' => 'images',
+               'ctime'   => DEFAULT_CTIME,
+               'format'  => 'qcow2',
+               'parent'  => undef,
+               'size'    => DEFAULT_SIZE,
+               'used'    => DEFAULT_USED,
+               'vmid'    => '16110',
+               'volid'   => 'local:16110/vm-16110-disk-0.qcow2',
+           },
+           {
+               'content' => 'images',
+               'ctime'   => DEFAULT_CTIME,
+               'format'  => 'raw',
+               'parent'  => undef,
+               'size'    => DEFAULT_SIZE,
+               'used'    => DEFAULT_USED,
+               'vmid'    => '16110',
+               'volid'   => 'local:16110/vm-16110-disk-1.raw',
+           },
+           {
+               'content' => 'images',
+               'ctime'   => DEFAULT_CTIME,
+               'format'  => 'vmdk',
+               'parent'  => undef,
+               'size'    => DEFAULT_SIZE,
+               'used'    => DEFAULT_USED,
+               'vmid'    => '16110',
+               'volid'   => 'local:16110/vm-16110-disk-2.vmdk',
+           },
+           {
+               'content' => 'backup',
+               'ctime'   => 1585602700,
+               'format'  => 'vma.gz',
+               'size'    => DEFAULT_SIZE,
+               'subtype' => 'qemu',
+               'vmid'    => '16110',
+               'volid'   => 'local:backup/vzdump-qemu-16110-2020_03_30-21_11_40.vma.gz',
+           },
+           {
+               'content' => 'backup',
+               'ctime'   => 1585602765,
+               'format'  => 'vma.lzo',
+               'size'    => DEFAULT_SIZE,
+               'subtype' => 'qemu',
+               'vmid'    => '16110',
+               'volid'   => 'local:backup/vzdump-qemu-16110-2020_03_30-21_12_45.vma.lzo',
+           },
+           {
+               'content' => 'backup',
+               'ctime'   => 1585602835,
+               'format'  => 'vma',
+               'size'    => DEFAULT_SIZE,
+               'subtype' => 'qemu',
+               'vmid'    => '16110',
+               'volid'   => 'local:backup/vzdump-qemu-16110-2020_03_30-21_13_55.vma',
+           },
+           {
+               'content' => 'backup',
+               'ctime'   => 1585602835,
+               'format'  => 'vma.zst',
+               'size'    => DEFAULT_SIZE,
+               'subtype' => 'qemu',
+               'vmid'    => '16110',
+               'volid'   => 'local:backup/vzdump-qemu-16110-2020_03_30-21_13_55.vma.zst',
+           },
+           {
+               'content' => 'snippets',
+               'ctime'   => DEFAULT_CTIME,
+               'format'  => 'snippet',
+               'size'    => DEFAULT_SIZE,
+               'volid'   => 'local:snippets/hookscript.pl',
+           },
+           {
+               'content' => 'snippets',
+               'ctime'   => DEFAULT_CTIME,
+               'format'  => 'snippet',
+               'size'    => DEFAULT_SIZE,
+               'volid'   => 'local:snippets/userconfig.yaml',
+           },
+       ],
+    },
+    {
+       description => 'VMID: 16112, lxc, raw, backup',
+       vmid => '16112',
+       files => [
+           "$storage_dir/images/16112/vm-16112-disk-0.raw",
+           "$storage_dir/dump/vzdump-lxc-16112-2020_03_30-21_39_30.tar.lzo",
+           "$storage_dir/dump/vzdump-lxc-16112-2020_03_30-21_49_30.tar.gz",
+           "$storage_dir/dump/vzdump-lxc-16112-2020_03_30-21_49_30.tar.zst",
+           "$storage_dir/dump/vzdump-lxc-16112-2020_03_30-21_59_30.tgz",
+       ],
+       expected => [
+           {
+               'content' => 'rootdir',
+               'ctime'   => DEFAULT_CTIME,
+               'format'  => 'raw',
+               'parent'  => undef,
+               'size'    => DEFAULT_SIZE,
+               'used'    => DEFAULT_USED,
+               'vmid'    => '16112',
+               'volid'   => 'local:16112/vm-16112-disk-0.raw',
+           },
+           {
+               'content' => 'backup',
+               'ctime'   => 1585604370,
+               'format'  => 'tar.lzo',
+               'size'    => DEFAULT_SIZE,
+               'subtype' => 'lxc',
+               'vmid'    => '16112',
+               'volid'   => 'local:backup/vzdump-lxc-16112-2020_03_30-21_39_30.tar.lzo',
+           },
+           {
+               'content' => 'backup',
+               'ctime'   => 1585604970,
+               'format'  => 'tar.gz',
+               'size'    => DEFAULT_SIZE,
+               'subtype' => 'lxc',
+               'vmid'    => '16112',
+               'volid'   => 'local:backup/vzdump-lxc-16112-2020_03_30-21_49_30.tar.gz',
+           },
+           {
+               'content' => 'backup',
+               'ctime'   => 1585604970,
+               'format'  => 'tar.zst',
+               'size'    => DEFAULT_SIZE,
+               'subtype' => 'lxc',
+               'vmid'    => '16112',
+               'volid'   => 'local:backup/vzdump-lxc-16112-2020_03_30-21_49_30.tar.zst',
+           },
+           {
+               'content' => 'backup',
+               'ctime'   => 1585605570,
+               'format'  => 'tgz',
+               'size'    => DEFAULT_SIZE,
+               'subtype' => 'lxc',
+               'vmid'    => '16112',
+               'volid'   => 'local:backup/vzdump-lxc-16112-2020_03_30-21_59_30.tgz',
+           },
+       ],
+    },
+    {
+       description => 'VMID: 16114, VM, qcow2, linked clone',
+       vmid => '16114',
+       files => [
+           "$storage_dir/images/16114/vm-16114-disk-0.qcow2",
+           "$storage_dir/images/16114/vm-16114-disk-1.qcow2",
+       ],
+       parent => [
+           "../9004/base-9004-disk-0.qcow2",
+           "../9004/base-9004-disk-1.qcow2",
+       ],
+       expected => [
+           {
+               'content' => 'images',
+               'ctime'   => DEFAULT_CTIME,
+               'format'  => 'qcow2',
+               'parent'  => '../9004/base-9004-disk-0.qcow2',
+               'size'    => DEFAULT_SIZE,
+               'used'    => DEFAULT_USED,
+               'vmid'    => '16114',
+               'volid'   => 'local:9004/base-9004-disk-0.qcow2/16114/vm-16114-disk-0.qcow2',
+           },
+           {
+               'content' => 'images',
+               'ctime'   => DEFAULT_CTIME,
+               'format'  => 'qcow2',
+               'parent'  => '../9004/base-9004-disk-1.qcow2',
+               'size'    => DEFAULT_SIZE,
+               'used'    => DEFAULT_USED,
+               'vmid'    => '16114',
+               'volid'   => 'local:9004/base-9004-disk-1.qcow2/16114/vm-16114-disk-1.qcow2',
+           },
+       ],
+    },
+    {
+       description => 'VMID: 9004, VM, template, qcow2',
+       vmid => '9004',
+       files => [
+           "$storage_dir/images/9004/base-9004-disk-0.qcow2",
+           "$storage_dir/images/9004/base-9004-disk-1.qcow2",
+       ],
+       expected => [
+           {
+               'content' => 'images',
+               'ctime'   => DEFAULT_CTIME,
+               'format'  => 'qcow2',
+               'parent'  => undef,
+               'size'    => DEFAULT_SIZE,
+               'used'    => DEFAULT_USED,
+               'vmid'    => '9004',
+               'volid'   => 'local:9004/base-9004-disk-0.qcow2',
+           },
+           {
+               'content' => 'images',
+               'ctime'   => DEFAULT_CTIME,
+               'format'  => 'qcow2',
+               'parent'  => undef,
+               'size'    => DEFAULT_SIZE,
+               'used'    => DEFAULT_USED,
+               'vmid'    => '9004',
+               'volid'   => 'local:9004/base-9004-disk-1.qcow2',
+           },
+       ],
+    },
+    {
+       description => 'VMID: none, templates, snippets, backup',
+       vmid => undef,
+       files => [
+           "$storage_dir/dump/vzdump-lxc-19253-2020_02_03-19_57_43.tar.gz",
+           "$storage_dir/dump/vzdump-lxc-19254-2019_01_21-19_29_19.tar",
+           "$storage_dir/template/iso/archlinux-2020.02.01-x86_64.iso",
+           "$storage_dir/template/iso/debian-8.11.1-amd64-DVD-1.iso",
+           "$storage_dir/template/iso/debian-9.12.0-amd64-netinst.iso",
+           "$storage_dir/template/iso/proxmox-ve_6.1-1.iso",
+           "$storage_dir/template/cache/archlinux-base_20190924-1_amd64.tar.gz",
+           "$storage_dir/template/cache/debian-10.0-standard_10.0-1_amd64.tar.gz",
+           "$storage_dir/template/cache/alpine-3.10-default_20190626_amd64.tar.xz",
+           "$storage_dir/snippets/userconfig.yaml",
+           "$storage_dir/snippets/hookscript.pl",
+           "$storage_dir/private/1234/", # fileparse needs / at the end
+           "$storage_dir/private/1234/subvol-1234-disk-0.subvol/", # fileparse needs / at the end
+       ],
+       expected => [
+           {
+               'content' => 'vztmpl',
+               'ctime'   => DEFAULT_CTIME,
+               'format'  => 'txz',
+               'size'    => DEFAULT_SIZE,
+               'volid'   => 'local:vztmpl/alpine-3.10-default_20190626_amd64.tar.xz',
+           },
+           {
+               'content' => 'vztmpl',
+               'ctime'   => DEFAULT_CTIME,
+               'format'  => 'tgz',
+               'size'    => DEFAULT_SIZE,
+               'volid'   => 'local:vztmpl/archlinux-base_20190924-1_amd64.tar.gz',
+           },
+           {
+               'content' => 'vztmpl',
+               'ctime'   => DEFAULT_CTIME,
+               'format'  => 'tgz',
+               'size'    => DEFAULT_SIZE,
+               'volid'   => 'local:vztmpl/debian-10.0-standard_10.0-1_amd64.tar.gz',
+           },
+           {
+               'content' => 'iso',
+               'ctime'   => DEFAULT_CTIME,
+               'format'  => 'iso',
+               'size'    => DEFAULT_SIZE,
+               'volid'   => 'local:iso/archlinux-2020.02.01-x86_64.iso',
+           },
+           {
+               'content' => 'iso',
+               'ctime'   => DEFAULT_CTIME,
+               'format'  => 'iso',
+               'size'    => DEFAULT_SIZE,
+               'volid'   => 'local:iso/debian-8.11.1-amd64-DVD-1.iso',
+           },
+           {
+               'content' => 'iso',
+               'ctime'   => DEFAULT_CTIME,
+               'format'  => 'iso',
+               'size'    => DEFAULT_SIZE,
+               'volid'   => 'local:iso/debian-9.12.0-amd64-netinst.iso',
+           },
+           {
+               'content' => 'iso',
+               'ctime'   => DEFAULT_CTIME,
+               'format'  => 'iso',
+               'size'    => DEFAULT_SIZE,
+               'volid'   => 'local:iso/proxmox-ve_6.1-1.iso',
+           },
+           {
+               'content' => 'backup',
+               'ctime'   => 1580759863,
+               'format'  => 'tar.gz',
+               'size'    => DEFAULT_SIZE,
+               'subtype' => 'lxc',
+               'vmid'    => '19253',
+               'volid'   => 'local:backup/vzdump-lxc-19253-2020_02_03-19_57_43.tar.gz',
+           },
+           {
+               'content' => 'backup',
+               'ctime'   => 1548098959,
+               'format'  => 'tar',
+               'size'    => DEFAULT_SIZE,
+               'subtype' => 'lxc',
+               'vmid'    => '19254',
+               'volid'   => 'local:backup/vzdump-lxc-19254-2019_01_21-19_29_19.tar',
+           },
+           {
+               'content' => 'snippets',
+               'ctime'   => DEFAULT_CTIME,
+               'format'  => 'snippet',
+               'size'    => DEFAULT_SIZE,
+               'volid'   => 'local:snippets/hookscript.pl',
+           },
+           {
+               'content' => 'snippets',
+               'ctime'   => DEFAULT_CTIME,
+               'format'  => 'snippet',
+               'size'    => DEFAULT_SIZE,
+               'volid'   => 'local:snippets/userconfig.yaml',
+           },
+       ],
+    },
+    {
+       description => 'VMID: none, parent, non-matching',
+       # string instead of vmid in folder
+       #"$storage_dir/images/ssss/base-4321-disk-0.qcow2/1234/vm-1234-disk-0.qcow2",
+       vmid => undef,
+       files => [
+           "$storage_dir/images/1234/vm-1234-disk-0.qcow2",
+       ],
+       parent => [
+           "../ssss/base-4321-disk-0.qcow2",
+       ],
+       expected => [
+           {
+               'content' => 'images',
+               'ctime'   => DEFAULT_CTIME,
+               'format'  => 'qcow2',
+               'parent'  => '../ssss/base-4321-disk-0.qcow2',
+               'size'    => DEFAULT_SIZE,
+               'used'    => DEFAULT_USED,
+               'vmid'    => '1234',
+               'volid'   => 'local:1234/vm-1234-disk-0.qcow2',
+           }
+       ],
+    },
+    {
+       description => 'VMID: none, non-matching',
+       # failed matches
+       vmid => undef,
+       files => [
+           "$storage_dir/images/ssss/base-4321-disk-0.raw",
+           "$storage_dir/images/ssss/vm-1234-disk-0.qcow2",
+           "$storage_dir/template/iso/yet-again-a-installation-disk.dvd",
+           "$storage_dir/template/cache/debian-10.0-standard_10.0-1_amd64.zip.gz",
+           "$storage_dir/template/cache/debian-10.0-standard_10.0-1_amd64.tar.bz2",
+           "$storage_dir/private/subvol-19254-disk-0/19254",
+           "$storage_dir/dump/vzdump-openvz-16112-2020_03_30-21_39_30.tar.bz2",
+           "$storage_dir/dump/vzdump-openvz-16112-2020_03_30-21_39_30.zip.gz",
+           "$storage_dir/dump/vzdump-openvz-16112-2020_03_30-21_39_30.tgz.lzo",
+           "$storage_dir/dump/vzdump-qemu-16110-2020_03_30-21_12_40.vma.xz",
+           "$storage_dir/dump/vzdump-qemu-16110-2020_03_30-21_12_40.vms.gz",
+       ],
+       expected => [], # returns empty list
+    },
+);
+
+
+# provide static vmlist for tests
+my $mock_cluster = Test::MockModule->new('PVE::Cluster', no_auto => 1);
+$mock_cluster->redefine(get_vmlist => sub { return $mocked_vmlist; });
+
+# populate is File::stat's method to fill all information from CORE::stat into
+# an blessed array.
+my $mock_stat = Test::MockModule->new('File::stat', no_auto => 1);
+$mock_stat->redefine(populate => sub {
+       my (@st) = @_;
+       $st[7] = DEFAULT_SIZE;
+       $st[10] = DEFAULT_CTIME;
+
+       my $result = $mock_stat->original('populate')->(@st);
+
+       return $result;
+});
+
+# override info provided by qemu-img in file_size_info
+my $mock_fsi = Test::MockModule->new('PVE::Storage::Plugin', no_auto => 1);
+$mock_fsi->redefine(file_size_info => sub {
+       my ($filename, $timeout) = @_;
+       my ($size, $format, $used, $parent, $ctime) = $mock_fsi->original('file_size_info')->($filename, $timeout);
+
+       $size = DEFAULT_SIZE;
+       $used = DEFAULT_USED;
+
+       return wantarray ? ($size, $format, $used, $parent, $ctime) : $size;
+});
+
+my $plan = scalar @tests;
+plan tests => $plan + 1;
+
+{
+    # don't accidentally modify vmlist, see bug report
+    # https://pve.proxmox.com/pipermail/pve-devel/2020-January/041096.html
+    my $scfg_with_type = { path => $storage_dir, type => 'dir' };
+    my $original_vmlist = { ids => {} };
+    my $tested_vmlist = dclone($original_vmlist);
+
+    PVE::Storage::Plugin->list_volumes('sid', $scfg_with_type, undef, ['images']);
+
+    is_deeply ($tested_vmlist, $original_vmlist,
+       'PVE::Cluster::vmlist remains unmodified')
+    || diag ("Expected vmlist to remain\n", explain($original_vmlist),
+       "but it turned to\n", explain($tested_vmlist));
+}
+
+
+{
+    my $sid = 'local';
+    my $types = [ 'rootdir', 'images', 'vztmpl', 'iso', 'backup', 'snippets' ];
+    my @suffixes = ( 'qcow2', 'raw', 'vmdk', 'vhdx' );
+
+    # run through test cases
+    foreach my $tt (@tests) {
+       my $vmid = $tt->{vmid};
+       my $files = $tt->{files};
+       my $expected = $tt->{expected};
+       my $description = $tt->{description};
+       my $parent = $tt->{parent};
+
+       # prepare environment
+       my $num = 0; #parent disks
+       for my $file (@$files) {
+           my ($name, $dir, $suffix) = fileparse($file, @suffixes);
+
+           make_path($dir, { verbose => 1, mode => 0755 });
+
+           if ($name) {
+               # using qemu-img to also be able to represent the backing device
+               my @cmd = ( '/usr/bin/qemu-img', 'create', "$file", DEFAULT_SIZE );
+               push @cmd, ( '-f', $suffix ) if $suffix;
+               push @cmd, ( '-u', '-b', @$parent[$num] ) if $parent;
+               push @cmd, ( '-F', $suffix ) if $parent && $suffix;
+               $num++;
+
+               run_command([@cmd]);
+           }
+       }
+
+       my $got;
+       eval { $got = PVE::Storage::Plugin->list_volumes($sid, $scfg, $vmid, $types) };
+       $got = $@ if $@;
+
+       is_deeply($got, $expected, $description) || diag(explain($got));
+
+       # clean up after each test case, otherwise
+       # we get wrong results from leftover files
+       remove_tree($storage_dir, { verbose => 1 });
+    }
+}
+
+done_testing();
+
+1;
diff --git a/src/test/parse_volname_test.pm b/src/test/parse_volname_test.pm
new file mode 100644 (file)
index 0000000..d6ac885
--- /dev/null
@@ -0,0 +1,253 @@
+package PVE::Storage::TestParseVolname;
+
+use strict;
+use warnings;
+
+use lib qw(..);
+
+use PVE::Storage;
+use Test::More;
+
+my $vmid = 1234;
+
+# an array of test cases, each test is comprised of the following keys:
+# description => to identify a single test
+# volname     => the input for parse_volname
+# expected    => the array that parse_volname returns
+my $tests = [
+    #
+    # VM images
+    #
+    {
+       description => 'VM disk image, linked, qcow2, vm- as base-',
+       volname     => "$vmid/vm-$vmid-disk-0.qcow2/$vmid/vm-$vmid-disk-0.qcow2",
+       expected    => [ 'images', "vm-$vmid-disk-0.qcow2", "$vmid", "vm-$vmid-disk-0.qcow2", "$vmid", undef, 'qcow2', ],
+    },
+    #
+    # iso
+    #
+    {
+       description => 'ISO image, iso',
+       volname     => 'iso/some-installation-disk.iso',
+       expected    => ['iso', 'some-installation-disk.iso'],
+    },
+    {
+       description => 'ISO image, img',
+       volname     => 'iso/some-other-installation-disk.img',
+       expected    => ['iso', 'some-other-installation-disk.img'],
+    },
+    #
+    # container templates
+    #
+    {
+       description => 'Container template tar.gz',
+       volname     => 'vztmpl/debian-10.0-standard_10.0-1_amd64.tar.gz',
+       expected    => ['vztmpl', 'debian-10.0-standard_10.0-1_amd64.tar.gz'],
+    },
+    {
+       description => 'Container template tar.xz',
+       volname     => 'vztmpl/debian-10.0-standard_10.0-1_amd64.tar.xz',
+       expected    => ['vztmpl', 'debian-10.0-standard_10.0-1_amd64.tar.xz'],
+    },
+    #
+    # container rootdir
+    #
+    {
+       description => 'Container rootdir, sub directory',
+       volname     => "rootdir/$vmid",
+       expected    => ['rootdir', "$vmid", "$vmid"],
+    },
+    {
+       description => 'Container rootdir, subvol',
+       volname     => "$vmid/subvol-$vmid-disk-0.subvol",
+       expected    => [ 'images', "subvol-$vmid-disk-0.subvol", "$vmid", undef, undef, undef, 'subvol' ],
+    },
+    {
+       description => 'Backup archive, no virtualization type',
+       volname     => "backup/vzdump-none-$vmid-2020_03_30-21_39_30.tar",
+       expected    => ['backup', "vzdump-none-$vmid-2020_03_30-21_39_30.tar"],
+    },
+    #
+    # Snippets
+    #
+    {
+       description => 'Snippets, yaml',
+       volname     => 'snippets/userconfig.yaml',
+       expected    => ['snippets', 'userconfig.yaml'],
+    },
+    {
+       description => 'Snippets, perl',
+       volname     => 'snippets/hookscript.pl',
+       expected    => ['snippets', 'hookscript.pl'],
+    },
+    #
+    # failed matches
+    #
+    {
+       description => "Failed match: VM disk image, base, raw",
+       volname     => "ssss/base-$vmid-disk-0.raw",
+       expected    => "unable to parse directory volume name 'ssss/base-$vmid-disk-0.raw'\n",
+    },
+    {
+       description => 'Failed match: ISO image, dvd',
+       volname     => 'iso/yet-again-a-installation-disk.dvd',
+       expected    => "unable to parse directory volume name 'iso/yet-again-a-installation-disk.dvd'\n",
+    },
+    {
+       description => 'Failed match: Container template, zip.gz',
+       volname     => 'vztmpl/debian-10.0-standard_10.0-1_amd64.zip.gz',
+       expected    => "unable to parse directory volume name 'vztmpl/debian-10.0-standard_10.0-1_amd64.zip.gz'\n",
+    },
+    {
+       description => 'Failed match: Container template, tar.bz2',
+       volname     => 'vztmpl/debian-10.0-standard_10.0-1_amd64.tar.bz2',
+       expected    => "unable to parse directory volume name 'vztmpl/debian-10.0-standard_10.0-1_amd64.tar.bz2'\n",
+    },
+    {
+       description => 'Failed match: Container rootdir, subvol',
+       volname     => "rootdir/subvol-$vmid-disk-0",
+       expected    => "unable to parse directory volume name 'rootdir/subvol-$vmid-disk-0'\n",
+    },
+    {
+       description => 'Failed match: VM disk image, linked, vhdx',
+       volname     => "$vmid/base-$vmid-disk-0.vhdx/$vmid/vm-$vmid-disk-0.vhdx",
+       expected    => "unable to parse volume filename 'base-$vmid-disk-0.vhdx'\n",
+    },
+    {
+       description => 'Failed match: VM disk image, linked, qcow2, first vmid',
+       volname     => "ssss/base-$vmid-disk-0.qcow2/$vmid/vm-$vmid-disk-0.qcow2",
+       expected    => "unable to parse directory volume name 'ssss/base-$vmid-disk-0.qcow2/$vmid/vm-$vmid-disk-0.qcow2'\n",
+    },
+    {
+       description => 'Failed match: VM disk image, linked, qcow2, second vmid',
+       volname     => "$vmid/base-$vmid-disk-0.qcow2/ssss/vm-$vmid-disk-0.qcow2",
+       expected    => "unable to parse volume filename 'base-$vmid-disk-0.qcow2/ssss/vm-$vmid-disk-0.qcow2'\n",
+    },
+];
+
+# create more test cases for VM disk images matches
+my $disk_suffix = [ 'raw', 'qcow2', 'vmdk' ];
+foreach my $s (@$disk_suffix) {
+    my @arr = (
+       {
+           description => "VM disk image, $s",
+           volname     => "$vmid/vm-$vmid-disk-1.$s",
+           expected    => [
+               'images',
+               "vm-$vmid-disk-1.$s",
+               "$vmid",
+               undef,
+               undef,
+               undef,
+               "$s",
+           ],
+       },
+       {
+           description => "VM disk image, linked, $s",
+           volname     => "$vmid/base-$vmid-disk-0.$s/$vmid/vm-$vmid-disk-0.$s",
+           expected    => [
+               'images',
+               "vm-$vmid-disk-0.$s",
+               "$vmid",
+               "base-$vmid-disk-0.$s",
+               "$vmid",
+               undef,
+               "$s",
+           ],
+       },
+       {
+           description => "VM disk image, base, $s",
+           volname     => "$vmid/base-$vmid-disk-0.$s",
+           expected    => [
+               'images',
+               "base-$vmid-disk-0.$s",
+               "$vmid",
+               undef,
+               undef,
+               'base-',
+               "$s"
+           ],
+       },
+    );
+
+    push @$tests, @arr;
+}
+
+
+# create more test cases for backup files matches
+my $bkp_suffix = {
+    qemu   => [ 'vma', 'vma.gz', 'vma.lzo', 'vma.zst' ],
+    lxc    => [ 'tar', 'tgz', 'tar.gz', 'tar.lzo', 'tar.zst' ],
+    openvz => [ 'tar', 'tgz', 'tar.gz', 'tar.lzo', 'tar.zst' ],
+};
+
+foreach my $virt (keys %$bkp_suffix) {
+    my $suffix = $bkp_suffix->{$virt};
+    foreach my $s (@$suffix) {
+       my @arr = (
+           {
+               description => "Backup archive, $virt, $s",
+               volname     => "backup/vzdump-$virt-$vmid-2020_03_30-21_12_40.$s",
+               expected    => [
+                   'backup',
+                   "vzdump-$virt-$vmid-2020_03_30-21_12_40.$s",
+                   "$vmid"
+               ],
+           },
+       );
+
+       push @$tests, @arr;
+    }
+}
+
+
+# create more test cases for failed backup files matches
+my $non_bkp_suffix = {
+    qemu   => [ 'vms.gz', 'vma.xz' ],
+    lxc    => [ 'tar.bz2', 'zip.gz', 'tgz.lzo' ],
+};
+foreach my $virt (keys %$non_bkp_suffix) {
+    my $suffix = $non_bkp_suffix->{$virt};
+    foreach my $s (@$suffix) {
+       my @arr = (
+           {
+               description => "Failed match: Backup archive, $virt, $s",
+               volname     => "backup/vzdump-$virt-$vmid-2020_03_30-21_12_40.$s",
+               expected    => "unable to parse directory volume name 'backup/vzdump-$virt-$vmid-2020_03_30-21_12_40.$s'\n",
+           },
+       );
+
+       push @$tests, @arr;
+    }
+}
+
+
+#
+# run through test case array
+#
+plan tests => scalar @$tests + 1;
+
+my $seen_vtype;
+my $vtype_subdirs = { map { $_ => 1 } keys %{ PVE::Storage::Plugin::get_vtype_subdirs() } };
+
+foreach my $t (@$tests) {
+    my $description = $t->{description};
+    my $volname = $t->{volname};
+    my $expected = $t->{expected};
+
+    my $got;
+    eval { $got = [ PVE::Storage::Plugin->parse_volname($volname) ] };
+    $got = $@ if $@;
+
+    is_deeply($got, $expected, $description);
+
+    $seen_vtype->{@$expected[0]} = 1 if ref $expected eq 'ARRAY';
+}
+
+# to check if all $vtype_subdirs are defined in path_to_volume_id
+# or have a test
+is_deeply($seen_vtype, $vtype_subdirs, "vtype_subdir check");
+
+done_testing();
+
+1;
diff --git a/src/test/path_to_volume_id_test.pm b/src/test/path_to_volume_id_test.pm
new file mode 100644 (file)
index 0000000..8149c88
--- /dev/null
@@ -0,0 +1,275 @@
+package PVE::Storage::TestPathToVolumeId;
+
+use strict;
+use warnings;
+
+use lib qw(..);
+
+use PVE::Storage;
+
+use Test::More;
+
+use Cwd;
+use File::Basename;
+use File::Path qw(make_path remove_tree);
+use File::Temp;
+
+my $storage_dir = File::Temp->newdir();
+my $scfg = {
+    'digest' => 'd29306346b8b25b90a4a96165f1e8f52d1af1eda',
+    'ids'    => {
+       'local' => {
+           'shared'   => 0,
+           'path'     => "$storage_dir",
+           'type'     => 'dir',
+           'maxfiles' => 0,
+           'content'  => {
+               'snippets' => 1,
+               'rootdir'  => 1,
+               'images'   => 1,
+               'iso'      => 1,
+               'backup'   => 1,
+               'vztmpl'   => 1,
+           },
+       },
+    },
+    'order' => {
+       'local' => 1,
+    },
+};
+
+# the tests array consists of hashes with the following keys:
+# description => to identify the test case
+# volname     => to create the test file
+# expected    => the result that path_to_volume_id should return
+my @tests = (
+    {
+       description => 'Image, qcow2',
+       volname     => "$storage_dir/images/16110/vm-16110-disk-0.qcow2",
+       expected    => [
+           'images',
+           'local:16110/vm-16110-disk-0.qcow2',
+       ],
+    },
+    {
+       description => 'Image, raw',
+       volname     => "$storage_dir/images/16112/vm-16112-disk-0.raw",
+       expected    => [
+           'images',
+           'local:16112/vm-16112-disk-0.raw',
+       ],
+    },
+    {
+       description => 'Image template, qcow2',
+       volname     => "$storage_dir/images/9004/base-9004-disk-0.qcow2",
+       expected    => [
+           'images',
+           'local:9004/base-9004-disk-0.qcow2',
+       ],
+    },
+
+    {
+       description => 'Backup, vma.gz',
+       volname     => "$storage_dir/dump/vzdump-qemu-16110-2020_03_30-21_11_40.vma.gz",
+       expected    => [
+           'backup',
+           'local:backup/vzdump-qemu-16110-2020_03_30-21_11_40.vma.gz',
+       ],
+    },
+    {
+       description => 'Backup, vma.lzo',
+       volname     => "$storage_dir/dump/vzdump-qemu-16110-2020_03_30-21_12_45.vma.lzo",
+       expected    => [
+           'backup',
+           'local:backup/vzdump-qemu-16110-2020_03_30-21_12_45.vma.lzo',
+       ],
+    },
+    {
+       description => 'Backup, vma',
+       volname     => "$storage_dir/dump/vzdump-qemu-16110-2020_03_30-21_13_55.vma",
+       expected    => [
+           'backup',
+           'local:backup/vzdump-qemu-16110-2020_03_30-21_13_55.vma',
+       ],
+    },
+    {
+       description => 'Backup, tar.lzo',
+       volname     => "$storage_dir/dump/vzdump-lxc-16112-2020_03_30-21_39_30.tar.lzo",
+       expected    => [
+           'backup',
+           'local:backup/vzdump-lxc-16112-2020_03_30-21_39_30.tar.lzo',
+       ],
+    },
+    {
+       description => 'Backup, vma.zst',
+       volname     => "$storage_dir/dump/vzdump-qemu-16110-2020_03_30-21_13_55.vma.zst",
+       expected    => [
+           'backup',
+           'local:backup/vzdump-qemu-16110-2020_03_30-21_13_55.vma.zst'
+       ],
+    },
+    {
+       description => 'Backup, tar.zst',
+       volname     => "$storage_dir/dump/vzdump-lxc-16112-2020_03_30-21_39_30.tar.zst",
+       expected    => [
+           'backup',
+           'local:backup/vzdump-lxc-16112-2020_03_30-21_39_30.tar.zst'
+       ],
+    },
+
+    {
+       description => 'ISO file',
+       volname     => "$storage_dir/template/iso/yet-again-a-installation-disk.iso",
+       expected    => [
+           'iso',
+           'local:iso/yet-again-a-installation-disk.iso',
+       ],
+    },
+    {
+       description => 'CT template, tar.gz',
+       volname     => "$storage_dir/template/cache/debian-10.0-standard_10.0-1_amd64.tar.gz",
+       expected    => [
+           'vztmpl',
+           'local:vztmpl/debian-10.0-standard_10.0-1_amd64.tar.gz',
+       ],
+    },
+
+    {
+       description => 'Rootdir',
+       volname     => "$storage_dir/private/1234/", # fileparse needs / at the end
+       expected    => [
+           'rootdir',
+           'local:rootdir/1234',
+       ],
+    },
+    {
+       description => 'Rootdir, folder subvol',
+       volname     => "$storage_dir/images/1234/subvol-1234-disk-0.subvol/", # fileparse needs / at the end
+       expected    => [
+           'images',
+           'local:1234/subvol-1234-disk-0.subvol'
+       ],
+    },
+    {
+       description => 'Snippets, yaml',
+       volname => "$storage_dir/snippets/userconfig.yaml",
+       expected => [
+           'snippets',
+           'local:snippets/userconfig.yaml',
+       ],
+    },
+    {
+       description => 'Snippets, hookscript',
+       volname     => "$storage_dir/snippets/hookscript.pl",
+       expected    => [
+           'snippets',
+           'local:snippets/hookscript.pl',
+       ],
+    },
+    {
+       description => 'CT template, tar.xz',
+       volname     => "$storage_dir/template/cache/debian-10.0-standard_10.0-1_amd64.tar.xz",
+       expected    => [
+           'vztmpl',
+           'local:vztmpl/debian-10.0-standard_10.0-1_amd64.tar.xz',
+       ],
+    },
+
+    # no matches, path or files with failures
+    {
+       description => 'Base template, string as vmid in folder name',
+       volname     => "$storage_dir/images/ssss/base-4321-disk-0.raw",
+       expected    => [''],
+    },
+    {
+       description => 'ISO file, wrong ending',
+       volname     => "$storage_dir/template/iso/yet-again-a-installation-disk.dvd",
+       expected    => [''],
+    },
+    {
+       description => 'CT template, wrong ending, zip.gz',
+       volname     => "$storage_dir/template/cache/debian-10.0-standard_10.0-1_amd64.zip.gz",
+       expected    => [''],
+    },
+    {
+       description => 'CT template, wrong ending, tar bz2',
+       volname     => "$storage_dir/template/cache/debian-10.0-standard_10.0-1_amd64.tar.bz2",
+       expected    => [''],
+    },
+    {
+       description => 'Rootdir as subvol, wrong path',
+       volname     => "$storage_dir/private/subvol-19254-disk-0/",
+       expected    => [''],
+    },
+    {
+       description => 'Backup, wrong ending, openvz, tar.bz2',
+       volname     => "$storage_dir/dump/vzdump-openvz-16112-2020_03_30-21_39_30.tar.bz2",
+       expected    => [''],
+    },
+    {
+       description => 'Backup, wrong format, openvz, zip.gz',
+       volname     => "$storage_dir/dump/vzdump-openvz-16112-2020_03_30-21_39_30.zip.gz",
+       expected    => [''],
+    },
+    {
+       description => 'Backup, wrong format, openvz, tgz.lzo',
+       volname     => "$storage_dir/dump/vzdump-openvz-16112-2020_03_30-21_39_30.tgz.lzo",
+       expected    => [''],
+    },
+    {
+       description => 'Backup, wrong ending, qemu, vma.xz',
+       volname     => "$storage_dir/dump/vzdump-qemu-16110-2020_03_30-21_12_40.vma.xz",
+       expected    => [''],
+    },
+    {
+       description => 'Backup, wrong format, qemu, vms.gz',
+       volname     => "$storage_dir/dump/vzdump-qemu-16110-2020_03_30-21_12_40.vms.gz",
+       expected    => [''],
+    },
+    {
+       description => 'Image, string as vmid in folder name',
+       volname     => "$storage_dir/images/ssss/vm-1234-disk-0.qcow2",
+       expected    => [''],
+    },
+);
+
+plan tests => scalar @tests + 1;
+
+my $seen_vtype;
+my $vtype_subdirs = { map { $_ => 1 } keys %{ PVE::Storage::Plugin::get_vtype_subdirs() } };
+
+foreach my $tt (@tests) {
+    my $file = $tt->{volname};
+    my $expected = $tt->{expected};
+    my $description = $tt->{description};
+
+    # prepare environment
+    my ($name, $dir, $suffix) = fileparse($file);
+    make_path($dir, { verbose => 1, mode => 0755 });
+
+    if ($name) {
+       open(my $fh, ">>", "$file") || die "Error open file: $!";
+       close($fh);
+    }
+
+    # run tests
+    my $got;
+    eval { $got = [ PVE::Storage::path_to_volume_id($scfg, $file) ] };
+    $got = $@ if $@;
+
+    is_deeply($got, $expected, $description) || diag(explain($got));
+
+    $seen_vtype->{@$expected[0]} = 1
+       if ( @$expected[0] ne '' && scalar @$expected > 1);
+}
+
+# to check if all $vtype_subdirs are defined in path_to_volume_id
+# or have a test
+is_deeply($seen_vtype, $vtype_subdirs, "vtype_subdir check");
+
+#cleanup
+# File::Temp unlinks tempdir on exit
+
+done_testing();
+
+1;
diff --git a/src/test/prune_backups_test.pm b/src/test/prune_backups_test.pm
new file mode 100644 (file)
index 0000000..b57d280
--- /dev/null
@@ -0,0 +1,492 @@
+package PVE::Storage::TestPruneBackups;
+
+use strict;
+use warnings;
+
+use lib qw(..);
+
+use PVE::Storage;
+use Test::More;
+use Test::MockModule;
+
+my $storeid = 'BackTest123';
+my @vmids = (1234, 9001);
+
+# only includes the information needed for prune_backups
+my $mocked_backups_lists = {};
+
+my $basetime = 1577881101; # 2020_01_01-12_18_21 UTC
+
+foreach my $vmid (@vmids) {
+    push @{$mocked_backups_lists->{default}}, (
+       {
+           'volid' => "$storeid:backup/vzdump-qemu-$vmid-2018_05_26-11_18_21.tar.zst",
+           'ctime' => $basetime - 585*24*60*60 - 60*60,
+           'vmid'  => $vmid,
+       },
+       {
+           'volid' => "$storeid:backup/vzdump-qemu-$vmid-2019_12_31-11_18_21.tar.zst",
+           'ctime' => $basetime - 24*60*60 - 60*60,
+           'vmid'  => $vmid,
+       },
+       {
+           'volid' => "$storeid:backup/vzdump-qemu-$vmid-2019_12_31-11_18_51.tar.zst",
+           'ctime' => $basetime - 24*60*60 - 60*60 + 30,
+           'vmid'  => $vmid,
+           'protected' => 1,
+       },
+       {
+           'volid' => "$storeid:backup/vzdump-qemu-$vmid-2019_12_31-11_19_21.tar.zst",
+           'ctime' => $basetime - 24*60*60 - 60*60 + 60,
+           'vmid'  => $vmid,
+       },
+       {
+           'volid' => "$storeid:backup/vzdump-qemu-$vmid-2020_01_01-11_18_21.tar.zst",
+           'ctime' => $basetime - 60*60,
+           'vmid'  => $vmid,
+       },
+       {
+           'volid' => "$storeid:backup/vzdump-qemu-$vmid-2020_01_01-12_18_21.tar.zst",
+           'ctime' => $basetime,
+           'vmid'  => $vmid,
+       },
+       {
+           'volid' => "$storeid:backup/vzdump-lxc-$vmid-2020_01_01-12_18_21.tar.zst",
+           'ctime' => $basetime,
+           'vmid'  => $vmid,
+       },
+       {
+           'volid' => "$storeid:backup/vzdump-$vmid-renamed.tar.zst",
+           'ctime' => 1234,
+           'vmid'  => $vmid,
+       },
+    );
+}
+push @{$mocked_backups_lists->{year1970}}, (
+    {
+       'volid' => "$storeid:backup/vzdump-lxc-321-1970_01_01-00_01_23.tar.zst",
+       'ctime' => 83,
+       'vmid'  => 321,
+    },
+    {
+       'volid' => "$storeid:backup/vzdump-lxc-321-2070_01_01-00_01_00.tar.zst",
+       'ctime' => 60*60*24 * (365*100 + 25) + 60,
+       'vmid'  => 321,
+    },
+);
+push @{$mocked_backups_lists->{novmid}}, (
+    {
+       'volid' => "$storeid:backup/vzdump-lxc-novmid.tar.gz",
+       'ctime' => 1234,
+    },
+);
+push @{$mocked_backups_lists->{threeway}}, (
+    {
+       'volid' => "$storeid:backup/vzdump-qemu-7654-2019_12_25-12_18_21.tar.zst",
+       'ctime' => $basetime - 7*24*60*60,
+       'vmid'  => 7654,
+    },
+    {
+       'volid' => "$storeid:backup/vzdump-qemu-7654-2019_12_31-12_18_21.tar.zst",
+       'ctime' => $basetime - 24*60*60,
+       'vmid'  => 7654,
+    },
+    {
+       'volid' => "$storeid:backup/vzdump-qemu-7654-2020_01_01-12_18_21.tar.zst",
+       'ctime' => $basetime,
+       'vmid'  => 7654,
+    },
+);
+push @{$mocked_backups_lists->{weekboundary}}, (
+    {
+       'volid' => "$storeid:backup/vzdump-qemu-7654-2020_12_03-12_18_21.tar.zst",
+       'ctime' => $basetime + (366-31+2)*24*60*60,
+       'vmid'  => 7654,
+    },
+    {
+       'volid' => "$storeid:backup/vzdump-qemu-7654-2020_12_04-12_18_21.tar.zst",
+       'ctime' => $basetime + (366-31+3)*24*60*60,
+       'vmid'  => 7654,
+    },
+    {
+       'volid' => "$storeid:backup/vzdump-qemu-7654-2020_12_07-12_18_21.tar.zst",
+       'ctime' => $basetime + (366-31+6)*24*60*60,
+       'vmid'  => 7654,
+    },
+);
+my $current_list;
+my $mock_plugin = Test::MockModule->new('PVE::Storage::Plugin');
+$mock_plugin->redefine(list_volumes => sub {
+    my ($class, $storeid, $scfg, $vmid, $content_types) = @_;
+
+    my $list = $mocked_backups_lists->{$current_list};
+
+    return $list if !defined($vmid);
+
+    return [ grep { $_->{vmid} eq $vmid } @{$list} ];
+});
+
+sub generate_expected {
+    my ($vmids, $type, $marks) = @_;
+
+    my @expected;
+    foreach my $vmid (@{$vmids}) {
+       push @expected, (
+           {
+               'volid' => "$storeid:backup/vzdump-qemu-$vmid-2018_05_26-11_18_21.tar.zst",
+               'type'  => 'qemu',
+               'ctime' => $basetime - 585*24*60*60 - 60*60,
+               'mark'  => $marks->[0],
+               'vmid'  => $vmid,
+           },
+           {
+               'volid' => "$storeid:backup/vzdump-qemu-$vmid-2019_12_31-11_18_21.tar.zst",
+               'type'  => 'qemu',
+               'ctime' => $basetime - 24*60*60 - 60*60,
+               'mark'  => $marks->[1],
+               'vmid'  => $vmid,
+           },
+           {
+               'volid' => "$storeid:backup/vzdump-qemu-$vmid-2019_12_31-11_18_51.tar.zst",
+               'type'  => 'qemu',
+               'ctime' => $basetime - 24*60*60 - 60*60 + 30,
+               'mark'  => 'protected',
+               'vmid'  => $vmid,
+           },
+           {
+               'volid' => "$storeid:backup/vzdump-qemu-$vmid-2019_12_31-11_19_21.tar.zst",
+               'type'  => 'qemu',
+               'ctime' => $basetime - 24*60*60 - 60*60 + 60,
+               'mark'  => $marks->[2],
+               'vmid'  => $vmid,
+           },
+           {
+               'volid' => "$storeid:backup/vzdump-qemu-$vmid-2020_01_01-11_18_21.tar.zst",
+               'type'  => 'qemu',
+               'ctime' => $basetime - 60*60,
+               'mark'  => $marks->[3],
+               'vmid'  => $vmid,
+           },
+           {
+               'volid' => "$storeid:backup/vzdump-qemu-$vmid-2020_01_01-12_18_21.tar.zst",
+               'type'  => 'qemu',
+               'ctime' => $basetime,
+               'mark'  => $marks->[4],
+               'vmid'  => $vmid,
+           },
+       ) if !defined($type) || $type eq 'qemu';
+       push @expected, (
+           {
+               'volid' => "$storeid:backup/vzdump-lxc-$vmid-2020_01_01-12_18_21.tar.zst",
+               'type'  => 'lxc',
+               'ctime' => $basetime,
+               'mark'  => $marks->[5],
+               'vmid'  => $vmid,
+           },
+       ) if !defined($type) || $type eq 'lxc';
+       push @expected, (
+           {
+               'volid' => "$storeid:backup/vzdump-$vmid-renamed.tar.zst",
+               'type'  => 'unknown',
+               'ctime' => 1234,
+               'mark'  => 'renamed',
+               'vmid'  => $vmid,
+           },
+       ) if !defined($type);
+    }
+    return [ sort { $a->{volid} cmp $b->{volid} } @expected ];
+}
+
+# an array of test cases, each test is comprised of the following keys:
+# description   => to identify a single test
+# vmid          => VMID or undef for all
+# type          => 'qemu' or 'lxc' or undef for all
+# keep          => options describing what to keep
+# list          => backups list to use. defaults to 'default'
+# expected      => what prune_backups should return
+#
+# most of them are created further below
+my $tests = [
+    {
+       description => 'last=3, multiple IDs',
+       keep => {
+           'keep-last' => 3,
+       },
+       expected => generate_expected(\@vmids, undef, ['remove', 'remove', 'keep', 'keep', 'keep', 'keep']),
+    },
+    {
+       description => 'weekly=2, one ID',
+       vmid => $vmids[0],
+       keep => {
+           'keep-weekly' => 2,
+       },
+       expected => generate_expected([$vmids[0]], undef, ['keep', 'remove', 'remove', 'remove', 'keep', 'keep']),
+    },
+    {
+       description => 'daily=weekly=monthly=1, multiple IDs',
+       keep => {
+           'keep-hourly' => 0,
+           'keep-daily' => 1,
+           'keep-weekly' => 1,
+           'keep-monthly' => 1,
+       },
+       expected => generate_expected(\@vmids, undef, ['keep', 'remove', 'keep', 'remove', 'keep', 'keep']),
+    },
+    {
+       description => 'hourly=4, one ID',
+       vmid => $vmids[0],
+       keep => {
+           'keep-hourly' => 4,
+           'keep-daily' => 0,
+       },
+       expected => generate_expected([$vmids[0]], undef, ['keep', 'remove', 'keep', 'keep', 'keep', 'keep']),
+    },
+    {
+       description => 'yearly=2, multiple IDs',
+       keep => {
+           'keep-hourly' => 0,
+           'keep-daily' => 0,
+           'keep-weekly' => 0,
+           'keep-monthly' => 0,
+           'keep-yearly' => 2,
+       },
+       expected => generate_expected(\@vmids, undef, ['remove', 'remove', 'keep', 'remove', 'keep', 'keep']),
+    },
+    {
+       description => 'last=2,hourly=2 one ID',
+       vmid => $vmids[0],
+       keep => {
+           'keep-last' => 2,
+           'keep-hourly' => 2,
+       },
+       expected => generate_expected([$vmids[0]], undef, ['keep', 'remove', 'keep', 'keep', 'keep', 'keep']),
+    },
+    {
+       description => 'last=1,monthly=2, multiple IDs',
+       keep => {
+           'keep-last' => 1,
+           'keep-monthly' => 2,
+       },
+       expected => generate_expected(\@vmids, undef, ['keep', 'remove', 'keep', 'remove', 'keep', 'keep']),
+    },
+    {
+       description => 'monthly=3, one ID',
+       vmid => $vmids[0],
+       keep => {
+           'keep-monthly' => 3,
+       },
+       expected => generate_expected([$vmids[0]], undef, ['keep', 'remove', 'keep', 'remove', 'keep', 'keep']),
+    },
+    {
+       description => 'last=daily=weekly=1, multiple IDs',
+       keep => {
+           'keep-last' => 1,
+           'keep-daily' => 1,
+           'keep-weekly' => 1,
+       },
+       expected => generate_expected(\@vmids, undef, ['keep', 'remove', 'keep', 'remove', 'keep', 'keep']),
+    },
+    {
+       description => 'last=daily=weekly=1, others zero, multiple IDs',
+       keep => {
+           'keep-hourly' => 0,
+           'keep-last' => 1,
+           'keep-daily' => 1,
+           'keep-weekly' => 1,
+           'keep-monthly' => 0,
+           'keep-yearly' => 0,
+       },
+       expected => generate_expected(\@vmids, undef, ['keep', 'remove', 'keep', 'remove', 'keep', 'keep']),
+    },
+    {
+       description => 'daily=2, one ID',
+       vmid => $vmids[0],
+       keep => {
+           'keep-daily' => 2,
+       },
+       expected => generate_expected([$vmids[0]], undef, ['remove', 'remove', 'keep', 'remove', 'keep', 'keep']),
+    },
+    {
+       description => 'weekly=monthly=1, multiple IDs',
+       keep => {
+           'keep-weekly' => 1,
+           'keep-monthly' => 1,
+       },
+       expected => generate_expected(\@vmids, undef, ['keep', 'remove', 'remove', 'remove', 'keep', 'keep']),
+    },
+    {
+       description => 'weekly=yearly=1, one ID',
+       vmid => $vmids[0],
+       keep => {
+           'keep-weekly' => 1,
+           'keep-yearly' => 1,
+       },
+       expected => generate_expected([$vmids[0]], undef, ['keep', 'remove', 'remove', 'remove', 'keep', 'keep']),
+    },
+    {
+       description => 'weekly=yearly=1, one ID, type qemu',
+       vmid => $vmids[0],
+       type => 'qemu',
+       keep => {
+           'keep-weekly' => 1,
+           'keep-yearly' => 1,
+       },
+       expected => generate_expected([$vmids[0]], 'qemu', ['keep', 'remove', 'remove', 'remove', 'keep', '']),
+    },
+    {
+       description => 'week=yearly=1, one ID, type lxc',
+       vmid => $vmids[0],
+       type => 'lxc',
+       keep => {
+           'keep-last' => 1,
+       },
+       expected => generate_expected([$vmids[0]], 'lxc', ['', '', '', '', '', 'keep']),
+    },
+    {
+       description => 'yearly=1, year before 2000',
+       keep => {
+           'keep-yearly' => 1,
+       },
+       list => 'year1970',
+       expected => [
+           {
+               'volid' => "$storeid:backup/vzdump-lxc-321-1970_01_01-00_01_23.tar.zst",
+               'ctime' => 83,
+               'mark'  => 'remove',
+               'type'  => 'lxc',
+               'vmid'  => 321,
+           },
+           {
+               'volid' => "$storeid:backup/vzdump-lxc-321-2070_01_01-00_01_00.tar.zst",
+               'ctime' => 60*60*24 * (365*100 + 25) + 60,
+               'mark'  => 'keep',
+               'type'  => 'lxc',
+               'vmid'  => 321,
+           },
+       ],
+    },
+    {
+       description => 'last=1, ne ID, year before 2000',
+       keep => {
+           'keep-last' => 1,
+       },
+       list => 'novmid',
+       expected => [
+           {
+               'volid' => "$storeid:backup/vzdump-lxc-novmid.tar.gz",
+               'ctime' => 1234,
+               'mark'  => 'renamed',
+               'type'  => 'lxc',
+           },
+       ],
+    },
+    {
+       description => 'all missing, multiple IDs',
+       keep => {},
+       expected => generate_expected(\@vmids, undef, ['keep', 'keep', 'keep', 'keep', 'keep', 'keep']),
+    },
+    {
+       description => 'all zero, multiple IDs',
+       keep => {
+           'keep-last' => 0,
+           'keep-hourly' => 0,
+           'keep-daily' => 0,
+           'keep-weekly' => 0,
+           'keep-monthyl' => 0,
+           'keep-yearly' => 0,
+       },
+       expected => generate_expected(\@vmids, undef, ['keep', 'keep', 'keep', 'keep', 'keep', 'keep']),
+    },
+    {
+       description => 'some zero, some missing, multiple IDs',
+       keep => {
+           'keep-last' => 0,
+           'keep-hourly' => 0,
+           'keep-daily' => 0,
+           'keep-monthyl' => 0,
+           'keep-yearly' => 0,
+       },
+       expected => generate_expected(\@vmids, undef, ['keep', 'keep', 'keep', 'keep', 'keep', 'keep']),
+    },
+    {
+       description => 'daily=weekly=monthly=1',
+       keep => {
+           'keep-daily' => 1,
+           'keep-weekly' => 1,
+           'keep-monthly' => 1,
+       },
+       list => 'threeway',
+       expected => [
+           {
+               'volid' => "$storeid:backup/vzdump-qemu-7654-2019_12_25-12_18_21.tar.zst",
+               'ctime' => $basetime - 7*24*60*60,
+               'type'  => 'qemu',
+               'vmid'  => 7654,
+               'mark'  => 'keep',
+           },
+           {
+               'volid' => "$storeid:backup/vzdump-qemu-7654-2019_12_31-12_18_21.tar.zst",
+               'ctime' => $basetime - 24*60*60,
+               'type'  => 'qemu',
+               'vmid'  => 7654,
+               'mark'  => 'remove', # month is already covered by the backup kept by keep-weekly!
+           },
+           {
+               'volid' => "$storeid:backup/vzdump-qemu-7654-2020_01_01-12_18_21.tar.zst",
+               'ctime' => $basetime,
+               'type'  => 'qemu',
+               'vmid'  => 7654,
+               'mark'  => 'keep',
+           },
+       ],
+    },
+    {
+       description => 'daily=weekly=1,weekboundary',
+       keep => {
+           'keep-daily' => 1,
+           'keep-weekly' => 1,
+       },
+       list => 'weekboundary',
+       expected => [
+           {
+               'volid' => "$storeid:backup/vzdump-qemu-7654-2020_12_03-12_18_21.tar.zst",
+               'ctime' => $basetime + (366-31+2)*24*60*60,
+               'type'  => 'qemu',
+               'vmid'  => 7654,
+               'mark'  => 'remove',
+           },
+           {
+               'volid' => "$storeid:backup/vzdump-qemu-7654-2020_12_04-12_18_21.tar.zst",
+               'ctime' => $basetime + (366-31+3)*24*60*60,
+               'type'  => 'qemu',
+               'vmid'  => 7654,
+               'mark'  => 'keep',
+           },
+           {
+               'volid' => "$storeid:backup/vzdump-qemu-7654-2020_12_07-12_18_21.tar.zst",
+               'ctime' => $basetime + (366-31+6)*24*60*60,
+               'type'  => 'qemu',
+               'vmid'  => 7654,
+               'mark'  => 'keep',
+           },
+       ],
+    },
+];
+
+plan tests => scalar @$tests;
+
+for my $tt (@$tests) {
+
+    my $got = eval {
+       $current_list = $tt->{list} // 'default';
+       my $res = PVE::Storage::Plugin->prune_backups($tt->{scfg}, $storeid, $tt->{keep}, $tt->{vmid}, $tt->{type}, 1);
+       return [ sort { $a->{volid} cmp $b->{volid} } @{$res} ];
+    };
+    $got = $@ if $@;
+
+    is_deeply($got, $tt->{expected}, $tt->{description}) || diag(explain($got));
+}
+
+done_testing();
+
+1;
diff --git a/src/test/rbd_namespace.pl b/src/test/rbd_namespace.pl
new file mode 100755 (executable)
index 0000000..6b115ce
--- /dev/null
@@ -0,0 +1,379 @@
+#!/usr/bin/perl
+
+# This script is meant to be run manually on hyperconverged PVE server with a
+# Ceph cluster. It tests how PVE handles RBD namespaces.
+#
+# The pool (default: rbd) must already exist. The namespace and VMs will be
+# created.
+#
+# Parameters like names for the pool an namespace and the VMID can be
+# configured.  The VMIDs for the clones is $vmid -1 and $vmid -2.
+#
+# Cleanup is done after a successful run. Cleanup can also be called manually.
+#
+# Known issues:
+#
+# * Snapshot rollback can sometimes be racy with stopping the VM and Ceph
+#  recognizing that the disk image is not in use anymore.
+
+use strict;
+use warnings;
+
+use Test::More;
+use Getopt::Long;
+use JSON;
+
+use PVE::Tools qw(run_command);
+
+my $pool = "testpool";
+my $use_existing= undef;
+my $namespace = "testspace";
+my $showhelp = '';
+my $vmid = 999999;
+my $cleanup = undef;
+my $DEBUG = 0;
+
+my $helpstring = "usage: $0 [OPTIONS]
+
+Known options are:
+
+ --pool <name>         pool name, default: ${pool}
+ --use-existing                use existing pool, default: 0, needs --pool set
+ --namespace <name>    rbd namespace, default: ${namespace}
+ --vmid <id>           VMID of the test VM, default: ${vmid}
+ --cleanup             Remove the storage definitions, namespaces and VM afterwards
+ -d, --debug           Enable debug output
+ -h, --help            Print this help message
+";
+
+GetOptions (
+    "pool=s" => \$pool,
+    "use-existing" => \$use_existing,
+    "namespace=s" => \$namespace,
+    "vmid=i" => \$vmid,
+    "h|help" => \$showhelp,
+    "cleanup" => \$cleanup,
+    "d|debug" => \$DEBUG,
+) or die ($helpstring);
+
+if ($showhelp) {
+    warn $helpstring;
+    exit(0);
+}
+
+my $storage_name = "${pool}-${namespace}";
+
+my $vmid_clone = int($vmid) - 1;
+my $vmid_linked_clone = int($vmid) - 2;
+
+sub jp {
+    print to_json($_[0], { utf8 => 8, pretty => 1, canonical => 1 }) . "\n";
+}
+sub dbgvar {
+    jp(@_) if $DEBUG;
+}
+
+sub run_cmd {
+    my ($cmd, $json, $ignore_errors) = @_;
+
+    my $raw = '';
+    my $parser = sub {$raw .= shift;};
+
+    eval {
+       run_command($cmd, outfunc => $parser);
+    };
+    if (my $err = $@) {
+       die $err if !$ignore_errors;
+    }
+
+    if ($json) {
+       my $result;
+       if ($raw eq '') {
+           $result = [];
+       } elsif ($raw =~ m/^(\[.*\])$/s) { # untaint
+           $result = JSON::decode_json($1);
+       } else {
+           die "got unexpected data from command: '$cmd' -> '$raw'\n";
+       }
+       return $result;
+       }
+    return $raw;
+}
+
+sub run_test_cmd {
+    my ($cmd) = @_;
+
+    my $raw = '';
+    my $out = sub {
+       my $line = shift;
+       $raw .= "${line}\n";
+    };
+
+    eval {
+       run_command($cmd, outfunc => $out);
+    };
+    if (my $err = $@) {
+       print $raw;
+       print $err;
+       return 0;
+    }
+    print $raw;
+    return 1;
+}
+
+sub prepare {
+    print "Preparing test environment\n";
+
+    my $pools = run_cmd("ceph osd pool ls --format json", 1);
+
+    my %poolnames = map {$_ => 1} @$pools;
+    die "Pool '$pool' does not exist!\n"
+       if !exists($poolnames{$pool}) && $use_existing;
+
+    run_cmd(['pveceph', 'pool', 'create', ${pool}, '--add_storages', 1])
+       if !$use_existing;
+
+    my $namespaces = run_cmd(['rbd', '-p', ${pool}, 'namespace', 'ls', '--format', 'json'], 1);
+    dbgvar($namespace);
+    my $ns_found = 0;
+    for my $i (@$namespaces) {
+       $ns_found = 1 if $i->{name} eq $namespace;
+    }
+
+    if (!$ns_found) {
+       print "Create namespace '${namespace}' in pool '${pool}'\n";
+       run_cmd(['rbd', 'namespace', 'create', "${pool}/${namespace}"]);
+    }
+
+    my $storages = run_cmd(['pvesh', 'get', 'storage', '--output-format', 'json'], 1);
+    dbgvar($storages);
+    my $rbd_found = 0;
+    my $pool_found = 0;
+
+    print "Create storage definition\n";
+    for my $stor (@$storages) {
+       $pool_found = 1 if $stor->{storage} eq $pool;
+       $rbd_found = 1 if $stor->{storage} eq $storage_name;
+
+       if ($rbd_found) {
+           run_cmd(['pvesm', 'set', ${storage_name}, '--krbd', '0']);
+           die "Enable the storage '$stor->{storage}'!" if $stor->{disable};
+       }
+    }
+    if (!$pool_found) {
+       die "No storage for pool '${pool}' found! Must have same name as pool!\n"
+           if $use_existing;
+
+       run_cmd(['pvesm', 'add', 'rbd', $pool, '--pool', $pool, '--content', 'images,rootdir']);
+    }
+    # create PVE storages (librbd / krbd)
+    run_cmd(['pvesm', 'add', 'rbd', ${storage_name}, '--krbd', '0', '--pool', ${pool}, '--namespace', ${namespace}, '--content', 'images,rootdir'])
+       if !$rbd_found;
+
+
+    # create test VM
+    print "Create test VM ${vmid}\n";
+    my $vms = run_cmd(['pvesh', 'get', 'cluster/resources', '--type', 'vm', '--output-format', 'json'], 1);
+    for my $vm (@$vms) {
+       # TODO: introduce a force flag to make this behaviour configurable
+
+       if ($vm->{vmid} eq $vmid) {
+           print "Test VM '${vmid}' already exists. It will be removed and recreated!\n";
+           run_cmd(['qm', 'stop', ${vmid}], 0, 1);
+           run_cmd(['qm', 'destroy', ${vmid}]);
+       }
+    }
+    run_cmd(['qm', 'create', ${vmid}, '--bios', 'ovmf', '--efidisk0', "${storage_name}:1", '--scsi0', "${storage_name}:2"]);
+}
+
+
+sub cleanup {
+    print "Cleaning up test environment!\n";
+    print "Removing VMs\n";
+    run_cmd(['qm', 'stop', ${vmid}], 0, 1);
+    run_cmd(['qm', 'stop', ${vmid_linked_clone}], 0, 1);
+    run_cmd(['qm', 'stop', ${vmid_clone}], 0, 1);
+    run_cmd(['qm', 'destroy', ${vmid_linked_clone}], 0, 1);
+    run_cmd(['qm', 'destroy', ${vmid_clone}], 0, 1);
+    run_cmd(['for', 'i', 'in', "/dev/rbd/${pool}/${namespace}/*;", 'do', '/usr/bin/rbd', 'unmap', '\$i;', 'done'], 0, 1);
+    run_cmd(['qm', 'unlock', ${vmid}], 0, 1);
+    run_cmd(['qm', 'destroy', ${vmid}], 0, 1);
+
+    print "Removing Storage definition for ${storage_name}\n";
+    run_cmd(['pvesm', 'remove', ${storage_name}], 0, 1);
+
+    print "Removing RBD namespace '${pool}/${namespace}'\n";
+    run_cmd(['rbd', 'namespace', 'remove', "${pool}/${namespace}"], 0, 1);
+
+    if (!$use_existing) {
+       print "Removing Storage definition for ${pool}\n";
+       run_cmd(['pvesm', 'remove', ${pool}], 0, 1);
+       print "Removing test pool\n";
+       run_cmd(['pveceph', 'pool', 'destroy', $pool]);
+    }
+}
+
+my $tests = [
+    # Example structure for tests
+    # {
+    #     name => "name of test section",
+    #     preparations => [
+    #         ['some', 'prep', 'command'],
+    #     ],
+    #     steps => [
+    #         ['test', 'cmd', $vmid],
+    #         ['second', 'step', $vmid],
+    #     ],
+    #     cleanup => [
+    #         ['cleanup', 'command'],
+    #     ],
+    # },
+    {
+       name => 'first VM start',
+       steps => [
+           ['qm', 'start', $vmid],
+       ],
+    },
+    {
+       name => 'snapshot/rollback',
+       steps => [
+           ['qm', 'snapshot', $vmid, 'test'],
+           ['qm', 'rollback', $vmid, 'test'],
+       ],
+       cleanup => [
+           ['qm', 'unlock', $vmid],
+       ],
+    },
+    {
+       name => 'remove snapshot',
+       steps => [
+           ['qm', 'delsnapshot', $vmid, 'test'],
+       ],
+    },
+    {
+       name => 'moving disk between namespaces',
+       steps => [
+           ['qm', 'move_disk', $vmid, 'scsi0', $pool, '--delete', 1],
+           ['qm', 'move_disk', $vmid, 'scsi0', $storage_name, '--delete', 1],
+       ],
+    },
+    {
+       name => 'switch to krbd',
+       preparations => [
+           ['qm', 'stop', $vmid],
+           ['pvesm', 'set', $storage_name, '--krbd', 1]
+       ],
+    },
+    {
+       name => 'start VM with krbd',
+       steps => [
+           ['qm', 'start', $vmid],
+       ],
+    },
+    {
+       name => 'snapshot/rollback with krbd',
+       steps => [
+           ['qm', 'snapshot', $vmid, 'test'],
+           ['qm', 'rollback', $vmid, 'test'],
+       ],
+       cleanup => [
+           ['qm', 'unlock', $vmid],
+       ],
+    },
+    {
+       name => 'remove snapshot with krbd',
+       steps => [
+           ['qm', 'delsnapshot', $vmid, 'test'],
+       ],
+    },
+    {
+       name => 'moving disk between namespaces with krbd',
+       steps => [
+           ['qm', 'move_disk', $vmid, 'scsi0', $pool, '--delete', 1],
+           ['qm', 'move_disk', $vmid, 'scsi0', $storage_name, '--delete', 1],
+       ],
+    },
+    {
+       name => 'clone VM with krbd',
+       steps => [
+           ['qm', 'clone', $vmid, $vmid_clone],
+       ],
+    },
+    {
+       name => 'switch to non krbd',
+       preparations => [
+           ['qm', 'stop', $vmid],
+           ['qm', 'stop', $vmid_clone],
+           ['pvesm', 'set', $storage_name, '--krbd', 0]
+       ],
+    },
+    {
+       name => 'templates and linked clone',
+       steps => [
+           ['qm', 'template', $vmid],
+           ['qm', 'clone', $vmid, $vmid_linked_clone],
+           ['qm', 'start', $vmid_linked_clone],
+           ['qm', 'stop', $vmid_linked_clone],
+       ],
+    },
+    {
+       name => 'start linked clone with krbd',
+       preparations => [
+           ['pvesm', 'set', $storage_name, '--krbd', 1]
+       ],
+       steps => [
+           ['qm', 'start', $vmid_linked_clone],
+           ['qm', 'stop', $vmid_linked_clone],
+       ],
+    },
+];
+
+sub run_prep_cleanup {
+    my ($cmds) = @_;
+
+    for (@$cmds) {
+       print join(' ', @$_). "\n";
+       run_cmd($_);
+    }
+}
+
+sub run_steps {
+    my ($steps) = @_;
+
+    for (@$steps) {
+       ok(run_test_cmd($_), join(' ', @$_));
+    }
+}
+
+sub run_tests {
+    print "Running tests:\n";
+
+    my $num_tests = 0;
+    for (@$tests) {
+       $num_tests += scalar(@{$_->{steps}}) if defined $_->{steps};
+    }
+
+    print("Tests: $num_tests\n");
+    plan tests => $num_tests;
+
+    for my $test (@$tests) {
+       print "Section: $test->{name}\n";
+       run_prep_cleanup($test->{preparations}) if defined $test->{preparations};
+       run_steps($test->{steps}) if defined $test->{steps};
+       run_prep_cleanup($test->{cleanup}) if defined $test->{cleanup};
+    }
+
+    done_testing();
+
+    if (Test::More->builder->is_passing()) {
+       cleanup();
+    }
+}
+
+if ($cleanup) {
+    cleanup();
+} else {
+    prepare();
+    run_tests();
+}
+
diff --git a/src/test/run_bwlimit_tests.pl b/src/test/run_bwlimit_tests.pl
new file mode 100755 (executable)
index 0000000..6ae379c
--- /dev/null
@@ -0,0 +1,211 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::MockModule;
+use Test::More;
+
+use lib ('.', '..');
+use PVE::RPCEnvironment;
+use PVE::Cluster;
+use PVE::Storage;
+
+my $datacenter_cfg = <<'EOF';
+bwlimit: default=100,move=80,restore=60
+EOF
+
+my $storage_cfg = <<'EOF';
+dir: nolimit
+       path /dir/a
+
+dir: d50
+       path /dir/b
+       bwlimit default=50
+
+dir: d50m40r30
+       path /dir/c
+       bwlimit default=50,move=40,restore=30
+
+dir: d20m40r30
+       path /dir/c
+       bwlimit default=20,move=40,restore=30
+
+dir: d200m400r300
+       path /dir/c
+       bwlimit default=200,move=400,restore=300
+
+dir: d10
+       path /dir/d
+       bwlimit default=10
+
+dir: m50
+       path /dir/e
+       bwlimit move=50
+
+dir: d200
+       path /dir/f
+       bwlimit default=200
+
+EOF
+
+my $permissions = {
+    'user1@test' => {},
+    'user2@test' => { '/' => ['Sys.Modify'], },
+    'user3@test' => { '/storage' => ['Datastore.Allocate'], },
+    'user4@test' => { '/storage/d20m40r30' => ['Datastore.Allocate'], },
+};
+
+my $pve_cluster_module;
+$pve_cluster_module = Test::MockModule->new('PVE::Cluster');
+$pve_cluster_module->mock(
+    cfs_update => sub {},
+    get_config => sub {
+       my ($file) = @_;
+       if ($file eq 'datacenter.cfg') {
+           return $datacenter_cfg;
+       } elsif ($file eq 'storage.cfg') {
+           return $storage_cfg;
+       }
+       die "TODO: mock get_config($file)\n";
+    },
+);
+
+my $rpcenv_module;
+$rpcenv_module = Test::MockModule->new('PVE::RPCEnvironment');
+$rpcenv_module->mock(
+    check => sub {
+       my ($env, $user, $path, $perms, $noerr) = @_;
+       return 1 if $user eq 'root@pam';
+       my $userperms = $permissions->{$user}
+           or die "no permissions defined for user $user\n";
+       if (defined(my $pathperms = $userperms->{$path})) {
+           foreach my $pp (@$pathperms) {
+               foreach my $reqp (@$perms) {
+                   return 1 if $pp eq $reqp;
+               }
+           }
+       }
+       die "permission denied\n" if !$noerr;
+       return 0;
+    },
+);
+
+my $rpcenv = PVE::RPCEnvironment->init('pub');
+
+my @tests = (
+    [ user => 'root@pam' ],
+    [ ['unknown', ['nolimit'],   undef], 100, 'root / generic default limit, requesting default' ],
+    [ ['move',    ['nolimit'],   undef],  80, 'root / specific default limit, requesting default (move)' ],
+    [ ['restore', ['nolimit'],   undef],  60, 'root / specific default limit, requesting default (restore)' ],
+    [ ['unknown', ['d50m40r30'], undef],  50, 'root / storage default limit' ],
+    [ ['move',    ['d50m40r30'], undef],  40, 'root / specific storage limit (move)' ],
+    [ ['restore', ['d50m40r30'], undef],  30, 'root / specific storage limit (restore)' ],
+    [ ['unknown', ['nolimit'],       0],   0, 'root / generic default limit' ],
+    [ ['move',    ['nolimit'],       0],   0, 'root / specific default limit (move)' ],
+    [ ['restore', ['nolimit'],       0],   0, 'root / specific default limit (restore)' ],
+    [ ['unknown', ['d50m40r30'],     0],   0, 'root / storage default limit' ],
+    [ ['move',    ['d50m40r30'],     0],   0, 'root / specific storage limit (move)' ],
+    [ ['restore', ['d50m40r30'],     0],   0, 'root / specific storage limit (restore)' ],
+    [ ['migrate', undef,           100], 100, 'root / undef storage (migrate)' ],
+    [ ['migrate', [],              100], 100, 'root / no storage (migrate)' ],
+    [ ['migrate', [undef],       undef], 100, 'root / [undef] storage no override (migrate)' ],
+    [ ['migrate', [undef, undef],  200], 200, 'root / list of undef storages with override (migrate)' ],
+
+    [ user => 'user1@test' ],
+    [ ['unknown', ['nolimit'],      undef], 100, 'generic default limit' ],
+    [ ['move',    ['nolimit'],      undef],  80, 'specific default limit (move)' ],
+    [ ['restore', ['nolimit'],      undef],  60, 'specific default limit (restore)' ],
+    [ ['unknown', ['d50m40r30'],    undef],  50, 'storage default limit' ],
+    [ ['move',    ['d50m40r30'],    undef],  40, 'specific storage limit (move)' ],
+    [ ['restore', ['d50m40r30'],    undef],  30, 'specific storage limit (restore)' ],
+    [ ['unknown', ['d200m400r300'], undef], 200, 'storage default limit above datacenter limits' ],
+    [ ['move',    ['d200m400r300'], undef], 400, 'specific storage limit above datacenter limits (move)' ],
+    [ ['restore', ['d200m400r300'], undef], 300, 'specific storage limit above datacenter limits (restore)' ],
+    [ ['unknown', ['d50'],          undef],  50, 'storage default limit' ],
+    [ ['move',    ['d50'],          undef],  50, 'storage default limit (move)' ],
+    [ ['restore', ['d50'],          undef],  50, 'storage default limit (restore)' ],
+
+    [ user => 'user2@test' ],
+    [ ['unknown', ['nolimit'],       0],     0, 'generic default limit with Sys.Modify, passing unlimited' ],
+    [ ['unknown', ['nolimit'],   undef],   100, 'generic default limit with Sys.Modify' ],
+    [ ['move',    ['nolimit'],   undef],    80, 'specific default limit with Sys.Modify (move)' ],
+    [ ['restore', ['nolimit'],   undef],    60, 'specific default limit with Sys.Modify (restore)' ],
+    [ ['restore', ['nolimit'],       0],     0, 'specific default limit with Sys.Modify, passing unlimited (restore)' ],
+    [ ['move',    ['nolimit'],       0],     0, 'specific default limit with Sys.Modify, passing unlimited (move)' ],
+    [ ['unknown', ['d50m40r30'], undef],    50, 'storage default limit with Sys.Modify' ],
+    [ ['restore', ['d50m40r30'], undef],    30, 'specific storage limit with Sys.Modify (restore)' ],
+    [ ['move',    ['d50m40r30'], undef],    40, 'specific storage limit with Sys.Modify (move)' ],
+
+    [ user => 'user3@test' ],
+    [ ['unknown', ['nolimit'],   undef],   100, 'generic default limit with privileges on /' ],
+    [ ['unknown', ['nolimit'],      80],    80, 'generic default limit with privileges on /, passing an override value' ],
+    [ ['unknown', ['nolimit'],       0],     0, 'generic default limit with privileges on /, passing unlimited' ],
+    [ ['move',    ['nolimit'],   undef],    80, 'specific default limit with privileges on / (move)' ],
+    [ ['move',    ['nolimit'],       0],     0, 'specific default limit with privileges on /, passing unlimited (move)' ],
+    [ ['restore', ['nolimit'],   undef],    60, 'specific default limit with privileges on / (restore)' ],
+    [ ['restore', ['nolimit'],       0],     0, 'specific default limit with privileges on /, passing unlimited (restore)' ],
+    [ ['unknown', ['d50m40r30'],     0],     0, 'storage default limit with privileges on /, passing unlimited' ],
+    [ ['unknown', ['d50m40r30'], undef],    50, 'storage default limit with privileges on /' ],
+    [ ['unknown', ['d50m40r30'],     0],     0, 'storage default limit with privileges on, passing unlimited /' ],
+    [ ['move',    ['d50m40r30'], undef],    40, 'specific storage limit with privileges on / (move)' ],
+    [ ['move',    ['d50m40r30'],     0],     0, 'specific storage limit with privileges on, passing unlimited / (move)' ],
+    [ ['restore', ['d50m40r30'], undef],    30, 'specific storage limit with privileges on / (restore)' ],
+    [ ['restore', ['d50m40r30'],     0],     0, 'specific storage limit with privileges on /, passing unlimited (restore)' ],
+
+    [ user => 'user4@test' ],
+    [ ['unknown', ['nolimit'],                   10],     10, 'generic default limit with privileges on a different storage, passing lower override' ],
+    [ ['unknown', ['nolimit'],                undef],    100, 'generic default limit with privileges on a different storage' ],
+    [ ['unknown', ['nolimit'],                    0],    100, 'generic default limit with privileges on a different storage, passing unlimited' ],
+    [ ['move',    ['nolimit'],                undef],     80, 'specific default limit with privileges on a different storage (move)' ],
+    [ ['restore', ['nolimit'],                undef],     60, 'specific default limit with privileges on a different storage (restore)' ],
+    [ ['unknown', ['d50m40r30'],              undef],     50, 'storage default limit with privileges on a different storage' ],
+    [ ['move',    ['d50m40r30'],              undef],     40, 'specific storage limit with privileges on a different storage (move)' ],
+    [ ['restore', ['d50m40r30'],              undef],     30, 'specific storage limit with privileges on a different storage (restore)' ],
+    [ ['unknown', ['d20m40r30'],              undef],     20, 'storage default limit with privileges on that storage' ],
+    [ ['unknown', ['d20m40r30'],                  0],      0, 'storage default limit with privileges on that storage, passing unlimited' ],
+    [ ['move',    ['d20m40r30'],              undef],     40, 'specific storage limit with privileges on that storage (move)' ],
+    [ ['move',    ['d20m40r30'],                  0],      0, 'specific storage limit with privileges on that storage, passing unlimited (move)' ],
+    [ ['move',    ['d20m40r30'],                 10],     10, 'specific storage limit with privileges on that storage, passing low override (move)' ],
+    [ ['move',    ['d20m40r30'],                300],    300, 'specific storage limit with privileges on that storage, passing high override (move)' ],
+    [ ['restore', ['d20m40r30'],              undef],     30, 'specific storage limit with privileges on that storage (restore)' ],
+    [ ['restore', ['d20m40r30'],                  0],      0, 'specific storage limit with privileges on that storage, passing unlimited (restore)' ],
+    [ ['unknown', ['d50m40r30', 'd20m40r30'],     0],     50, 'multiple storages default limit with privileges on one of them, passing unlimited' ],
+    [ ['move',    ['d50m40r30', 'd20m40r30'],     0],     40, 'multiple storages specific limit with privileges on one of them, passing unlimited (move)' ],
+    [ ['restore', ['d50m40r30', 'd20m40r30'],     0],     30, 'multiple storages specific limit with privileges on one of them, passing unlimited (restore)' ],
+    [ ['unknown', ['d50m40r30', 'd20m40r30'], undef],     20, 'multiple storages default limit with privileges on one of them' ],
+    [ ['unknown', ['d10', 'd20m40r30'],       undef],     10, 'multiple storages default limit with privileges on one of them (storage limited)' ],
+    [ ['move',    ['d10', 'd20m40r30'],       undef],     10, 'multiple storages specific limit with privileges on one of them (storage limited) (move)' ],
+    [ ['restore', ['d10', 'd20m40r30'],       undef],     10, 'multiple storages specific limit with privileges on one of them (storage limited) (restore)' ],
+    [ ['restore', ['d10', 'd20m40r30'],           5],      5, 'multiple storages specific limit (storage limited) (restore), passing lower override' ],
+    [ ['restore', ['d200', 'd200m400r300'],      65],     65, 'multiple storages specific limit (storage limited) (restore), passing lower override' ],
+    [ ['restore', ['d200', 'd200m400r300'],     400],    200, 'multiple storages specific limit (storage limited) (restore), passing higher override' ],
+    [ ['restore', ['d200', 'd200m400r300'],       0],    200, 'multiple storages specific limit (storage limited) (restore), passing unlimited' ],
+    [ ['restore', ['d200', 'd200m400r300'],       1],      1, 'multiple storages specific limit (storage limited) (restore), passing 1' ],
+    [ ['restore', ['d10', 'd20m40r30'],         500],     10, 'multiple storages specific limit with privileges on one of them (storage limited) (restore), passing higher override' ],
+    [ ['unknown', ['nolimit', 'd20m40r30'],       0],    100, 'multiple storages default limit with privileges on one of them, passing unlimited (default limited)' ],
+    [ ['move',    ['nolimit', 'd20m40r30'],       0],     80, 'multiple storages specific limit with privileges on one of them, passing unlimited (default limited) (move)' ],
+    [ ['restore', ['nolimit', 'd20m40r30'],       0],     60, 'multiple storages specific limit with privileges on one of them, passing unlimited (default limited) (restore)' ],
+    [ ['unknown', ['nolimit', 'd20m40r30'],   undef],     20, 'multiple storages default limit with privileges on one of them (default limited)' ],
+    [ ['move',    ['nolimit', 'd20m40r30'],   undef],     40, 'multiple storages specific limit with privileges on one of them (default limited) (move)' ],
+    [ ['restore', ['nolimit', 'd20m40r30'],   undef],     30, 'multiple storages specific limit with privileges on one of them (default limited) (restore)' ],
+    [ ['restore', ['d20m40r30', 'm50'],         200],     60, 'multiple storages specific limit with privileges on one of them (global default limited) (restore)' ],
+    [ ['move',    ['nolimit', undef ],          40] ,     40, 'multiple storages one undefined, passing 40 (move)' ],
+    [ ['move',    undef,                       100] ,     80, 'undef storage, passing 100 (move)' ],
+    [ ['move',    [undef],                     100] ,     80, '[undef] storage, passing 100 (move)' ],
+    [ ['move',    [undef],                   undef] ,     80, '[undef] storage, no override (move)' ],
+);
+
+foreach my $t (@tests) {
+    my ($args, $expected, $description) = @$t;
+    if (!ref($args)) {
+       if ($args eq 'user') {
+           $rpcenv->set_user($expected);
+       } else {
+           die "not a test specification\n";
+       }
+       next;
+    }
+    is(PVE::Storage::get_bandwidth_limit(@$args), $expected, $description);
+}
+done_testing();
diff --git a/src/test/run_disk_tests.pl b/src/test/run_disk_tests.pl
new file mode 100755 (executable)
index 0000000..c1a698e
--- /dev/null
@@ -0,0 +1,12 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use TAP::Harness;
+
+my $harness = TAP::Harness->new( { verbosity => -2 });
+my $res = $harness->runtests( "disklist_test.pm" );
+
+exit -1 if !$res || $res->{failed} || $res->{parse_errors};
+
diff --git a/src/test/run_plugin_tests.pl b/src/test/run_plugin_tests.pl
new file mode 100755 (executable)
index 0000000..d33429a
--- /dev/null
@@ -0,0 +1,22 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+# to ensure consistent ctime values on all systems
+$ENV{TZ} = 'UTC';
+
+use TAP::Harness;
+
+my $harness = TAP::Harness->new( { verbosity => -1 });
+my $res = $harness->runtests(
+    "archive_info_test.pm",
+    "parse_volname_test.pm",
+    "list_volumes_test.pm",
+    "path_to_volume_id_test.pm",
+    "get_subdir_test.pm",
+    "filesystem_path_test.pm",
+    "prune_backups_test.pm",
+);
+
+exit -1 if !$res || $res->{failed} || $res->{parse_errors};
diff --git a/src/test/run_test_zfspoolplugin.pl b/src/test/run_test_zfspoolplugin.pl
new file mode 100755 (executable)
index 0000000..095ccb3
--- /dev/null
@@ -0,0 +1,2765 @@
+#!/usr/bin/perl
+
+use lib '..';
+
+use strict;
+use warnings;
+
+use Data::Dumper qw(Dumper);
+use PVE::Storage;
+use PVE::Cluster;
+use PVE::Tools qw(run_command);
+use Cwd;
+$Data::Dumper::Sortkeys = 1;
+
+my $verbose = undef;
+
+my $storagename = "zfstank99";
+my $subvol = 'regressiontest';
+my $mountpoint = "${subvol}_mnt";
+
+#volsize in GB
+my $volsize = 1;
+my $vmdisk = "vm-102-disk-1";
+my $vmbase = "base-100-disk-1";
+my $vmlinked = "vm-101-disk-1";
+my $ctdisk = "subvol-202-disk-1";
+my $ctbase = "basevol-200-disk-1";
+my $ctlinked = "subvol-201-disk-1";
+
+my $basesnap = '@__base__';
+my $tests = {};
+
+#create zfs subvol for testing
+my $pool = undef;
+my $zpath = undef;
+my $cfg = undef;
+my $scfg = undef;
+my $count = 0;
+my $testnum = 19;
+my $end_test = $testnum;
+my $start_test = 1;
+
+if (@ARGV == 2) {
+    $end_test = $ARGV[1];
+    $start_test = $ARGV[0];
+} elsif (@ARGV == 1) {
+    $start_test = $ARGV[0];
+    $end_test = $ARGV[0];
+}
+
+my $test19 = sub {
+
+    print "\nrun test19 \"path\"\n";
+
+    my @res;
+    my $fail = 0;
+    eval {
+       @res = PVE::Storage::path($cfg, "$storagename:$vmdisk");
+       if ($res[0] ne "\/dev\/zvol\/regressiontest\/$vmdisk") {
+           $count++;
+           $fail = 1;
+           warn "Test 19 a: path is not correct: expected \'\/dev\/zvol\/regressiontest\/$vmdisk'\  get \'$res[0]\'";
+       }
+       if ($res[1] ne "102") {
+           if (!$fail) {
+               $count++;
+               $fail = 1;
+           }
+           warn "Test 19 a: owner is not correct: expected \'102\'  get \'$res[1]\'";
+       }
+       if ($res[2] ne "images") {
+           if (!$fail) {
+               $count++;
+               $fail = 1;
+           }
+           warn "Test 19 a: owner is not correct: expected \'images\'  get \'$res[2]\'";
+       }
+    };
+    if ( $@ ) {
+       $count++;
+       warn "Test 19 a: $@";
+    }
+
+    @res = undef;
+    $fail = 0;
+    eval {
+       @res = PVE::Storage::path($cfg, "$storagename:$vmbase");
+       if ($res[0] ne "\/dev\/zvol\/regressiontest\/$vmbase") {
+           $count++;
+           $fail = 1;
+           warn "Test 19 b: path is not correct: expected \'\/dev\/zvol\/regressiontest\/$vmbase'\  get \'$res[0]\'";
+       }
+       if ($res[1] ne "100") {
+           if (!$fail) {
+               $count++;
+               $fail = 1;
+           }
+           warn "Test 19 b: owner is not correct: expected \'100\'  get \'$res[1]\'";
+       }
+       if ($res[2] ne "images") {
+           if (!$fail) {
+               $count++;
+               $fail = 1;
+           }
+           warn "Test 19 b: owner is not correct: expected \'images\'  get \'$res[2]\'";
+       }
+    };
+    if ( $@ ) {
+       $count++;
+       warn "Test 19 b: $@";
+    }
+
+    @res = undef;
+    $fail = 0;
+    eval {
+       @res = PVE::Storage::path($cfg, "$storagename:$vmbase\/$vmlinked");
+       if ($res[0] ne "\/dev\/zvol\/regressiontest\/$vmlinked") {
+           $count++;
+           $fail = 1;
+           warn "Test 19 c: path is not correct: expected \'\/dev\/zvol\/regressiontest\/$vmlinked'\  get \'$res[0]\'";
+       }
+       if ($res[1] ne "101") {
+           if (!$fail) {
+               $count++;
+               $fail = 1;
+           }
+           warn "Test 19 c: owner is not correct: expected \'101\'  get \'$res[1]\'";
+       }
+       if ($res[2] ne "images") {
+           if (!$fail) {
+               $count++;
+               $fail = 1;
+           }
+           warn "Test 19 c: owner is not correct: expected \'images\'  get \'$res[2]\'";
+       }
+    };
+    if ( $@ ) {
+       $count++;
+       warn "Test 19 c: $@";
+    }
+
+    @res = undef;
+    $fail = 0;
+    eval {
+       @res = PVE::Storage::path($cfg, "$storagename:$ctdisk");
+       if ($res[0] ne "\/$mountpoint\/$ctdisk") {
+           $count++;
+           $fail = 1;
+           warn "Test 19 d: path is not correct: expected \'\/$mountpoint\/$ctdisk'\  get \'$res[0]\'";
+       }
+       if ($res[1] ne "202") {
+           if (!$fail) {
+               $count++;
+               $fail = 1;
+           }
+           warn "Test 19 d: owner is not correct: expected \'202\'  get \'$res[1]\'";
+       }
+       if ($res[2] ne "images") {
+           if (!$fail) {
+               $count++;
+               $fail = 1;
+           }
+           warn "Test 19 d: owner is not correct: expected \'images\'  get \'$res[2]\'";
+       }
+    };
+    if ( $@ ) {
+       $count++;
+       warn "Test 19 d: $@";
+    }
+
+    @res = undef;
+    $fail = 0;
+    eval {
+       @res = PVE::Storage::path($cfg, "$storagename:$ctbase");
+       if ($res[0] ne "\/$mountpoint\/$ctbase") {
+           $count++;
+           $fail = 1;
+           warn "Test 19 e: path is not correct: expected \'\/$mountpoint\/$ctbase'\  get \'$res[0]\'";
+       }
+       if ($res[1] ne "200") {
+           if (!$fail) {
+               $count++;
+               $fail = 1;
+           }
+           warn "Test 19 e: owner is not correct: expected \'200\'  get \'$res[1]\'";
+       }
+       if ($res[2] ne "images") {
+           if (!$fail) {
+               $count++;
+               $fail = 1;
+           }
+           warn "Test 19 e: owner is not correct: expected \'images\'  get \'$res[2]\'";
+       }
+    };
+    if ( $@ ) {
+       $count++;
+       warn "Test 19 e: $@";
+    }
+
+    @res = undef;
+    $fail = 0;
+    eval {
+       @res = PVE::Storage::path($cfg, "$storagename:$ctbase\/$ctlinked");
+       if ($res[0] ne "\/$mountpoint\/$ctlinked") {
+           $count++;
+           $fail = 1;
+           warn "Test 19 f: path is not correct: expected \'\/$mountpoint\/$ctlinked'\  get \'$res[0]\'";
+       }
+       if ($res[1] ne "201") {
+           if (!$fail) {
+               $count++;
+               $fail = 1;
+           }
+           warn "Test 19 f: owner is not correct: expected \'201\'  get \'$res[1]\'";
+       }
+       if ($res[2] ne "images") {
+           if (!$fail) {
+               $count++;
+               $fail = 1;
+           }
+           warn "Test 19 f: owner is not correct: expected \'images\'  get \'$res[2]\'";
+       }
+    };
+    if ( $@ ) {
+       $count++;
+       warn "Test 19 f: $@";
+    }
+};
+$tests->{19} = $test19;
+
+my $test18 = sub {
+
+    print "\nrun test18 \"scan_zfs\"\n";
+    my $res;
+
+    eval {
+       $res = PVE::Storage::scan_zfs($cfg, $storagename);
+
+       my $exists = 0;
+       foreach my $subvol (@$res){
+           if ($subvol->{pool} eq 'regressiontest') {
+               $exists++;
+           }
+       }
+       if (!$exists) {
+           $count++;
+           warn "Test 18 a: not pool";
+       }
+    };
+    if ( $@ ) {
+       $count++;
+       warn "Test 18 a: $@";
+    }
+    $res = undef;
+
+    eval {
+       $res = PVE::Storage::scan_zfs($cfg, $storagename);
+       
+       foreach my $subvol (@$res){
+           if ($subvol->{pool} eq 'zfspool/subvol') {
+               $count++;
+               warn "Test 18 b:";
+           }
+       }
+
+       foreach my $subvol (@$res){
+           if ($subvol->{pool} eq 'zfspool/basevol') {
+               $count++;
+               warn "Test 18 c";
+           }
+       }
+    };
+    if ( $@ ) {
+       $count++;
+       warn "Test 18 a: $@";
+    }
+};
+$tests->{18} = $test18;
+
+my $test17 = sub {
+
+    print "\nrun test17 \"deactivate_storage\"\n";
+
+    eval {
+       PVE::Storage::activate_storage($cfg, $storagename);
+       PVE::Storage::deactivate_storage($cfg, $storagename);
+    };
+    if ($@) {
+       $count++;
+       warn "Test 17 a: $@";
+    }
+};
+$tests->{17} = $test17;
+
+my $test16 = sub {
+
+    print "\nrun test16 \"activate_storage\"\n";
+
+    eval {
+       PVE::Storage::activate_storage($cfg, $storagename);
+    };
+    if ($@) {
+       $count++;
+       warn "Test 16 a: $@";
+    }
+};
+$tests->{16} = $test16;
+
+my $test15 = sub {
+
+    print "\nrun test15 \"template_list and vdisk_list\"\n";
+
+    my $hash = Dumper {};
+
+    my $res = Dumper PVE::Storage::template_list($cfg, $storagename, "vztmpl");
+    if ( $hash ne $res ) {
+       $count++;
+       warn "Test 15 a failed\n";
+    }
+    $res = undef;
+
+    $res = Dumper PVE::Storage::template_list($cfg, $storagename, "iso");
+    if ( $hash ne $res ) {
+       $count++;
+       warn "Test 15 b failed\n";
+    }
+    $res = undef;
+
+    $res = Dumper PVE::Storage::template_list($cfg, $storagename, "backup");
+    if ( $hash ne $res ) {
+       $count++;
+       warn "Test 15 c failed\n";
+    }
+    $res = undef;
+
+    $hash = Dumper {'zfstank99' => [
+                       {
+                           'parent' => undef,
+                           'volid' => 'zfstank99:base-100-disk-1',
+                           'name' => 'base-100-disk-1',
+                           'vmid' => '100',
+                           'size' => 1073741824,
+                           'format' => 'raw'
+                       }
+                       ]};
+
+    $res = Dumper PVE::Storage::vdisk_list($cfg, $storagename, 100, ["$storagename:$vmbase"]);
+
+    if ( $hash ne $res ) {
+       $count++;
+       warn "Test 15 d failed\n";
+    }
+    $res = undef;
+
+    $hash = Dumper {'zfstank99' => [
+                       {
+                           'parent' => undef,
+                           'volid' => 'zfstank99:vm-102-disk-1',
+                           'name' => 'vm-102-disk-1',
+                           'vmid' => '102',
+                           'size' => 1073741824,
+                           'format' => 'raw'
+                       }
+                       ]};
+
+    $res = Dumper PVE::Storage::vdisk_list($cfg, $storagename, 102, ["$storagename:$vmdisk"]);
+    if ( $hash ne $res ) {
+       $count++;
+       warn "Test 15 e failed\n";
+    }
+    $res = undef;
+
+    $hash = Dumper {'zfstank99' => [
+                       {
+                           'parent' => 'base-100-disk-1@__base__',
+                           'volid' => "$storagename:$vmbase\/$vmlinked",
+                           'name' => 'vm-101-disk-1',
+                           'vmid' => '101',
+                           'size' => 1073741824,
+                           'format' => 'raw'
+                       }
+                       ]};
+
+    $res =  Dumper PVE::Storage::vdisk_list($cfg, $storagename, 101, ["$storagename:$vmbase\/$vmlinked"]);
+    if ( $hash ne $res ) {
+       $count++;
+       warn "Test 15 f failed\n";
+    }
+    $res = undef;
+
+    $hash = Dumper {'zfstank99' => [
+                       {
+                           'parent' => undef,
+                           'volid' => 'zfstank99:basevol-200-disk-1',
+                           'name' => 'basevol-200-disk-1',
+                           'vmid' => '200',
+                           'size' => 1073741824,
+                           'format' => 'subvol'
+                       }
+                       ]};
+
+    $res =  Dumper PVE::Storage::vdisk_list($cfg, $storagename, 200, ["$storagename:$ctbase"]);
+    if ( $hash ne $res ) {
+       $count++;
+       warn "Test 15 g failed\n";
+    }
+    $res = undef;
+
+    $hash = Dumper {'zfstank99' => [
+                       {
+                           'parent' => undef,
+                           'volid' => 'zfstank99:subvol-202-disk-1',
+                           'name' => 'subvol-202-disk-1',
+                           'vmid' => '202',
+                           'size' => 1073741824,
+                           'format' => 'subvol'
+                       }
+                       ]};
+
+    $res = Dumper PVE::Storage::vdisk_list($cfg, $storagename, 202, ["$storagename:$ctdisk"]);
+    if ( $hash ne $res ) {
+       $count++;
+       warn "Test 15 h failed\n";
+    }
+    $res = undef;
+
+    $hash = Dumper {'zfstank99' => [
+                       {
+                           'parent' => 'basevol-200-disk-1@__base__',
+                           'volid' => "$storagename:$ctbase\/$ctlinked",
+                           'name' => 'subvol-201-disk-1',
+                           'vmid' => '201',
+                           'size' => 1073741824,
+                           'format' => 'subvol'
+                       }
+                       ]};
+    $res = Dumper PVE::Storage::vdisk_list($cfg, $storagename, 201, ["$storagename:$ctbase\/$ctlinked"]);
+    if ( $hash ne $res ) {
+       $count++;
+       warn "Test 15 i failed\n";
+    }
+};
+$tests->{15} = $test15;
+
+my $test14 = sub {
+
+    print "\nrun test14 \"vdisk_free\"\n";
+
+    eval {
+       PVE::Storage::vdisk_free($cfg, "$storagename:$vmdisk");
+
+       eval {
+           run_command("zfs list $zpath\/$vmdisk", outfunc => sub {}, errfunc => sub {});
+       };
+       if (!$@) {
+           $count++;
+           warn "Test14 a: vdisk still exists\n";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test14 a: $@";
+    }
+
+    eval {
+       PVE::Storage::vdisk_free($cfg, "$storagename:$vmbase");
+    };
+    if (!$@) {
+       $count++;
+       warn "Test14 b: free vdisk should not work\n";
+    }
+
+    eval {
+       PVE::Storage::vdisk_free($cfg, "$storagename:$vmbase\/$vmlinked");
+
+       eval {
+           run_command("zfs list $zpath\/$vmlinked", outfunc => sub {}, errfunc => sub {});
+       };
+       if (!$@) {
+           $count++;
+           warn "Test14 c: vdisk still exists\n";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test14 c: $@";
+    }
+
+    eval {
+       PVE::Storage::vdisk_free($cfg, "$storagename:$ctdisk");
+
+       eval {
+           run_command("zfs list $zpath\/$ctdisk", outfunc => sub {}, errfunc => sub {});
+       };
+       if (!$@) {
+           $count++;
+           warn "Test14 d: vdisk still exists\n";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test14 d: $@";
+    }
+
+    eval {
+       PVE::Storage::vdisk_free($cfg, "$storagename:$ctbase");
+    };
+    if (!$@) {
+       $count++;
+       warn "Test14 e: free vdisk should not work\n";
+    }
+
+    eval {
+       PVE::Storage::vdisk_free($cfg, "$storagename:$ctbase\/$ctlinked");
+
+       eval {
+           run_command("zfs list $zpath\/$ctlinked", outfunc => sub {}, errfunc => sub {});
+       };
+       if (!$@) {
+           $count++;
+           warn "Test14 f: vdisk still exists\n";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test14 f: $@";
+    }
+
+    eval {
+       PVE::Storage::vdisk_free($cfg, "$storagename:$vmbase");
+
+       eval {
+           run_command("zfs list $zpath\/$vmbase", outfunc => sub {}, errfunc => sub {});
+       };
+       if (!$@) {
+           $count++;
+           warn "Test14 g: vdisk still exists\n";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test14 g: $@";
+    }
+
+    eval {
+       PVE::Storage::vdisk_free($cfg, "$storagename:$ctbase");
+
+       eval {
+           run_command("zfs list $zpath\/$ctbase", outfunc => sub {}, errfunc => sub {});
+       };
+       if (!$@) {
+           $count++;
+           warn "Test14 h: vdisk still exists\n";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test14 h: $@";
+    }
+};
+$tests->{14} = $test14;
+
+my $test13 = sub {
+
+    print "\nrun test13 \"vdisk_alloc\"\n";
+
+    eval {
+       my $tmp_volid = PVE::Storage::vdisk_alloc($cfg, $storagename, "112", "raw", undef ,1024 * 1024);
+
+       if ($tmp_volid ne "$storagename:vm-112-disk-0") {
+           die "volname:$tmp_volid don't match\n";
+       }
+       eval {
+           run_command("zfs get -H volsize $zpath\/vm-112-disk-0", outfunc =>
+                       sub { my $tmp = shift;
+                             if ($tmp !~ m/^$zpath\/vm-112-disk-0.*volsize.*1G.*$/) {
+                                 die "size don't match\n";
+                             }
+                       });
+       };
+       if ($@) {
+           $count++;
+           warn "Test13 a: $@";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test13 a: $@";
+    }
+
+    eval {
+       my $tmp_volid = PVE::Storage::vdisk_alloc($cfg, $storagename, "112", "raw", undef ,2048 * 1024);
+
+       if ($tmp_volid ne "$storagename:vm-112-disk-1") {
+           die "volname:$tmp_volid don't match\n";
+       }
+       eval {
+           run_command("zfs get -H volsize $zpath\/vm-112-disk-1", outfunc =>
+                       sub { my $tmp = shift;
+                             if ($tmp !~ m/^$zpath\/vm-112-disk-1.*volsize.*2G.*$/) {
+                                 die "size don't match\n";
+                             }
+                       });
+       };
+       if ($@) {
+           $count++;
+           warn "Test13 b: $@";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test13 b: $@";
+    }
+
+    eval {
+       my $tmp_volid = PVE::Storage::vdisk_alloc($cfg, $storagename, "113", "subvol", undef ,1024 * 1024);
+
+       if ($tmp_volid ne "$storagename:subvol-113-disk-0") {
+           die "volname:$tmp_volid  don't match\n";
+       }
+       eval {
+           run_command("zfs get -H refquota $zpath\/subvol-113-disk-0", outfunc =>
+                       sub { my $tmp = shift;
+                             if ($tmp !~ m/^$zpath\/subvol-113-disk-0.*refquota.*1G.*$/) {
+                                 die "size don't match\n";
+                             }
+                       });
+       };
+       if ($@) {
+           $count++;
+           warn "Test13 c: $@";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test13 c: $@";
+    }
+
+    eval {
+       my $tmp_volid = PVE::Storage::vdisk_alloc($cfg, $storagename, "113", "subvol", undef ,2048 * 1024);
+
+       if ($tmp_volid ne "$storagename:subvol-113-disk-1") {
+           die "volname:$tmp_volid  don't match\n";
+       }
+       eval {
+           run_command("zfs get -H refquota $zpath\/subvol-113-disk-1", outfunc =>
+                       sub { my $tmp = shift;
+                             if ($tmp !~ m/^$zpath\/subvol-113-disk-1.*refquota.*G.*$/) {
+                                 die "size don't match\n";
+                             }
+                       });
+       };
+       if ($@) {
+           $count++;
+           warn "Test13 d: $@";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test13 d: $@";
+    }
+};
+$tests->{13} = $test13;
+
+my $test12 = sub {
+
+    print "\nrun test12 \"vdisk_create_base\"\n";
+
+    eval {
+       my $tmp_volid = PVE::Storage::vdisk_create_base($cfg, "$storagename:$vmdisk");
+
+       if ($tmp_volid ne "$storagename:base-102-disk-1") {
+           die;
+       }
+       eval {
+           run_command("zfs list $zpath\/base-102-disk-1", outfunc => sub {});
+       };
+       if ($@) {
+           $count++;
+           warn "Test12 a: $@";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test12 a: $@";
+    }
+
+    eval {
+       my $tmp_volid = PVE::Storage::vdisk_create_base($cfg, "$storagename:$vmlinked");
+
+       if ($tmp_volid ne "$storagename:base-101-disk-1") {
+           die;
+       }
+       eval {
+           run_command("zfs list $zpath\/base-101-disk-1", outfunc => sub {});
+       };
+       if ($@) {
+           $count++;
+           warn "Test12 b: $@";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test12 b: $@";
+    }
+
+    eval {
+       my $tmp_volid = PVE::Storage::vdisk_create_base($cfg, "$storagename:$ctdisk");
+
+       if ($tmp_volid ne "$storagename:basevol-202-disk-1") {
+           die ;
+       }
+       eval {
+           run_command("zfs list $zpath\/basevol-202-disk-1", outfunc => sub {});
+       };
+       if ($@) {
+           $count++;
+           warn "Test12 c: $@";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test12 c: $@";
+    }
+
+    eval {
+       my $tmp_volid = PVE::Storage::vdisk_create_base($cfg, "$storagename:$ctlinked");
+
+       if ($tmp_volid ne "$storagename:basevol-201-disk-1") {
+           die;
+       }
+       eval {
+           run_command("zfs list $zpath\/basevol-201-disk-1", outfunc => sub {});
+       };
+       if ($@) {
+           $count++;
+           warn "Test12 d: $@";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test12 d: $@";
+    }
+};
+$tests->{12} = $test12;
+
+my $test11 = sub {
+
+    print "\nrun test11 \"volume_is_base\"\n";
+
+    eval {
+       PVE::Storage::vdisk_clone($cfg, "$storagename:$vmdisk", 110);
+    };
+    if (!$@) {
+       $count++;
+       warn "Test11 a: clone_image only works on base images";
+    }
+
+    eval {
+       if ("$storagename:$vmbase\/vm-110-disk-0" ne
+           PVE::Storage::vdisk_clone($cfg, "$storagename:$vmbase", 110, '__base__')){
+           $count++;
+           warn  "Test11 b";
+       }
+       run_command("zfs list -H -o volsize $zpath\/vm-110-disk-0", outfunc => sub {
+           my $line = shift;
+
+           chomp($line);
+           warn "Test11 b not correct volsize" if $line !~ m/$volsize/; 
+                   });
+    };
+    if ($@) {
+       $count++;
+       warn "Test11 b: $@";
+    }
+
+    eval {
+       PVE::Storage::vdisk_clone($cfg, "$storagename:$vmbase\/$vmlinked", 111);
+    };
+    if (!$@) {
+       $count++;
+       warn "Test11 c: clone_image only works on base images";
+    }
+
+    eval {
+       PVE::Storage::vdisk_clone($cfg, "$storagename:$ctdisk", 110);
+    };
+    if (!$@) {
+       $count++;
+       warn "Test11 d: clone_image only works on base images";
+    }
+
+    eval {
+       if ( "$storagename:$ctbase\/subvol-210-disk-0" ne
+            PVE::Storage::vdisk_clone($cfg, "$storagename:$ctbase", 210, '__base__')){
+           $count++;
+           warn  "Test11 e";
+       }
+       run_command("zfs list -H -o refquota $zpath\/subvol-210-disk-0", outfunc => sub {
+           my $line = shift;
+
+           chomp($line);
+           warn "Test11 e not correct volsize" if $line !~ m/$volsize/; 
+                   });
+    };
+    if ($@) {
+       $count++;
+       warn "Test11 e: $@";
+    }
+
+    eval {
+       PVE::Storage::vdisk_clone($cfg, "$storagename:$ctbase\/$ctlinked", 211);
+    };
+    if (!$@) {
+       $count++;
+       warn "Test11 f: clone_image only works on base images";
+    }
+};
+$tests->{11} = $test11;
+
+my $test10 =sub {
+
+    print "\nrun test10 \"volume_is_base\"\n";
+
+    eval {
+       if (1 == volume_is_base($cfg, "$storagename:$vmdisk")) {
+           $count++;
+           warn "Test10 a: is no base";
+       }
+
+    };
+    if ($@) {
+       $count++;
+       warn "Test10 a: $@";
+    }
+
+    eval {
+       if (0 == volume_is_base($cfg, "$storagename:$vmbase")) {
+           $count++;
+           warn "Test10 b: is base";
+       }
+
+    };
+    if ($@) {
+       $count++;
+       warn "Test10 b: $@";
+    }
+
+    eval {
+       if (1 == volume_is_base($cfg, "$storagename:$vmbase\/$vmlinked")) {
+           $count++;
+           warn "Test10 c: is no base";
+       }
+
+    };
+    if ($@) {
+       $count++;
+       warn "Test10 c: $@";
+    }
+
+    eval {
+       if (1 == volume_is_base($cfg, "$storagename:$ctdisk")) {
+           $count++;
+           warn "Test10 d: is no base";
+       }
+
+    };
+    if ($@) {
+       $count++;
+       warn "Test10 d: $@";
+    }
+
+    eval {
+       if (0 == volume_is_base($cfg, "$storagename:$ctbase")) {
+           $count++;
+           warn "Test10 e: is base";
+       }
+
+    };
+    if ($@) {
+       $count++;
+       warn "Test10 e: $@";
+    }
+
+    eval {
+       if (1 == volume_is_base($cfg, "$storagename:$ctbase\/$ctlinked")) {
+           $count++;
+           warn "Test10 f: is no base";
+       }
+
+    };
+    if ($@) {
+       $count++;
+       warn "Test10 f: $@";
+    }
+};
+$tests->{10} = $test10;
+
+my $test9 =sub {
+
+    print "\nrun test9 \"parse_volume_id\"\n";
+
+    eval {
+       my ($store, $disk) = PVE::Storage::parse_volume_id("$storagename:$vmdisk");
+
+       if ($store ne $storagename || $disk ne $vmdisk) {
+           $count++;
+           warn "Test9 a: parsing wrong";
+       }
+
+    };
+    if ($@) {
+       $count++;
+       warn "Test9 a: $@";
+    }
+
+    eval {
+       my ($store, $disk) = PVE::Storage::parse_volume_id("$storagename:$vmbase");
+
+       if ($store ne $storagename || $disk ne $vmbase) {
+           $count++;
+           warn "Test9 b: parsing wrong";
+       }
+
+    };
+    if ($@) {
+       $count++;
+       warn "Test9 b: $@";
+    }
+
+    eval {
+       my ($store, $disk) = PVE::Storage::parse_volume_id("$storagename:$vmbase\/$vmlinked");
+
+       if ($store ne $storagename || $disk ne "$vmbase\/$vmlinked") {
+           $count++;
+           warn "Test9 c: parsing wrong";
+       }
+
+    };
+    if ($@) {
+       $count++;
+       warn "Test9 c: $@";
+    }
+
+    eval {
+       my ($store, $disk) = PVE::Storage::parse_volume_id("$storagename:$ctdisk");
+
+       if ($store ne $storagename || $disk ne $ctdisk) {
+           $count++;
+           warn "Test9 d: parsing wrong";
+       }
+
+    };
+    if ($@) {
+       $count++;
+       warn "Test9 d: $@";
+    }
+
+    eval {
+       my ($store, $disk) = PVE::Storage::parse_volume_id("$storagename:$ctbase");
+
+       if ($store ne $storagename || $disk ne $ctbase) {
+           $count++;
+           warn "Test9 e: parsing wrong";
+       }
+
+    };
+    if ($@) {
+       $count++;
+       warn "Test9 e: $@";
+    }
+
+    eval {
+       my ($store, $disk) = PVE::Storage::parse_volume_id("$storagename:$ctbase\/$ctlinked");
+
+       if ($store ne $storagename || $disk ne "$ctbase\/$ctlinked") {
+           $count++;
+           warn "Test9 f: parsing wrong";
+       }
+
+    };
+    if ($@) {
+       $count++;
+       warn "Test9 f: $@";
+    }
+};
+$tests->{9} = $test9;
+
+my $test8 = sub {
+
+    print "\nrun test8 \"parse_volname\"\n";
+
+    eval {
+       my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) = PVE::Storage::parse_volname($cfg, "$storagename:$vmdisk");
+
+       if ($vtype ne 'images' || $vmid ne '102' ||  $name ne $vmdisk ||
+           defined($basename) || defined($basevmid) || $isBase ||
+           $format ne 'raw') {
+           $count++;
+           warn "Test8 a: parsing wrong";
+       }
+
+    };
+    if ($@) {
+       $count++;
+       warn "Test8 a: $@";
+    }
+
+    eval {
+       my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) = PVE::Storage::parse_volname($cfg, "$storagename:$vmbase");
+
+       if ($vtype ne 'images' || $vmid ne '100' ||  $name ne $vmbase ||
+           defined($basename) || defined($basevmid) || !$isBase ||
+           $format ne 'raw') {
+           $count++;
+           warn "Test8 b: parsing wrong";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test8 b: $@";
+    }
+
+    eval {
+       my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) = PVE::Storage::parse_volname($cfg, "$storagename:$vmbase\/$vmlinked");
+
+       if ($vtype ne 'images' ||  $name ne $vmlinked || $vmid ne '101' ||
+           $basename ne $vmbase || $basevmid ne '100' || $isBase ||
+           $format ne 'raw') {
+           $count++;
+           warn "Test8 c: parsing wrong";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test8 c: $@";
+    }
+
+    eval {
+       my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) = PVE::Storage::parse_volname($cfg, "$storagename:$ctdisk");
+
+       if ($vtype ne 'images' || $vmid ne '202' ||  $name ne $ctdisk ||
+           defined($basename) || defined($basevmid) || $isBase ||
+           $format ne 'subvol') {
+           $count++;
+           warn "Test8 d: parsing wrong";
+       }
+
+    };
+    if ($@) {
+       $count++;
+       warn "Test8 d: $@";
+    }
+
+    eval {
+       my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) = PVE::Storage::parse_volname($cfg, "$storagename:$ctbase");
+       if ($vtype ne 'images' || $vmid ne '200' ||  $name ne $ctbase ||
+           defined($basename) || defined($basevmid) || !$isBase ||
+           $format ne 'subvol') {
+           $count++;
+           warn "Test8 e: parsing wrong";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test8 e: $@";
+    }
+
+    eval {
+       my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) = PVE::Storage::parse_volname($cfg, "$storagename:$ctbase\/$ctlinked");
+
+       if ($vtype ne 'images' ||  $name ne $ctlinked || $vmid ne '201' ||
+           $basename ne $ctbase || $basevmid ne '200' || $isBase ||
+           $format ne 'subvol') {
+           $count++;
+           warn "Test8 f: parsing wrong";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test8 f: $@";
+    }
+};
+$tests->{8} = $test8;
+
+my $test7 = sub {
+
+    print "\nrun test7 \"volume_rollback\"\n";
+
+    my $tmp_guid;
+    my $parse_guid = sub {
+       my ($line) = shift;
+
+       if ( $line =~ m/^Disk identifier \(GUID\)\: (.*)$/ ) {
+           $tmp_guid = $1;
+       }
+    };
+
+    eval {
+       PVE::Storage::activate_volumes($cfg, ["$storagename:$vmdisk"]);
+       run_command("sgdisk --randomize-guids \/dev\/zvol\/$zpath\/$vmdisk", outfunc => $parse_guid);
+       run_command("sgdisk -p \/dev\/zvol\/$zpath\/$vmdisk", outfunc => $parse_guid);
+
+       my $old_guid = $tmp_guid;
+       PVE::Storage::volume_snapshot($cfg, "$storagename:$vmdisk", 'snap1');
+
+       run_command("sgdisk --randomize-guids \/dev\/zvol\/$zpath\/$vmdisk", outfunc => $parse_guid);
+       eval {
+           PVE::Storage::volume_snapshot_rollback($cfg, "$storagename:$vmdisk", 'snap1');
+           PVE::Storage::activate_volumes($cfg, ["$storagename:$vmdisk"]);
+           $tmp_guid = undef;
+           run_command("sgdisk -p \/dev\/zvol\/$zpath\/$vmdisk", outfunc => $parse_guid);
+           if ($old_guid ne $tmp_guid) {
+               $count++;
+               warn "Test7 a: Zvol makes no rollback";
+           }
+       };
+       if ($@) {
+           $count++;
+           warn "Test7 a: $@";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test7 a: $@";
+    }
+    $tmp_guid = undef;
+
+    eval {
+       PVE::Storage::activate_volumes($cfg, ["$storagename:$vmbase"]);
+       run_command("sgdisk --randomize-guids \/dev\/zvol\/$zpath\/$vmbase", outfunc => $parse_guid);
+       run_command("sgdisk -p \/dev\/zvol\/$zpath\/$vmbase", outfunc => $parse_guid);
+
+       my $old_guid = $tmp_guid;
+       PVE::Storage::volume_snapshot($cfg, "$storagename:$vmbase", 'snap1');
+
+       run_command("sgdisk --randomize-guids \/dev\/zvol\/$zpath\/$vmbase", outfunc => $parse_guid);
+       eval {
+           PVE::Storage::volume_snapshot_rollback($cfg, "$storagename:$vmbase", 'snap1');
+           PVE::Storage::activate_volumes($cfg, ["$storagename:$vmbase"]);
+           $tmp_guid = undef;
+           run_command("sgdisk -p \/dev\/zvol\/$zpath\/$vmbase", outfunc => $parse_guid);
+           if ($old_guid ne $tmp_guid) {
+               $count++;
+               warn "Test7 b: Zvol makes no rollback";
+           }
+       };
+       if ($@) {
+           $count++;
+           warn "Test7 b: $@";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test7 b: $@";
+    }
+    $tmp_guid = undef;
+
+    eval {
+       PVE::Storage::activate_volumes($cfg, ["$storagename:$vmbase/$vmlinked"]);
+       run_command("sgdisk --randomize-guids \/dev\/zvol\/$zpath\/$vmlinked", outfunc => $parse_guid);
+       run_command("sgdisk -p \/dev\/zvol\/$zpath\/$vmlinked", outfunc => $parse_guid);
+
+       my $old_guid = $tmp_guid;
+       PVE::Storage::volume_snapshot($cfg, "$storagename:$vmbase\/$vmlinked", 'snap1');
+
+       run_command("sgdisk --randomize-guids \/dev\/zvol\/$zpath\/$vmlinked", outfunc => $parse_guid);
+       eval {
+           PVE::Storage::volume_snapshot_rollback($cfg, "$storagename:$vmbase\/$vmlinked", 'snap1');
+           PVE::Storage::activate_volumes($cfg, ["$storagename:$vmbase/$vmlinked"]);
+           $tmp_guid = undef;
+           run_command("sgdisk -p \/dev\/zvol\/$zpath\/$vmlinked", outfunc => $parse_guid);
+           if ($old_guid ne $tmp_guid) {
+               $count++;
+               warn "Test7 c: Zvol makes no rollback";
+           }
+       };
+       if ($@) {
+           $count++;
+           warn "Test7 c: $@";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test7 c: $@";
+    }
+    $tmp_guid = undef;
+
+    eval {
+       PVE::Storage::volume_snapshot($cfg, "$storagename:$ctdisk", 'snap1');
+
+       run_command("touch \/$mountpoint\/$ctdisk\/test.txt", outfunc => $parse_guid);
+       eval {
+           PVE::Storage::volume_snapshot_rollback($cfg, "$storagename:$ctdisk", 'snap1');
+           eval {
+               run_command("ls \/$mountpoint\/$ctdisk\/test.txt", errofunc => sub {});
+           };
+           if (!$@) {
+               $count++;
+               warn "Test7 d: $@";
+           }
+       };
+       if ($@) {
+           $count++;
+           warn "Test7 d: $@";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test7 d: $@";
+    }
+
+    eval {
+       PVE::Storage::volume_snapshot($cfg, "$storagename:$ctbase", 'snap1');
+
+       run_command("touch \/$mountpoint\/$ctbase\/test.txt", outfunc => $parse_guid);
+       eval {
+           PVE::Storage::volume_snapshot_rollback($cfg, "$storagename:$ctbase", 'snap1');
+           eval {
+               run_command("ls \/$mountpoint\/$ctbase\/test.txt", errofunc => sub {});
+           };
+           if (!$@) {
+               $count++;
+               warn "Test7 e: $@";
+           }
+       };
+       if ($@) {
+           $count++;
+           warn "Test7 e: $@";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test7 f: $@";
+    }
+
+    eval {
+       PVE::Storage::volume_snapshot($cfg, "$storagename:$ctbase/$ctlinked", 'snap1');
+
+       run_command("touch \/$mountpoint\/$ctlinked\/test.txt", outfunc => $parse_guid);
+       eval {
+           PVE::Storage::volume_snapshot_rollback($cfg, "$storagename:$ctbase/$ctlinked", 'snap1');
+           eval {
+               run_command("ls \/$zpath\/$ctlinked\/test.txt", errofunc => sub {});
+           };
+           if (!$@) {
+               $count++;
+               warn "Test7 g: $@";
+           }
+       };
+       if ($@) {
+           $count++;
+           warn "Test7 g: $@";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test7 g: $@";
+    }
+
+    eval {
+       PVE::Storage::volume_snapshot($cfg, "$storagename:$vmdisk", 'snap2');
+
+       eval {
+           PVE::Storage::volume_snapshot_rollback($cfg, "$storagename:$vmdisk", 'snap1');
+       };
+       if (!$@) {
+           $count++;
+           warn "Test7 h: Not allowed to rollback";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test7 h: $@";
+    }
+
+    eval {
+       PVE::Storage::volume_snapshot($cfg, "$storagename:$vmbase", 'snap2');
+
+       eval {
+           PVE::Storage::volume_snapshot_rollback($cfg, "$storagename:$vmbase", 'snap1');
+       };
+       if (!$@) {
+           $count++;
+           warn "Test7 i: Not allowed to rollback";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test7 i: $@";
+    }
+
+    eval {
+       PVE::Storage::volume_snapshot($cfg, "$storagename:$vmbase\/$vmlinked", 'snap2');
+
+       eval {
+           PVE::Storage::volume_snapshot_rollback($cfg, "$storagename:$vmbase\/$vmlinked", 'snap1');
+       };
+       if (!$@) {
+           $count++;
+           warn "Test7 j: Not allowed to rollback";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test7 j: $@";
+    }
+
+    eval {
+       PVE::Storage::volume_snapshot($cfg, "$storagename:$ctdisk", 'snap2');
+
+       eval {
+           PVE::Storage::volume_snapshot_rollback($cfg, "$storagename:$ctdisk", 'snap1');
+       };
+       if (!$@) {
+           $count++;
+           warn "Test7 k: Not allowed to rollback";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test7 k: $@";
+    }
+
+    eval {
+       PVE::Storage::volume_snapshot($cfg, "$storagename:$ctbase", 'snap2');
+
+       eval {
+           PVE::Storage::volume_snapshot_rollback($cfg, "$storagename:$ctbase", 'snap1');
+       };
+       if (!$@) {
+           $count++;
+           warn "Test7 l: Not allowed to rollback";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test7 l: $@";
+    }
+
+    eval {
+       PVE::Storage::volume_snapshot($cfg, "$storagename:$ctbase/$ctlinked", 'snap2');
+
+       eval {
+           PVE::Storage::volume_snapshot_rollback($cfg, "$storagename:$ctbase/$ctlinked", 'snap1');
+       };
+       if (!$@) {
+           $count++;
+           warn "Test7 m: Not allowed to rollback";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test7 m: $@";
+    }
+};
+$tests->{7} = $test7;
+
+my $test6 = sub {
+
+    print "\nrun test6 \"volume_rollback_is_possible\"\n";
+
+    eval {
+       PVE::Storage::volume_snapshot($cfg, "$storagename:$vmdisk", 'snap1');
+
+       my $blockers = [];
+       my $res = PVE::Storage::volume_rollback_is_possible(
+           $cfg,
+           "$storagename:$vmdisk",
+           'snap1',
+           $blockers,
+       );
+       if ($res != 1) {
+           $count++;
+           warn "Test6 a: Rollback should be possible";
+       }
+       if (scalar($blockers->@*) != 0) {
+           $count++;
+           warn "Test6 a: 'blockers' should be empty";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test6 a: $@";
+    }
+
+    eval {
+       PVE::Storage::volume_snapshot($cfg, "$storagename:$vmbase", 'snap1');
+       if ( 1 !=
+            PVE::Storage::volume_rollback_is_possible($cfg, "$storagename:$vmbase", 'snap1')) {
+           $count++;
+           warn "Test6 b: Rollback should be possible";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test6 b: $@";
+    }
+
+    eval {
+       PVE::Storage::volume_snapshot($cfg, "$storagename:$vmlinked", 'snap1');
+       if ( 1 !=
+            PVE::Storage::volume_rollback_is_possible($cfg, "$storagename:$vmbase\/$vmlinked", 'snap1')) {
+           $count++;
+           warn "Test6 c: Rollback should be possible";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test6 c: $@";
+    }
+
+    eval {
+       PVE::Storage::volume_snapshot($cfg, "$storagename:$ctdisk", 'snap1');
+       if ( 1 !=
+            PVE::Storage::volume_rollback_is_possible($cfg, "$storagename:$ctdisk", 'snap1')) {
+           $count++;
+           warn "Test6 d: Rollback should be possible";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test6 d: $@";
+    }
+
+    eval {
+       PVE::Storage::volume_snapshot($cfg, "$storagename:$ctbase", 'snap1');
+       if ( 1 !=
+            PVE::Storage::volume_rollback_is_possible($cfg, "$storagename:$ctbase", 'snap1')) {
+           $count++;
+           warn "Test6 e: Rollback should be possible";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test6 e: $@";
+    }
+
+    eval {
+       PVE::Storage::volume_snapshot($cfg, "$storagename:$ctlinked", 'snap1');
+       if ( 1 !=
+            PVE::Storage::volume_rollback_is_possible($cfg, "$storagename:$ctbase\/$ctlinked", 'snap1')) {
+           $count++;
+           warn "Test6 f: Rollback should be possible";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test6 f: $@";
+    }
+
+    my $blockers = [];
+    eval {
+       PVE::Storage::volume_snapshot($cfg, "$storagename:$vmdisk", 'snap2');
+       PVE::Storage::volume_rollback_is_possible($cfg, "$storagename:$vmdisk", 'snap1', $blockers);
+    };
+    if (!$@) {
+       $count++;
+       warn "Test6 g: Rollback should not be possible";
+    } elsif (scalar($blockers->@*) != 1 || $blockers->[0] ne 'snap2') {
+       $count++;
+       warn "Test6 g: 'blockers' should be ['snap2']";
+    }
+    undef $blockers;
+
+    $blockers = [];
+    eval {
+       PVE::Storage::volume_snapshot($cfg, "$storagename:$vmbase", 'snap2');
+       PVE::Storage::volume_snapshot($cfg, "$storagename:$vmbase", 'snap3');
+       PVE::Storage::volume_rollback_is_possible($cfg, "$storagename:$vmbase", 'snap1', $blockers);
+    };
+    if (!$@) {
+       $count++;
+       warn "Test6 h: Rollback should not be possible";
+    } else {
+       if (scalar($blockers->@*) != 2) {
+           $count++;
+           warn "Test6 g: 'blockers' should contain two elements";
+       }
+       my $blockers_hash = { map { $_ => 1 } $blockers->@* };
+       if (!$blockers_hash->{'snap2'}) {
+           $count++;
+           warn "Test6 g: 'blockers' should contain 'snap2'";
+       }
+       if (!$blockers_hash->{'snap3'}) {
+           $count++;
+           warn "Test6 g: 'blockers' should contain 'snap3'";
+       }
+    }
+    undef $blockers;
+
+    eval {
+       PVE::Storage::volume_snapshot($cfg, "$storagename:$vmlinked", 'snap2');
+       PVE::Storage::volume_rollback_is_possible($cfg, "$storagename:$vmbase\/$vmlinked", 'snap1');
+    };
+    if (!$@) {
+       $count++;
+       warn "Test6 j: Rollback should not be possible";
+    }
+
+    eval {
+       PVE::Storage::volume_snapshot($cfg, "$storagename:$ctdisk", 'snap2');
+       PVE::Storage::volume_rollback_is_possible($cfg, "$storagename:$ctdisk", 'snap1');
+    };
+    if (!$@) {
+       $count++;
+       warn "Test6 k: Rollback should not be possible";
+    }
+
+    eval {
+       PVE::Storage::volume_snapshot($cfg, "$storagename:$ctbase", 'snap2');
+        PVE::Storage::volume_rollback_is_possible($cfg, "$storagename:$ctbase", 'snap1');
+    };
+    if (!$@) {
+       $count++;
+       warn "Test6 l: Rollback should not be possible";
+    }
+
+    eval {
+       PVE::Storage::volume_snapshot($cfg, "$storagename:$ctlinked", 'snap2');
+       PVE::Storage::volume_rollback_is_possible($cfg, "$storagename:$ctbase\/$ctlinked", 'snap1');
+    };
+    if (!$@) {
+       $count++;
+       warn "Test6 m: Rollback should not be possible";
+    }
+};
+$tests->{6} = $test6;
+
+my $test5 = sub {
+
+    print "\nrun test5 \"volume_snapshot_delete\"\n";
+    my $out = sub{my $tmp = shift;};
+
+    eval {
+       run_command("zfs snapshot $zpath\/$vmdisk\@snap");
+       eval{
+           PVE::Storage::volume_snapshot_delete($cfg, "$storagename:$vmdisk", 'snap');
+           eval{
+               run_command("zfs list $zpath\/$vmdisk\@snap", errfunc => $out, outfunc => $out);
+           };
+           if (!$@) {
+               $count++;
+               warn "Test5 a: snapshot still exists";
+           }
+       };
+       if ($@) {
+           $count++;
+           warn "Test5 PVE a: $@";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test5 a: $@";
+    }
+
+    eval {
+       run_command("zfs snapshot $zpath\/$vmbase\@snap");
+       eval{
+           PVE::Storage::volume_snapshot_delete($cfg, "$storagename:$vmbase", 'snap');
+           eval{
+               run_command("zfs list $zpath\/$vmbase\@snap", errmsg => $out, outfunc => $out);
+           };
+           if (!$@) {
+               $count++;
+               warn "Test5 b: snapshot still exists";
+           }
+       };
+       if ($@) {
+           $count++;
+           warn "Test5 PVE b: $@";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test5 b: $@";
+    }
+
+    eval {
+       run_command("zfs snapshot $zpath\/$vmlinked\@snap");
+       eval{
+           PVE::Storage::volume_snapshot_delete($cfg, "$storagename:$vmbase\/$vmlinked", 'snap');
+           eval{
+               run_command("zfs list $zpath\/$vmlinked\@snap", errmsg => $out, outfunc => $out);
+           };
+           if (!$@) {
+               $count++;
+               warn "Test5 c: snapshot still exists";
+           }
+       };
+       if ($@) {
+           $count++;
+           warn "Test5 PVE c: $@";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test5 c: $@";
+    }
+
+    eval {
+       run_command("zfs snapshot $zpath\/$ctdisk\@snap");
+       eval{
+           PVE::Storage::volume_snapshot_delete($cfg, "$storagename:$ctdisk", 'snap');
+           eval{
+               run_command("zfs list $zpath\/$ctdisk\@snap", errmsg => $out, outfunc => $out);
+           };
+           if (!$@) {
+               $count++;
+               warn "Test5 d: snapshot still exists";
+           }
+       };
+       if ($@) {
+           $count++;
+           warn "Test5 PVE d: $@";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test5 d: $@";
+    }
+
+    eval {
+       run_command("zfs snapshot $zpath\/$ctbase\@snap");
+       eval{
+           PVE::Storage::volume_snapshot_delete($cfg, "$storagename:$ctbase", 'snap');
+           eval{
+               run_command("zfs list $zpath\/$ctbase\@snap", errmsg => $out, outfunc => $out);
+           };
+           if (!$@) {
+               $count++;
+               warn "Test5 e: snapshot still exists";
+           }
+       };
+       if ($@) {
+           $count++;
+           warn "Test5 PVE e: $@";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test5 e: $@";
+    }
+
+    eval {
+       run_command("zfs snapshot $zpath\/$ctlinked\@snap");
+       eval{
+           PVE::Storage::volume_snapshot_delete($cfg, "$storagename:$ctbase\/$ctlinked", 'snap');
+           eval{
+               run_command("zfs list $zpath\/$ctlinked\@snap", errmsg => $out, outfunc => $out);
+           };
+           if (!$@) {
+               $count++;
+               warn "Test5 f: snapshot still exists";
+           }
+       };
+       if ($@) {
+           $count++;
+           warn "Test5 PVE f: $@";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test5 f: $@";
+    }
+    print "######Ignore Output if no Test5 g: is included######\n";
+    eval{
+       PVE::Storage::volume_snapshot_delete($cfg, "$storagename:$vmbase", '__base__');
+       eval{
+           run_command("zfs list $zpath\/$vmbase\@__base__", outfunc => $out);
+       };
+       if ($@) {
+           $count++;
+           warn "Test5 g: $@";
+       }
+    };
+    if (!$@) {
+       $count++;
+       warn "Test5 PVE g: snapshot __base__ can be erased";
+    }
+    print "######End Ignore#######\n";
+};
+$tests->{5} = $test5;
+
+my $test4 = sub {
+
+    print "\nrun test4 \"volume_snapshot\"\n";
+    my $out = sub{};
+
+    eval {
+       PVE::Storage::volume_snapshot($cfg, "$storagename:$vmdisk", 'snap');
+       eval{
+           run_command("zfs list $zpath\/$vmdisk\@snap", errmsg => $out, outfunc => $out);
+       };
+       if ($@) {
+           $count++;
+           warn "Test4 a: $@";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test4 a: $@";
+    }
+
+    eval {
+       PVE::Storage::volume_snapshot($cfg, "$storagename:$vmbase", 'snap');
+       eval{
+           run_command("zfs list $zpath\/$vmbase\@snap", errmsg => $out, outfunc => $out);
+       };
+       if ($@) {
+           $count++;
+           warn "Test4 b: $@";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test4 c: $@";
+    }
+
+    eval {
+       PVE::Storage::volume_snapshot($cfg, "$storagename:$vmbase\/$vmlinked", 'snap');
+       eval{
+           run_command("zfs list $zpath\/$vmdisk\@snap", errmsg => $out, outfunc => $out);
+       };
+       if ($@) {
+           $count++;
+           warn "Test4 c: $@";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test4 c: $@";
+    }
+
+    eval {
+       PVE::Storage::volume_snapshot($cfg, "$storagename:$ctdisk", 'snap');
+       eval{
+           run_command("zfs list $zpath\/$ctdisk\@snap", errmsg => $out, outfunc => $out);
+       };
+       if ($@) {
+           $count++;
+           warn "Test4 d: $@";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test4 d: $@";
+    }
+
+    eval {
+       PVE::Storage::volume_snapshot($cfg, "$storagename:$ctbase", 'snap');
+       eval{
+           run_command("zfs list $zpath\/$ctbase\@snap", errmsg => $out, outfunc => $out);
+       };
+       if ($@) {
+           $count++;
+           warn "Test4 e: $@";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test4 e: $@";
+    }
+
+    eval {
+       PVE::Storage::volume_snapshot($cfg, "$storagename:$ctbase\/$ctlinked", 'snap');
+       eval{
+           run_command("zfs list $zpath\/$ctdisk\@snap", errmsg => $out, outfunc => $out);
+       };
+       if ($@) {
+           $count++;
+           warn "Test4 f: $@";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test4 f: $@";
+    }
+};
+$tests->{4} = $test4;
+
+my $test3 = sub {
+
+    print "\nrun test3 \"volume_has_feature\"\n";
+
+    eval {
+       if (!PVE::Storage::volume_has_feature($cfg, 'snapshot', "$storagename:$vmdisk", undef, 0)) {
+           $count++;
+           warn "Test3 a failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 a: $@";
+    }
+
+    eval {
+       if (PVE::Storage::volume_has_feature($cfg, 'snapshot', "$storagename:$vmbase", undef, 0)) {
+           $count++;
+           warn "Test3 b failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 b: $@";
+    }
+
+    eval {
+       if (!PVE::Storage::volume_has_feature($cfg, 'snapshot', "$storagename:$vmbase\/$vmlinked", undef, 0)) {
+           $count++;
+           warn "Test3 c failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 c: $@";
+    }
+
+    eval {
+       if (!PVE::Storage::volume_has_feature($cfg, 'snapshot', "$storagename:$ctdisk", undef, 0)) {
+           $count++;
+           warn "Test3 d failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 d: $@";
+    }
+
+    eval {
+       if (PVE::Storage::volume_has_feature($cfg, 'snapshot', "$storagename:$ctbase", undef, 0)) {
+           $count++;
+           warn "Test3 e failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 e: $@";
+    }
+
+    eval {
+       if (!PVE::Storage::volume_has_feature($cfg, 'snapshot', "$storagename:$ctbase\/$ctlinked", undef, 0)) {
+           $count++;
+           warn "Test3 f failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 f: $@";
+    }
+
+    eval {
+       if (PVE::Storage::volume_has_feature($cfg, 'clone', "$storagename:$vmdisk", undef, 0)) {
+           $count++;
+           warn "Test3 g failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 g: $@";
+    }
+
+    eval {
+       if (!PVE::Storage::volume_has_feature($cfg, 'clone', "$storagename:$vmbase", undef, 0)) {
+           $count++;
+           warn "Test3 h failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 h: $@";
+    }
+
+    eval {
+       if (PVE::Storage::volume_has_feature($cfg, 'clone', "$storagename:$vmbase\/$vmlinked", undef, 0)) {
+           $count++;
+           warn "Test3 h failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 h: $@";
+    }
+
+    eval {
+       if (PVE::Storage::volume_has_feature($cfg, 'clone', "$storagename:$ctdisk", undef, 0)) {
+           $count++;
+           warn "Test3 i failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 i: $@";
+    }
+
+    eval {
+       if (!PVE::Storage::volume_has_feature($cfg, 'clone', "$storagename:$ctbase", undef, 0)) {
+           $count++;
+           warn "Test3 j failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 j: $@";
+    }
+
+    eval {
+       if (PVE::Storage::volume_has_feature($cfg, 'clone', "$storagename:$ctbase\/$ctlinked", undef, 0)) {
+           $count++;
+           warn "Test3 k failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 k: $@";
+    }
+
+    eval {
+       if (!PVE::Storage::volume_has_feature($cfg, 'template', "$storagename:$vmdisk", undef, 0)) {
+           $count++;
+           warn "Test3 l failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 l: $@";
+    }
+
+    eval {
+       if (PVE::Storage::volume_has_feature($cfg, 'template', "$storagename:$vmbase", undef, 0)) {
+           $count++;
+           warn "Test3 m failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 m: $@";
+    }
+
+    eval {
+       if (!PVE::Storage::volume_has_feature($cfg, 'template', "$storagename:$vmbase\/$vmlinked", undef, 0)) {
+           $count++;
+           warn "Test3 n failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 n: $@";
+    }
+
+    eval {
+       if (!PVE::Storage::volume_has_feature($cfg, 'template', "$storagename:$ctdisk", undef, 0)) {
+           $count++;
+           warn "Test3 o failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 o: $@";
+    }
+
+    eval {
+       if (PVE::Storage::volume_has_feature($cfg, 'template', "$storagename:$ctbase", undef, 0)) {
+           $count++;
+           warn "Test3 p failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 p: $@";
+    }
+
+    eval {
+       if (!PVE::Storage::volume_has_feature($cfg, 'template', "$storagename:$ctbase\/$ctlinked", undef, 0)) {
+           $count++;
+           warn "Test3 q failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 q: $@";
+    }
+
+    eval {
+       if (!PVE::Storage::volume_has_feature($cfg, 'copy', "$storagename:$vmdisk", undef, 0)) {
+           $count++;
+           warn "Test3 r failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 r: $@";
+    }
+
+    eval {
+       if (!PVE::Storage::volume_has_feature($cfg, 'copy', "$storagename:$vmbase", undef, 0)) {
+           $count++;
+           warn "Test3 s failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 s: $@";
+    }
+
+    eval {
+       if (!PVE::Storage::volume_has_feature($cfg, 'copy', "$storagename:$vmbase\/$vmlinked", undef, 0)) {
+           $count++;
+           warn "Test3 t failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 t: $@";
+    }
+
+    eval {
+       if (!PVE::Storage::volume_has_feature($cfg, 'copy', "$storagename:$ctdisk", undef, 0)) {
+           $count++;
+           warn "Test3 u failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 u: $@";
+    }
+
+    eval {
+       if (!PVE::Storage::volume_has_feature($cfg, 'copy', "$storagename:$ctbase", undef, 0)) {
+           $count++;
+           warn "Test3 v failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 v: $@";
+    }
+
+    eval {
+       if (!PVE::Storage::volume_has_feature($cfg, 'copy', "$storagename:$ctbase\/$ctlinked", undef, 0)) {
+           $count++;
+           warn "Test3 w failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 w: $@";
+    }
+
+    eval {
+       if (!PVE::Storage::volume_has_feature($cfg, 'sparseinit', "$storagename:$vmdisk", undef, 0)) {
+           $count++;
+           warn "Test3 x failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 x: $@";
+    }
+
+    eval {
+       if (!PVE::Storage::volume_has_feature($cfg, 'sparseinit', "$storagename:$vmbase", undef, 0)) {
+           $count++;
+           warn "Test3 y failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 y: $@";
+    }
+
+    eval {
+       if (!PVE::Storage::volume_has_feature($cfg, 'sparseinit', "$storagename:$vmbase\/$vmlinked", undef, 0)) {
+           $count++;
+           warn "Test3 z failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 z: $@";
+    }
+
+    eval {
+       if (!PVE::Storage::volume_has_feature($cfg, 'sparseinit', "$storagename:$ctdisk", undef, 0)) {
+           $count++;
+           warn "Test3 A failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 A: $@";
+    }
+
+    eval {
+       if (!PVE::Storage::volume_has_feature($cfg, 'sparseinit', "$storagename:$ctbase", undef, 0)) {
+           $count++;
+           warn "Test3 B failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 B: $@";
+    }
+
+    eval {
+       if (!PVE::Storage::volume_has_feature($cfg, 'sparseinit', "$storagename:$ctbase\/$ctlinked", undef, 0)) {
+           $count++;
+           warn "Test3 C failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 C: $@";
+    }
+
+    eval {
+       if (!PVE::Storage::volume_has_feature($cfg, 'snapshot', "$storagename:$vmdisk", 'test', 0)) {
+           $count++;
+           warn "Test3 a1 failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 a1: $@";
+    }
+
+    eval {
+       if (!PVE::Storage::volume_has_feature($cfg, 'snapshot', "$storagename:$vmbase", 'test', 0)) {
+           $count++;
+           warn "Test3 b1 failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 b1: $@";
+    }
+
+    eval {
+       if (!PVE::Storage::volume_has_feature($cfg, 'snapshot', "$storagename:$vmbase\/$vmlinked", 'test', 0)) {
+           $count++;
+           warn "Test3 c1 failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 c1: $@";
+    }
+
+    eval {
+       if (!PVE::Storage::volume_has_feature($cfg, 'snapshot', "$storagename:$ctdisk", 'test', 0)) {
+           $count++;
+           warn "Test3 d1 failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 d1: $@";
+    }
+
+    eval {
+       if (!PVE::Storage::volume_has_feature($cfg, 'snapshot', "$storagename:$ctbase", 'test', 0)) {
+           $count++;
+           warn "Test3 e1 failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 e1: $@";
+    }
+
+    eval {
+       if (!PVE::Storage::volume_has_feature($cfg, 'snapshot', "$storagename:$ctbase\/$ctlinked", 'test', 0)) {
+           $count++;
+           warn "Test3 f1 failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 f1: $@";
+    }
+
+    eval {
+       if (PVE::Storage::volume_has_feature($cfg, 'clone', "$storagename:$vmdisk", 'test', 0)) {
+           $count++;
+           warn "Test3 g1 failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 g1: $@";
+    }
+
+    eval {
+       if (PVE::Storage::volume_has_feature($cfg, 'clone', "$storagename:$vmbase", 'test', 0)) {
+           $count++;
+           warn "Test3 h1 failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 h1: $@";
+    }
+
+    eval {
+       if (PVE::Storage::volume_has_feature($cfg, 'clone', "$storagename:$vmbase\/$vmlinked", 'test', 0)) {
+           $count++;
+           warn "Test3 h1 failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 h1: $@";
+    }
+
+    eval {
+       if (PVE::Storage::volume_has_feature($cfg, 'clone', "$storagename:$ctdisk", 'test', 0)) {
+           $count++;
+           warn "Test3 i1 failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 i1: $@";
+    }
+
+    eval {
+       if (PVE::Storage::volume_has_feature($cfg, 'clone', "$storagename:$ctbase", 'test', 0)) {
+           $count++;
+           warn "Test3 j1 failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 j1: $@";
+    }
+
+    eval {
+       if (PVE::Storage::volume_has_feature($cfg, 'clone', "$storagename:$ctbase\/$ctlinked", 'test', 0)) {
+           $count++;
+           warn "Test3 k1 failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 k1: $@";
+    }
+
+    eval {
+       if (PVE::Storage::volume_has_feature($cfg, 'template', "$storagename:$vmdisk", 'test', 0)) {
+           $count++;
+           warn "Test3 l1 failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 l1: $@";
+    }
+
+    eval {
+       if (PVE::Storage::volume_has_feature($cfg, 'template', "$storagename:$vmbase", 'test', 0)) {
+           $count++;
+           warn "Test3 m1 failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 m1: $@";
+    }
+
+    eval {
+       if (PVE::Storage::volume_has_feature($cfg, 'template', "$storagename:$vmbase\/$vmlinked", 'test', 0)) {
+           $count++;
+           warn "Test3 n1 failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 n1: $@";
+    }
+
+    eval {
+       if (PVE::Storage::volume_has_feature($cfg, 'template', "$storagename:$ctdisk", 'test', 0)) {
+           $count++;
+           warn "Test3 o1 failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 o1: $@";
+    }
+
+    eval {
+       if (PVE::Storage::volume_has_feature($cfg, 'template', "$storagename:$ctbase", 'test', 0)) {
+           $count++;
+           warn "Test3 p1 failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 p1: $@";
+    }
+
+    eval {
+       if (PVE::Storage::volume_has_feature($cfg, 'template', "$storagename:$ctbase\/$ctlinked", 'test', 0)) {
+           $count++;
+           warn "Test3 q1 failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 q1: $@";
+    }
+
+    eval {
+       if (PVE::Storage::volume_has_feature($cfg, 'copy', "$storagename:$vmdisk", 'test', 0)) {
+           $count++;
+           warn "Test3 r1 failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 r1: $@";
+    }
+
+    eval {
+       if (PVE::Storage::volume_has_feature($cfg, 'copy', "$storagename:$vmbase", 'test', 0)) {
+           $count++;
+           warn "Test3 s1 failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 s1: $@";
+    }
+
+    eval {
+       if (PVE::Storage::volume_has_feature($cfg, 'copy', "$storagename:$vmbase\/$vmlinked", 'test', 0)) {
+           $count++;
+           warn "Test3 t1 failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 t1: $@";
+    }
+
+    eval {
+       if (PVE::Storage::volume_has_feature($cfg, 'copy', "$storagename:$ctdisk", 'test', 0)) {
+           $count++;
+           warn "Test3 u1 failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 u1: $@";
+    }
+
+    eval {
+       if (PVE::Storage::volume_has_feature($cfg, 'copy', "$storagename:$ctbase", 'test', 0)) {
+           $count++;
+           warn "Test3 v1 failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 v1: $@";
+    }
+
+    eval {
+       if (PVE::Storage::volume_has_feature($cfg, 'copy', "$storagename:$ctbase\/$ctlinked", 'test', 0)) {
+           $count++;
+           warn "Test3 w1 failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 w1: $@";
+    }
+
+    eval {
+       if (PVE::Storage::volume_has_feature($cfg, 'sparseinit', "$storagename:$vmdisk", 'test', 0)) {
+           $count++;
+           warn "Test3 x1 failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 x1: $@";
+    }
+
+    eval {
+       if (PVE::Storage::volume_has_feature($cfg, 'sparseinit', "$storagename:$vmbase", 'test', 0)) {
+           $count++;
+           warn "Test3 y1 failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 y1: $@";
+    }
+
+    eval {
+       if (PVE::Storage::volume_has_feature($cfg, 'sparseinit', "$storagename:$vmbase\/$vmlinked", 'test', 0)) {
+           $count++;
+           warn "Test3 z1 failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 z1: $@";
+    }
+
+    eval {
+       if (PVE::Storage::volume_has_feature($cfg, 'sparseinit', "$storagename:$ctdisk", 'test', 0)) {
+           $count++;
+           warn "Test3 A1 failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 A1: $@";
+    }
+
+    eval {
+       if (PVE::Storage::volume_has_feature($cfg, 'sparseinit', "$storagename:$ctbase", 'test', 0)) {
+           $count++;
+           warn "Test3 B1 failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 B1: $@";
+    }
+
+    eval {
+       if (PVE::Storage::volume_has_feature($cfg, 'sparseinit', "$storagename:$ctbase\/$ctlinked", 'test', 0)) {
+           $count++;
+           warn "Test3 C1 failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test3 C1: $@";
+    }
+};
+$tests->{3} = $test3;
+
+my $test2 = sub {
+
+    print "\nrun test2 \"volume_resize\"\n";
+    my $newsize = ($volsize + 1) * 1024 * 1024 * 1024;
+
+    eval {
+       if (($newsize/1024) !=
+           PVE::Storage::volume_resize($cfg, "$storagename:$vmdisk", $newsize, 0)) {
+           $count++;
+           warn "Test2 a failed";
+       }
+       if ($newsize  !=
+           PVE::Storage::volume_size_info($cfg, "$storagename:$vmdisk")) {
+           $count++;
+           warn "Test2 a failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test2 a: $@";
+    }
+
+    eval {
+       warn "Test2 b failed" if ($newsize/1024) != PVE::Storage::volume_resize($cfg, "$storagename:$vmbase", $newsize, 0);
+       warn "Test2 b failed" if $newsize  !=
+           PVE::Storage::volume_size_info($cfg, "$storagename:$vmbase");
+    };
+    if ($@) {
+       $count++;
+       warn "Test2 b: $@";
+    }
+
+    eval {
+       if (($newsize/1024) != PVE::Storage::volume_resize($cfg, "$storagename:$vmbase\/$vmlinked", $newsize, 0)) {
+           $count++;
+           warn "Test2 c failed";
+       }
+       if ($newsize  !=
+           PVE::Storage::volume_size_info($cfg, "$storagename:$vmbase\/$vmlinked")) {
+           $count++;
+           warn "Test2 c failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test2 c: $@";
+    }
+
+    eval {
+       if (($newsize/1024) != PVE::Storage::volume_resize($cfg, "$storagename:$ctdisk", $newsize, 0)) {
+           $count++;
+           warn "Test2 d failed";
+       }
+       if ($newsize  !=
+           PVE::Storage::volume_size_info($cfg, "$storagename:$ctdisk")) {
+           $count++;
+           warn "Test2 d failed"
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test2 d: $@";
+    }
+
+    eval {
+       if (($newsize/1024) !=
+           PVE::Storage::volume_resize($cfg, "$storagename:$ctbase", $newsize, 0)) {
+           $count++;
+           warn "Test2 e failed";
+       }
+       if ($newsize  !=
+           PVE::Storage::volume_size_info($cfg, "$storagename:$ctbase")) {
+           $count++;
+           warn "Test2 e failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test2 e: $@";
+    }
+
+    eval {
+       if (($newsize/1024) !=
+           PVE::Storage::volume_resize($cfg, "$storagename:$ctbase\/$ctlinked", $newsize, 0)) {
+           $count++;
+           warn "Test2 f failed";
+       }
+       if ($newsize  !=
+           PVE::Storage::volume_size_info($cfg, "$storagename:$ctbase\/$ctlinked")) {
+           $count++;
+           warn "Test2 f failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test2 f: $@";
+    }
+};
+$tests->{2} = $test2;
+
+my $test1 = sub {
+
+    print "\nrun test1 \"volume_size_info\"\n";
+    my $size = ($volsize * 1024 * 1024 * 1024);
+
+    eval {
+       if ($size != PVE::Storage::volume_size_info($cfg, "$storagename:$vmdisk")) {
+           $count++;
+           warn "Test1 a failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test1 a : $@";
+    }
+
+    eval {
+       if ($size != PVE::Storage::volume_size_info($cfg, "$storagename:$vmbase")) {
+           $count++;
+           warn "Test1 b failed";
+       }
+
+    };
+    if ($@) {
+       $count++;
+       warn "Test1 b : $@";
+    }
+
+    eval {
+       if ($size !=
+           PVE::Storage::volume_size_info($cfg, "$storagename:$vmbase\/$vmlinked")) {
+           $count++;
+           warn "Test1 c failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test1 c : $@";
+    }
+
+    eval {
+       if ($size !=
+           PVE::Storage::volume_size_info($cfg, "$storagename:$ctdisk")) {
+           $count++;
+           warn "Test1 d failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test1 d : $@";
+    }
+
+    eval {
+       if ($size !=
+           PVE::Storage::volume_size_info($cfg, "$storagename:$ctbase")) {
+           $count++;
+           warn "Test1 e failed";
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test1 e : $@";
+    }
+
+    eval {
+       if ($size !=
+           PVE::Storage::volume_size_info($cfg, "$storagename:$vmbase\/$vmlinked")) {
+           $count++;
+           warn "Test1 f failed"
+       }
+    };
+    if ($@) {
+       $count++;
+       warn "Test1 f : $@";
+    }
+
+};
+$tests->{1} = $test1;
+
+sub setup_zfs {
+    #create VM zvol
+    print "create zvol $vmdisk\n" if $verbose;
+    run_command("zfs create -V${volsize}G $zpath\/$vmdisk");
+
+    print "create zvol $vmbase\n" if $verbose;
+    run_command("zfs create -V${volsize}G $zpath\/$vmbase");
+    run_command("zfs snapshot $zpath\/$vmbase$basesnap");
+
+    print "create linked clone $vmlinked\n" if $verbose;
+    run_command("zfs clone $zpath\/$vmbase$basesnap $zpath\/$vmlinked");
+
+    #create CT subvol
+    print "create subvol $ctdisk\n" if $verbose;
+    run_command("zfs create -o refquota=${volsize}G $zpath\/$ctdisk");
+
+    print "create subvol $ctbase\n" if $verbose;
+    run_command("zfs create -o refquota=${volsize}G $zpath\/$ctbase");
+    run_command("zfs snapshot $zpath\/$ctbase$basesnap");
+
+    print "create linked clone $ctlinked\n" if $verbose;
+    run_command("zfs clone $zpath\/$ctbase$basesnap $zpath\/$ctlinked -o refquota=${volsize}G");
+
+    my $vollist = [
+       "$storagename:$vmdisk",
+       "$storagename:$vmbase",
+       "$storagename:$vmbase/$vmlinked",
+       "$storagename:$ctdisk",
+       "$storagename:$ctbase",
+       "$storagename:$ctbase/$ctlinked",
+    ];
+
+    PVE::Storage::activate_volumes($cfg, $vollist);
+}
+
+sub cleanup_zfs {
+
+    print "destroy $pool\/$subvol\n" if $verbose;
+    eval { run_command("zfs destroy $zpath -r"); };
+    if ($@) {
+       print "cleanup failed: $@\nretrying once\n" if $verbose;
+       eval { run_command("zfs destroy $zpath -r"); };
+       if ($@) {
+           clean_up_zpool();
+           setup_zpool();
+       }
+    }
+}
+
+sub setup_zpool {
+
+    unlink 'zpool.img';
+    eval {
+       run_command("truncate -s 8G zpool.img");
+    };
+    if ($@) {
+       clean_up_zpool();
+    }
+    my $pwd = cwd();
+    eval {
+       run_command("zpool create -m \/$mountpoint $subvol $pwd\/zpool.img");
+    };
+    if ($@) {
+       clean_up_zpool();
+    }
+}
+
+sub clean_up_zpool {
+
+    eval {
+       run_command("zpool destroy -f $subvol");
+    };
+    if ($@) {
+       warn $@;}
+    unlink 'zpool.img';
+}
+
+sub volume_is_base {
+    my ($cfg, $volid) = @_;
+
+    my (undef, undef, undef, undef, undef, $isBase, undef) = PVE::Storage::parse_volname($cfg, $volid);
+
+    return $isBase;
+}
+
+if ($> != 0) { #EUID
+    warn "not root, skipping zfs tests\n";
+    exit 0;
+}
+
+eval { run_command("zpool status"); };
+if ($@) {
+    warn "zpool status failed, not running tests: $@\n";
+    exit 0;
+}
+
+setup_zpool();
+
+my $time = time;
+print "Start tests for ZFSPoolPlugin\n";
+
+$cfg = {'ids' => {
+    $storagename => {
+       'content' => {
+           'images' => 1,
+           'rootdir' => 1
+       },
+               'pool' => $subvol,
+               'mountpoint' => "\/$mountpoint",
+               'type' => 'zfspool'
+    },
+       },
+               'order' => {'zfstank99' => 1,}
+};
+
+$zpath = $subvol;
+
+for (my $i = $start_test; $i <= $end_test; $i++) {
+    setup_zfs();
+
+    eval {
+       $tests->{$i}();
+    };
+    if (my $err = $@) {
+       warn $err;
+       $count++;
+    }
+    cleanup_zfs();
+}
+
+clean_up_zpool();
+
+$time = time - $time;
+
+print "Stop tests for ZFSPoolPlugin\n";
+print "$count tests failed\n";
+print "Time: ${time}s\n";
+
+exit -1 if $count > 0;
diff --git a/src/udev-rbd/50-rbd-pve.rules b/src/udev-rbd/50-rbd-pve.rules
new file mode 100644 (file)
index 0000000..79432df
--- /dev/null
@@ -0,0 +1,2 @@
+KERNEL=="rbd[0-9]*", ENV{DEVTYPE}=="disk", PROGRAM="/usr/libexec/ceph-rbdnamer-pve %k", SYMLINK+="rbd-pve/%c"
+KERNEL=="rbd[0-9]*", ENV{DEVTYPE}=="partition", PROGRAM="/usr/libexec/ceph-rbdnamer-pve %k", SYMLINK+="rbd-pve/%c-part%n"
diff --git a/src/udev-rbd/Makefile b/src/udev-rbd/Makefile
new file mode 100644 (file)
index 0000000..065933b
--- /dev/null
@@ -0,0 +1,21 @@
+PACKAGE=libpve-storage-perl
+
+DESTDIR=
+PREFIX=/usr
+LIBEXECDIR=${PREFIX}/libexec
+LIBDIR=${PREFIX}/lib
+
+all:
+
+.PHONY: install
+install: 50-rbd-pve.rules ceph-rbdnamer-pve
+       install -d ${DESTDIR}${LIBEXECDIR}
+       install -m 0755 ceph-rbdnamer-pve ${DESTDIR}${LIBEXECDIR}
+       install -d ${DESTDIR}${LIBDIR}/udev/rules.d
+       install -m 0644 50-rbd-pve.rules ${DESTDIR}${LIBDIR}/udev/rules.d
+
+.PHONY: clean
+clean:
+
+.PHONY: distclean
+distclean: clean
diff --git a/src/udev-rbd/ceph-rbdnamer-pve b/src/udev-rbd/ceph-rbdnamer-pve
new file mode 100755 (executable)
index 0000000..23dd626
--- /dev/null
@@ -0,0 +1,24 @@
+#!/bin/sh
+
+DEV=$1
+NUM=`echo $DEV | sed 's#p.*##g; s#[a-z]##g'`
+POOL=`cat /sys/devices/rbd/$NUM/pool`
+CLUSTER_FSID=`cat /sys/devices/rbd/$NUM/cluster_fsid`
+
+if [ -f /sys/devices/rbd/$NUM/pool_ns ]; then
+    NAMESPACE=`cat /sys/devices/rbd/$NUM/pool_ns`
+else
+    NAMESPACE=""
+fi
+IMAGE=`cat /sys/devices/rbd/$NUM/name`
+SNAP=`cat /sys/devices/rbd/$NUM/current_snap`
+
+echo -n "/$CLUSTER_FSID/$POOL"
+
+if [ -n "$NAMESPACE" ]; then
+    echo -n "/$NAMESPACE"
+fi
+echo -n "/$IMAGE"
+if [ "$SNAP" != "-" ]; then
+    echo -n "@$SNAP"
+fi
diff --git a/test/Makefile b/test/Makefile
deleted file mode 100644 (file)
index c54b10f..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-all: test
-
-test: test_zfspoolplugin test_disklist test_bwlimit test_plugin
-
-test_zfspoolplugin: run_test_zfspoolplugin.pl
-       ./run_test_zfspoolplugin.pl
-
-test_disklist: run_disk_tests.pl
-       ./run_disk_tests.pl
-
-test_bwlimit: run_bwlimit_tests.pl
-       ./run_bwlimit_tests.pl
-
-test_plugin: run_plugin_tests.pl
-       ./run_plugin_tests.pl
diff --git a/test/archive_info_test.pm b/test/archive_info_test.pm
deleted file mode 100644 (file)
index cf03c3d..0000000
+++ /dev/null
@@ -1,198 +0,0 @@
-package PVE::Storage::TestArchiveInfo;
-
-use strict;
-use warnings;
-
-use lib qw(..);
-
-use PVE::Storage;
-use Test::More;
-
-my $vmid = 16110;
-
-my $LOG_EXT = PVE::Storage::Plugin::LOG_EXT;
-my $NOTES_EXT = PVE::Storage::Plugin::NOTES_EXT;
-
-# an array of test cases, each test is comprised of the following keys:
-# description => to identify a single test
-# archive     => the input filename for archive_info
-# expected    => the hash that archive_info returns
-#
-# most of them are created further below
-my $tests = [
-    # backup archives
-    {
-       description => 'Backup archive, lxc, tgz, future millenium',
-       archive     => "backup/vzdump-lxc-$vmid-3070_01_01-00_00_00.tgz",
-       expected    => {
-           'filename'     => "vzdump-lxc-$vmid-3070_01_01-00_00_00.tgz",
-           'logfilename'  => "vzdump-lxc-$vmid-3070_01_01-00_00_00".$LOG_EXT,
-           'notesfilename'=> "vzdump-lxc-$vmid-3070_01_01-00_00_00.tgz".$NOTES_EXT,
-           'type'         => 'lxc',
-           'format'       => 'tar',
-           'decompressor' => ['tar', '-z'],
-           'compression'  => 'gz',
-           'vmid'         => $vmid,
-           'ctime'        => 60*60*24 * (365*1100 + 267),
-           'is_std_name'  => 1,
-       },
-    },
-    {
-       description => 'Backup archive, lxc, tgz, very old',
-       archive     => "backup/vzdump-lxc-$vmid-1970_01_01-02_00_30.tgz",
-       expected    => {
-           'filename'     => "vzdump-lxc-$vmid-1970_01_01-02_00_30.tgz",
-           'logfilename'  => "vzdump-lxc-$vmid-1970_01_01-02_00_30".$LOG_EXT,
-           'notesfilename'=> "vzdump-lxc-$vmid-1970_01_01-02_00_30.tgz".$NOTES_EXT,
-           'type'         => 'lxc',
-           'format'       => 'tar',
-           'decompressor' => ['tar', '-z'],
-           'compression'  => 'gz',
-           'vmid'         => $vmid,
-           'ctime'        => 60*60*2 + 30,
-           'is_std_name'  => 1,
-       },
-    },
-    {
-       description => 'Backup archive, lxc, tgz',
-       archive     => "backup/vzdump-lxc-$vmid-2020_03_30-21_39_30.tgz",
-       expected    => {
-           'filename'     => "vzdump-lxc-$vmid-2020_03_30-21_39_30.tgz",
-           'logfilename'  => "vzdump-lxc-$vmid-2020_03_30-21_39_30".$LOG_EXT,
-           'notesfilename'=> "vzdump-lxc-$vmid-2020_03_30-21_39_30.tgz".$NOTES_EXT,
-           'type'         => 'lxc',
-           'format'       => 'tar',
-           'decompressor' => ['tar', '-z'],
-           'compression'  => 'gz',
-           'vmid'         => $vmid,
-           'ctime'        => 1585604370,
-           'is_std_name'  => 1,
-       },
-    },
-    {
-       description => 'Backup archive, openvz, tgz',
-       archive     => "backup/vzdump-openvz-$vmid-2020_03_30-21_39_30.tgz",
-       expected    => {
-           'filename'     => "vzdump-openvz-$vmid-2020_03_30-21_39_30.tgz",
-           'logfilename'  => "vzdump-openvz-$vmid-2020_03_30-21_39_30".$LOG_EXT,
-           'notesfilename'=> "vzdump-openvz-$vmid-2020_03_30-21_39_30.tgz".$NOTES_EXT,
-           'type'         => 'openvz',
-           'format'       => 'tar',
-           'decompressor' => ['tar', '-z'],
-           'compression'  => 'gz',
-           'vmid'         => $vmid,
-           'ctime'        => 1585604370,
-           'is_std_name'  => 1,
-       },
-    },
-    {
-       description => 'Backup archive, custom dump directory, qemu, tgz',
-       archive     => "/here/be/Back-ups/vzdump-qemu-$vmid-2020_03_30-21_39_30.tgz",
-       expected    => {
-           'filename'     => "vzdump-qemu-$vmid-2020_03_30-21_39_30.tgz",
-           'logfilename'  => "vzdump-qemu-$vmid-2020_03_30-21_39_30".$LOG_EXT,
-           'notesfilename'=> "vzdump-qemu-$vmid-2020_03_30-21_39_30.tgz".$NOTES_EXT,
-           'type'         => 'qemu',
-           'format'       => 'tar',
-           'decompressor' => ['tar', '-z'],
-           'compression'  => 'gz',
-           'vmid'         => $vmid,
-           'ctime'        => 1585604370,
-           'is_std_name'  => 1,
-       },
-    },
-    {
-       description => 'Backup archive, none, tgz',
-       archive     => "backup/vzdump-qemu-$vmid-whatever-the-name_is_here.tgz",
-       expected    => {
-           'filename'     => "vzdump-qemu-$vmid-whatever-the-name_is_here.tgz",
-           'type'         => 'qemu',
-           'format'       => 'tar',
-           'decompressor' => ['tar', '-z'],
-           'compression'  => 'gz',
-           'is_std_name'  => 0,
-       },
-    },
-];
-
-# add new compression fromats to test
-my $decompressor = {
-    tar => {
-       gz  => ['tar', '-z'],
-       lzo => ['tar', '--lzop'],
-       zst => ['tar', '--zstd'],
-    },
-    vma => {
-       gz  => ['zcat'],
-       lzo => ['lzop', '-d', '-c'],
-       zst => ['zstd', '-q', '-d', '-c'],
-    },
-};
-
-my $bkp_suffix = {
-    qemu   => [ 'vma', $decompressor->{vma}, ],
-    lxc    => [ 'tar', $decompressor->{tar}, ],
-    openvz => [ 'tar', $decompressor->{tar}, ],
-};
-
-# create more test cases for backup files matches
-for my $virt (sort keys %$bkp_suffix) {
-    my ($format, $decomp) = $bkp_suffix->{$virt}->@*;
-    my $archive_name = "vzdump-$virt-$vmid-2020_03_30-21_12_40";
-
-    for my $suffix (sort keys %$decomp) {
-       push @$tests, {
-           description => "Backup archive, $virt, $format.$suffix",
-           archive     => "backup/$archive_name.$format.$suffix",
-           expected    => {
-               'filename'     => "$archive_name.$format.$suffix",
-               'logfilename'  => $archive_name.$LOG_EXT,
-               'notesfilename'=> "$archive_name.$format.$suffix".$NOTES_EXT,
-               'type'         => "$virt",
-               'format'       => "$format",
-               'decompressor' => $decomp->{$suffix},
-               'compression'  => "$suffix",
-               'vmid'         => $vmid,
-               'ctime'        => 1585602760,
-               'is_std_name'  => 1,
-           },
-       };
-    }
-}
-
-
-# add compression formats to test failed matches
-my $non_bkp_suffix = {
-    'openvz' => [ 'zip', 'tgz.lzo', 'tar.bz2', 'zip.gz', '', ],
-    'lxc'    => [ 'zip', 'tgz.lzo', 'tar.bz2', 'zip.gz', '', ],
-    'qemu'   => [ 'vma.xz', 'vms.gz', 'vmx.zst', '', ],
-    'none'   => [ 'tar.gz', ],
-};
-
-# create tests for failed matches
-for my $virt (sort keys %$non_bkp_suffix) {
-    my $suffix = $non_bkp_suffix->{$virt};
-    for my $s (@$suffix) {
-       my $archive = "backup/vzdump-$virt-$vmid-2020_03_30-21_12_40.$s";
-       push @$tests, {
-           description => "Failed match: Backup archive, $virt, $s",
-           archive     => $archive,
-           expected    => "ERROR: couldn't determine archive info from '$archive'\n",
-       };
-    }
-}
-
-
-plan tests => scalar @$tests;
-
-for my $tt (@$tests) {
-
-    my $got = eval { PVE::Storage::archive_info($tt->{archive}) };
-    $got = $@ if $@;
-
-    is_deeply($got, $tt->{expected}, $tt->{description}) || diag(explain($got));
-}
-
-done_testing();
-
-1;
diff --git a/test/disk_tests/cciss/cciss!c0d0/device/model b/test/disk_tests/cciss/cciss!c0d0/device/model
deleted file mode 100644 (file)
index 676b97d..0000000
+++ /dev/null
@@ -1 +0,0 @@
-LOGICAL_VOLUME
diff --git a/test/disk_tests/cciss/cciss!c0d0/device/vendor b/test/disk_tests/cciss/cciss!c0d0/device/vendor
deleted file mode 100644 (file)
index e8b2ad6..0000000
+++ /dev/null
@@ -1 +0,0 @@
-HP
diff --git a/test/disk_tests/cciss/cciss!c0d0/queue/rotational b/test/disk_tests/cciss/cciss!c0d0/queue/rotational
deleted file mode 100644 (file)
index d00491f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-1
diff --git a/test/disk_tests/cciss/cciss!c0d0/size b/test/disk_tests/cciss/cciss!c0d0/size
deleted file mode 100644 (file)
index f599e28..0000000
+++ /dev/null
@@ -1 +0,0 @@
-10
diff --git a/test/disk_tests/cciss/cciss!c0d0_udevadm b/test/disk_tests/cciss/cciss!c0d0_udevadm
deleted file mode 100644 (file)
index 21e8899..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-P: /devices/pci0000:40/0000:40:13.0/0000:45:00.0/cciss0/c0d0/block/cciss!c0d0
-N: cciss/c0d0
-S: disk/by-id/cciss-SERIAL111
-S: disk/by-id/wwn-0x00000000000000000000000000000000
-S: disk/by-path/pci-0000:45:00.0-cciss-disk0
-E: DEVLINKS=/dev/disk/by-id/cciss-000000000000000000000000000000000 /dev/disk/by-id/wwn-0x000000000000000000000000000000000/dev/disk/by-path/pci-0000:45:00.0-cciss-disk0
-E: DEVNAME=/dev/cciss/c0d0
-E: DEVPATH=/devices/pci0000:40/0000:40:13.0/0000:45:00.0/cciss0/c0d0/block/cciss!c0d0
-E: DEVTYPE=disk
-E: ID_BUS=cciss
-E: ID_MODEL=LOGICAL_VOLUME
-E: ID_MODEL_ENC=LOGICAL\x20VOLUME\x20\x20
-E: ID_PART_TABLE_TYPE=gpt
-E: ID_PART_TABLE_UUID=cfe72deb-65d1-487c-bdfa-8af66dc1a969
-E: ID_PATH=pci-0000:45:00.0-cciss-disk0
-E: ID_PATH_TAG=pci-0000_45_00_0-cciss-disk0
-E: ID_REVISION=7.24
-E: ID_SCSI=1
-E: ID_SCSI_SERIAL=SERIAL1
-E: ID_SERIAL=SERIAL111
-E: ID_SERIAL_SHORT=SER111
-E: ID_TYPE=disk
-E: ID_VENDOR=HP
-E: ID_VENDOR_ENC=HP\x20\x20\x20\x20\x20\x20
-E: ID_WWN=0x0000000000000000
-E: ID_WWN_VENDOR_EXTENSION=0x0000000000000000
-E: ID_WWN_WITH_EXTENSION=0x00000000000000000000000000000000
-E: MAJOR=104
-E: MINOR=0
-E: SUBSYSTEM=block
-E: TAGS=:systemd:
-E: USEC_INITIALIZED=2247
diff --git a/test/disk_tests/cciss/disklist b/test/disk_tests/cciss/disklist
deleted file mode 100644 (file)
index aa174aa..0000000
+++ /dev/null
@@ -1 +0,0 @@
-cciss!c0d0
diff --git a/test/disk_tests/cciss/disklist_expected.json b/test/disk_tests/cciss/disklist_expected.json
deleted file mode 100644 (file)
index eff58db..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-{
-    "cciss/c0d0" : {
-       "wearout" : "N/A",
-       "vendor" : "HP",
-       "rpm" : -1,
-       "type" : "unknown",
-       "serial" : "SER111",
-       "osdid" : -1,
-       "health" : "UNKNOWN",
-       "model" : "LOGICAL_VOLUME",
-       "size" : 5120,
-       "wwn" : "0x0000000000000000",
-       "gpt" : 1,
-       "devpath" : "/dev/cciss/c0d0"
-    }
-}
diff --git a/test/disk_tests/hdd_smart/disklist b/test/disk_tests/hdd_smart/disklist
deleted file mode 100644 (file)
index 9f6776c..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-sda
-sdb
diff --git a/test/disk_tests/hdd_smart/disklist_expected.json b/test/disk_tests/hdd_smart/disklist_expected.json
deleted file mode 100644 (file)
index 02a341e..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-{
-    "sdb" : {
-       "devpath" : "/dev/sdb",
-       "size" : 1024000,
-       "gpt" : 1,
-       "osdid" : -1,
-       "rpm" : 7200,
-       "model" : "ST4000NM0033-9ZM170",
-       "vendor" : "ATA",
-       "health" : "PASSED",
-       "type" : "hdd",
-       "wwn" : "0x0000000000000000",
-       "wearout" : "N/A",
-       "serial" : "00000000"
-    },
-    "sda" : {
-       "osdid" : -1,
-       "size" : 1024000,
-       "gpt" : 1,
-       "devpath" : "/dev/sda",
-       "model" : "ST4000DM000-1F2168",
-       "rpm" : 5900,
-       "type" : "hdd",
-       "health" : "PASSED",
-       "vendor" : "ATA",
-       "serial" : "00000000",
-       "wearout" : "N/A",
-       "wwn" : "0x0000000000000000"
-    }
-}
diff --git a/test/disk_tests/hdd_smart/sda/device/vendor b/test/disk_tests/hdd_smart/sda/device/vendor
deleted file mode 100644 (file)
index 531030d..0000000
+++ /dev/null
@@ -1 +0,0 @@
-ATA
diff --git a/test/disk_tests/hdd_smart/sda/queue/rotational b/test/disk_tests/hdd_smart/sda/queue/rotational
deleted file mode 100644 (file)
index d00491f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-1
diff --git a/test/disk_tests/hdd_smart/sda/size b/test/disk_tests/hdd_smart/sda/size
deleted file mode 100644 (file)
index 8bd1af1..0000000
+++ /dev/null
@@ -1 +0,0 @@
-2000
diff --git a/test/disk_tests/hdd_smart/sda_health b/test/disk_tests/hdd_smart/sda_health
deleted file mode 100644 (file)
index faf4ce3..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-smartctl 6.6 2016-05-31 r4324 [x86_64-linux-4.4.21-1-pve] (local build)
-Copyright (C) 2002-16, Bruce Allen, Christian Franke, www.smartmontools.org
-
-=== START OF READ SMART DATA SECTION ===
-SMART overall-health self-assessment test result: PASSED
diff --git a/test/disk_tests/hdd_smart/sda_smart b/test/disk_tests/hdd_smart/sda_smart
deleted file mode 100644 (file)
index a3f8f0a..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-smartctl 6.6 2016-05-31 r4324 [x86_64-linux-4.4.21-1-pve] (local build)
-Copyright (C) 2002-16, Bruce Allen, Christian Franke, www.smartmontools.org
-
-=== START OF READ SMART DATA SECTION ===
-SMART overall-health self-assessment test result: PASSED
-
-SMART Attributes Data Structure revision number: 10
-Vendor Specific SMART Attributes with Thresholds:
-ID# ATTRIBUTE_NAME          FLAGS    VALUE WORST THRESH FAIL RAW_VALUE
-  1 Raw_Read_Error_Rate     POSR--   117   099   006    -    158983656
-  3 Spin_Up_Time            PO----   092   091   000    -    0
-  4 Start_Stop_Count        -O--CK   100   100   020    -    189
-  5 Reallocated_Sector_Ct   PO--CK   100   100   010    -    0
-  7 Seek_Error_Rate         POSR--   075   060   030    -    30779387
-  9 Power_On_Hours          -O--CK   099   099   000    -    1250
- 10 Spin_Retry_Count        PO--C-   100   100   097    -    0
- 12 Power_Cycle_Count       -O--CK   100   100   020    -    190
-183 Runtime_Bad_Block       -O--CK   100   100   000    -    0
-184 End-to-End_Error        -O--CK   100   100   099    -    0
-187 Reported_Uncorrect      -O--CK   100   100   000    -    0
-188 Command_Timeout         -O--CK   100   100   000    -    0 0 0
-189 High_Fly_Writes         -O-RCK   100   100   000    -    0
-190 Airflow_Temperature_Cel -O---K   069   061   045    -    31 (Min/Max 20/33)
-191 G-Sense_Error_Rate      -O--CK   100   100   000    -    0
-192 Power-Off_Retract_Count -O--CK   100   100   000    -    43
-193 Load_Cycle_Count        -O--CK   100   100   000    -    201
-194 Temperature_Celsius     -O---K   031   040   000    -    31 (0 17 0 0 0)
-197 Current_Pending_Sector  -O--C-   100   100   000    -    0
-198 Offline_Uncorrectable   ----C-   100   100   000    -    0
-199 UDMA_CRC_Error_Count    -OSRCK   200   200   000    -    0
-240 Head_Flying_Hours       ------   100   253   000    -    1259h+06m+33.546s
-241 Total_LBAs_Written      ------   100   253   000    -    24013587236
-242 Total_LBAs_Read         ------   100   253   000    -    66916845706732
-                            ||||||_ K auto-keep
-                            |||||__ C event count
-                            ||||___ R error rate
-                            |||____ S speed/performance
-                            ||_____ O updated online
-                            |______ P prefailure warning
-
diff --git a/test/disk_tests/hdd_smart/sda_smart_expected.json b/test/disk_tests/hdd_smart/sda_smart_expected.json
deleted file mode 100644 (file)
index 73cd01c..0000000
+++ /dev/null
@@ -1,270 +0,0 @@
-{
-    "attributes" : [
-       {
-           "threshold" : 6,
-           "fail" : "-",
-           "flags" : "POSR--",
-           "normalized" : 117,
-           "value" : 117,
-           "id" : "  1",
-           "raw" : "158983656",
-           "name" : "Raw_Read_Error_Rate",
-           "worst" : 99
-       },
-       {
-           "flags" : "PO----",
-           "normalized" : 92,
-           "value" : 92,
-           "raw" : "0",
-           "name" : "Spin_Up_Time",
-           "worst" : 91,
-           "id" : "  3",
-           "fail" : "-",
-           "threshold" : 0
-       },
-       {
-           "normalized" : 100,
-           "value" : 100,
-           "flags" : "-O--CK",
-           "id" : "  4",
-           "name" : "Start_Stop_Count",
-           "worst" : 100,
-           "raw" : "189",
-           "threshold" : 20,
-           "fail" : "-"
-       },
-       {
-           "normalized" : 100,
-           "value" : 100,
-           "flags" : "PO--CK",
-           "id" : "  5",
-           "name" : "Reallocated_Sector_Ct",
-           "worst" : 100,
-           "raw" : "0",
-           "threshold" : 10,
-           "fail" : "-"
-       },
-       {
-           "flags" : "POSR--",
-           "normalized" : 75,
-           "value" : 75,
-           "raw" : "30779387",
-           "worst" : 60,
-           "name" : "Seek_Error_Rate",
-           "id" : "  7",
-           "fail" : "-",
-           "threshold" : 30
-       },
-       {
-           "raw" : "1250",
-           "worst" : 99,
-           "name" : "Power_On_Hours",
-           "id" : "  9",
-           "flags" : "-O--CK",
-           "normalized" : 99,
-           "value" : 99,
-           "fail" : "-",
-           "threshold" : 0
-       },
-       {
-           "normalized" : 100,
-           "value" : 100,
-           "flags" : "PO--C-",
-           "id" : " 10",
-           "name" : "Spin_Retry_Count",
-           "worst" : 100,
-           "raw" : "0",
-           "threshold" : 97,
-           "fail" : "-"
-       },
-       {
-           "flags" : "-O--CK",
-           "normalized" : 100,
-           "value" : 100,
-           "id" : " 12",
-           "raw" : "190",
-           "worst" : 100,
-           "name" : "Power_Cycle_Count",
-           "threshold" : 20,
-           "fail" : "-"
-       },
-       {
-           "threshold" : 0,
-           "fail" : "-",
-           "flags" : "-O--CK",
-           "normalized" : 100,
-           "value" : 100,
-           "id" : "183",
-           "raw" : "0",
-           "worst" : 100,
-           "name" : "Runtime_Bad_Block"
-       },
-       {
-           "fail" : "-",
-           "threshold" : 99,
-           "flags" : "-O--CK",
-           "normalized" : 100,
-           "value" : 100,
-           "raw" : "0",
-           "worst" : 100,
-           "name" : "End-to-End_Error",
-           "id" : "184"
-       },
-       {
-           "worst" : 100,
-           "name" : "Reported_Uncorrect",
-           "raw" : "0",
-           "id" : "187",
-           "normalized" : 100,
-           "value" : 100,
-           "flags" : "-O--CK",
-           "fail" : "-",
-           "threshold" : 0
-       },
-       {
-           "flags" : "-O--CK",
-           "normalized" : 100,
-           "value" : 100,
-           "raw" : "0 0 0",
-           "worst" : 100,
-           "name" : "Command_Timeout",
-           "id" : "188",
-           "fail" : "-",
-           "threshold" : 0
-       },
-       {
-           "threshold" : 0,
-           "fail" : "-",
-           "flags" : "-O-RCK",
-           "normalized" : 100,
-           "value" : 100,
-           "id" : "189",
-           "raw" : "0",
-           "name" : "High_Fly_Writes",
-           "worst" : 100
-       },
-       {
-           "worst" : 61,
-           "name" : "Airflow_Temperature_Cel",
-           "raw" : "31 (Min/Max 20/33)",
-           "id" : "190",
-           "normalized" : 69,
-           "value" : 69,
-           "flags" : "-O---K",
-           "fail" : "-",
-           "threshold" : 45
-       },
-       {
-           "fail" : "-",
-           "threshold" : 0,
-           "worst" : 100,
-           "name" : "G-Sense_Error_Rate",
-           "raw" : "0",
-           "id" : "191",
-           "normalized" : 100,
-           "value" : 100,
-           "flags" : "-O--CK"
-       },
-       {
-           "id" : "192",
-           "raw" : "43",
-           "name" : "Power-Off_Retract_Count",
-           "worst" : 100,
-           "flags" : "-O--CK",
-           "normalized" : 100,
-           "value" : 100,
-           "threshold" : 0,
-           "fail" : "-"
-       },
-       {
-           "flags" : "-O--CK",
-           "normalized" : 100,
-           "value" : 100,
-           "raw" : "201",
-           "worst" : 100,
-           "name" : "Load_Cycle_Count",
-           "id" : "193",
-           "fail" : "-",
-           "threshold" : 0
-       },
-       {
-           "fail" : "-",
-           "threshold" : 0,
-           "normalized" : 31,
-           "value" : 31,
-           "flags" : "-O---K",
-           "name" : "Temperature_Celsius",
-           "worst" : 40,
-           "raw" : "31 (0 17 0 0 0)",
-           "id" : "194"
-       },
-       {
-           "normalized" : 100,
-           "value" : 100,
-           "flags" : "-O--C-",
-           "id" : "197",
-           "worst" : 100,
-           "name" : "Current_Pending_Sector",
-           "raw" : "0",
-           "threshold" : 0,
-           "fail" : "-"
-       },
-       {
-           "worst" : 100,
-           "name" : "Offline_Uncorrectable",
-           "raw" : "0",
-           "id" : "198",
-           "normalized" : 100,
-           "value" : 100,
-           "flags" : "----C-",
-           "fail" : "-",
-           "threshold" : 0
-       },
-       {
-           "threshold" : 0,
-           "fail" : "-",
-           "normalized" : 200,
-           "value" : 200,
-           "flags" : "-OSRCK",
-           "id" : "199",
-           "worst" : 200,
-           "name" : "UDMA_CRC_Error_Count",
-           "raw" : "0"
-       },
-       {
-           "raw" : "1259h+06m+33.546s",
-           "name" : "Head_Flying_Hours",
-           "worst" : 253,
-           "id" : "240",
-           "flags" : "------",
-           "normalized" : 100,
-           "value" : 100,
-           "fail" : "-",
-           "threshold" : 0
-       },
-       {
-           "fail" : "-",
-           "threshold" : 0,
-           "name" : "Total_LBAs_Written",
-           "worst" : 253,
-           "raw" : "24013587236",
-           "id" : "241",
-           "normalized" : 100,
-           "value" : 100,
-           "flags" : "------"
-       },
-       {
-           "flags" : "------",
-           "normalized" : 100,
-           "value" : 100,
-           "id" : "242",
-           "raw" : "66916845706732",
-           "worst" : 253,
-           "name" : "Total_LBAs_Read",
-           "threshold" : 0,
-           "fail" : "-"
-       }
-    ],
-    "health" : "PASSED",
-    "type" : "ata"
-}
diff --git a/test/disk_tests/hdd_smart/sda_udevadm b/test/disk_tests/hdd_smart/sda_udevadm
deleted file mode 100644 (file)
index d9b5497..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-E: DEVNAME=/dev/sda
-E: DEVTYPE=disk
-E: ID_ATA_ROTATION_RATE_RPM=5900
-E: ID_BUS=ata
-E: ID_MODEL=ST4000DM000-1F2168
-E: ID_PART_TABLE_TYPE=gpt
-E: ID_PART_TABLE_UUID=4f2e07a6-5437-2b4e-b6e8-9cba98639324
-E: ID_SERIAL_SHORT=00000000
-E: ID_TYPE=disk
-E: ID_WWN=0x0000000000000000
-E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/test/disk_tests/hdd_smart/sdb/device/vendor b/test/disk_tests/hdd_smart/sdb/device/vendor
deleted file mode 100644 (file)
index 531030d..0000000
+++ /dev/null
@@ -1 +0,0 @@
-ATA
diff --git a/test/disk_tests/hdd_smart/sdb/queue/rotational b/test/disk_tests/hdd_smart/sdb/queue/rotational
deleted file mode 100644 (file)
index d00491f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-1
diff --git a/test/disk_tests/hdd_smart/sdb/size b/test/disk_tests/hdd_smart/sdb/size
deleted file mode 100644 (file)
index 8bd1af1..0000000
+++ /dev/null
@@ -1 +0,0 @@
-2000
diff --git a/test/disk_tests/hdd_smart/sdb_health b/test/disk_tests/hdd_smart/sdb_health
deleted file mode 100644 (file)
index faf4ce3..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-smartctl 6.6 2016-05-31 r4324 [x86_64-linux-4.4.21-1-pve] (local build)
-Copyright (C) 2002-16, Bruce Allen, Christian Franke, www.smartmontools.org
-
-=== START OF READ SMART DATA SECTION ===
-SMART overall-health self-assessment test result: PASSED
diff --git a/test/disk_tests/hdd_smart/sdb_smart b/test/disk_tests/hdd_smart/sdb_smart
deleted file mode 100644 (file)
index ce52bea..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-smartctl 6.4 2014-10-07 r4002 [x86_64-linux-4.4.10-1-pve] (local build)
-Copyright (C) 2002-14, Bruce Allen, Christian Franke, www.smartmontools.org
-
-=== START OF READ SMART DATA SECTION ===
-SMART overall-health self-assessment test result: PASSED
-
-SMART Attributes Data Structure revision number: 10
-Vendor Specific SMART Attributes with Thresholds:
-ID# ATTRIBUTE_NAME          FLAG     VALUE WORST THRESH FAIL RAW_VALUE
-  1 Raw_Read_Error_Rate     POSR--   083   063   044    -    215697022
-  3 Spin_Up_Time            PO----   092   091   000    -    0
-  4 Start_Stop_Count        -O--CK   100   100   020    -    265
-  5 Reallocated_Sector_Ct   PO--CK   100   100   010    -    0
-  7 Seek_Error_Rate         POSR--   091   060   030    -    1572375006
-  9 Power_On_Hours          -O--CK   089   089   000    -    9885
- 10 Spin_Retry_Count        PO--C-   100   100   097    -    0
- 12 Power_Cycle_Count       -O--CK   100   100   020    -    265
-184 End-to-End_Error        -O--CK   100   100   099    -    0
-187 Reported_Uncorrect      -O--CK   100   100   000    -    0
-188 Command_Timeout         -O--CK   100   100   000    -    0
-189 High_Fly_Writes         -O--CK   100   100   000    -    0
-190 Airflow_Temperature_Cel -O-RCK   045   036   045    NOW  55 (147 229 55 24 0)
-191 G-Sense_Error_Rate      -O---K   100   100   000    -    0
-192 Power-Off_Retract_Count -O--CK   100   100   000    -    57
-193 Load_Cycle_Count        -O--CK   100   100   000    -    265
-194 Temperature_Celsius     -O--CK   055   064   000    -    55 (0 16 0 0 0)
-195 Hardware_ECC_Recovered  -O---K   023   013   000    -    215697022
-197 Current_Pending_Sector  -O--C-   100   100   000    -    0
-198 Offline_Uncorrectable   ----C-   100   100   000    -    0
-199 UDMA_CRC_Error_Count    -OSRCK   200   200   000    -    0
-                            ||||||_ K auto-keep
-                            |||||__ C event count
-                            ||||___ R error rate
-                            |||____ S speed/performance
-                            ||_____ O updated online
-                            |______ P prefailure warning
diff --git a/test/disk_tests/hdd_smart/sdb_smart_expected.json b/test/disk_tests/hdd_smart/sdb_smart_expected.json
deleted file mode 100644 (file)
index ae0f014..0000000
+++ /dev/null
@@ -1,237 +0,0 @@
-{
-    "attributes" : [
-       {
-           "threshold" : 44,
-           "fail" : "-",
-           "id" : "  1",
-           "name" : "Raw_Read_Error_Rate",
-           "worst" : 63,
-           "raw" : "215697022",
-           "normalized" : 83,
-           "value" : 83,
-           "flags" : "POSR--"
-       },
-       {
-           "flags" : "PO----",
-           "normalized" : 92,
-           "value" : 92,
-           "id" : "  3",
-           "raw" : "0",
-           "worst" : 91,
-           "name" : "Spin_Up_Time",
-           "threshold" : 0,
-           "fail" : "-"
-       },
-       {
-           "fail" : "-",
-           "threshold" : 20,
-           "normalized" : 100,
-           "value" : 100,
-           "flags" : "-O--CK",
-           "worst" : 100,
-           "name" : "Start_Stop_Count",
-           "raw" : "265",
-           "id" : "  4"
-       },
-       {
-           "flags" : "PO--CK",
-           "normalized" : 100,
-           "value" : 100,
-           "id" : "  5",
-           "raw" : "0",
-           "name" : "Reallocated_Sector_Ct",
-           "worst" : 100,
-           "threshold" : 10,
-           "fail" : "-"
-       },
-       {
-           "id" : "  7",
-           "raw" : "1572375006",
-           "name" : "Seek_Error_Rate",
-           "worst" : 60,
-           "flags" : "POSR--",
-           "normalized" : 91,
-           "value" : 91,
-           "threshold" : 30,
-           "fail" : "-"
-       },
-       {
-           "raw" : "9885",
-           "name" : "Power_On_Hours",
-           "worst" : 89,
-           "id" : "  9",
-           "flags" : "-O--CK",
-           "normalized" : 89,
-           "value" : 89,
-           "fail" : "-",
-           "threshold" : 0
-       },
-       {
-           "fail" : "-",
-           "threshold" : 97,
-           "normalized" : 100,
-           "value" : 100,
-           "flags" : "PO--C-",
-           "worst" : 100,
-           "name" : "Spin_Retry_Count",
-           "raw" : "0",
-           "id" : " 10"
-       },
-       {
-           "threshold" : 20,
-           "fail" : "-",
-           "id" : " 12",
-           "raw" : "265",
-           "name" : "Power_Cycle_Count",
-           "worst" : 100,
-           "flags" : "-O--CK",
-           "normalized" : 100,
-           "value" : 100
-       },
-       {
-           "name" : "End-to-End_Error",
-           "worst" : 100,
-           "raw" : "0",
-           "id" : "184",
-           "normalized" : 100,
-           "value" : 100,
-           "flags" : "-O--CK",
-           "fail" : "-",
-           "threshold" : 99
-       },
-       {
-           "threshold" : 0,
-           "fail" : "-",
-           "id" : "187",
-           "raw" : "0",
-           "name" : "Reported_Uncorrect",
-           "worst" : 100,
-           "flags" : "-O--CK",
-           "normalized" : 100,
-           "value" : 100
-       },
-       {
-           "threshold" : 0,
-           "fail" : "-",
-           "id" : "188",
-           "raw" : "0",
-           "name" : "Command_Timeout",
-           "worst" : 100,
-           "flags" : "-O--CK",
-           "normalized" : 100,
-           "value" : 100
-       },
-       {
-           "fail" : "-",
-           "threshold" : 0,
-           "raw" : "0",
-           "worst" : 100,
-           "name" : "High_Fly_Writes",
-           "id" : "189",
-           "flags" : "-O--CK",
-           "normalized" : 100,
-           "value" : 100
-       },
-       {
-           "flags" : "-O-RCK",
-           "normalized" : 45,
-           "value" : 45,
-           "raw" : "55 (147 229 55 24 0)",
-           "worst" : 36,
-           "name" : "Airflow_Temperature_Cel",
-           "id" : "190",
-           "fail" : "NOW",
-           "threshold" : 45
-       },
-       {
-           "fail" : "-",
-           "threshold" : 0,
-           "raw" : "0",
-           "worst" : 100,
-           "name" : "G-Sense_Error_Rate",
-           "id" : "191",
-           "flags" : "-O---K",
-           "normalized" : 100,
-           "value" : 100
-       },
-       {
-           "threshold" : 0,
-           "fail" : "-",
-           "id" : "192",
-           "raw" : "57",
-           "worst" : 100,
-           "name" : "Power-Off_Retract_Count",
-           "flags" : "-O--CK",
-           "normalized" : 100,
-           "value" : 100
-       },
-       {
-           "fail" : "-",
-           "threshold" : 0,
-           "normalized" : 100,
-           "value" : 100,
-           "flags" : "-O--CK",
-           "name" : "Load_Cycle_Count",
-           "worst" : 100,
-           "raw" : "265",
-           "id" : "193"
-       },
-       {
-           "threshold" : 0,
-           "fail" : "-",
-           "flags" : "-O--CK",
-           "normalized" : 55,
-           "value" : 55,
-           "id" : "194",
-           "raw" : "55 (0 16 0 0 0)",
-           "name" : "Temperature_Celsius",
-           "worst" : 64
-       },
-       {
-           "threshold" : 0,
-           "fail" : "-",
-           "id" : "195",
-           "name" : "Hardware_ECC_Recovered",
-           "worst" : 13,
-           "raw" : "215697022",
-           "normalized" : 23,
-           "value" : 23,
-           "flags" : "-O---K"
-       },
-       {
-           "worst" : 100,
-           "name" : "Current_Pending_Sector",
-           "raw" : "0",
-           "id" : "197",
-           "normalized" : 100,
-           "value" : 100,
-           "flags" : "-O--C-",
-           "fail" : "-",
-           "threshold" : 0
-       },
-       {
-           "worst" : 100,
-           "name" : "Offline_Uncorrectable",
-           "raw" : "0",
-           "id" : "198",
-           "normalized" : 100,
-           "value" : 100,
-           "flags" : "----C-",
-           "fail" : "-",
-           "threshold" : 0
-       },
-       {
-           "fail" : "-",
-           "threshold" : 0,
-           "normalized" : 200,
-           "value" : 200,
-           "flags" : "-OSRCK",
-           "worst" : 200,
-           "name" : "UDMA_CRC_Error_Count",
-           "raw" : "0",
-           "id" : "199"
-       }
-    ],
-    "type" : "ata",
-    "health" : "PASSED"
-}
diff --git a/test/disk_tests/hdd_smart/sdb_udevadm b/test/disk_tests/hdd_smart/sdb_udevadm
deleted file mode 100644 (file)
index 3bc3a57..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-E: DEVNAME=/dev/sdb
-E: DEVTYPE=disk
-E: ID_ATA_ROTATION_RATE_RPM=7200
-E: ID_BUS=ata
-E: ID_MODEL=ST4000NM0033-9ZM170
-E: ID_PART_TABLE_TYPE=gpt
-E: ID_PART_TABLE_UUID=4f2e07a6-5437-2b4e-b6e8-9cba98639324
-E: ID_SERIAL_SHORT=00000000
-E: ID_TYPE=disk
-E: ID_WWN=0x0000000000000000
-E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/test/disk_tests/nvme_smart/disklist b/test/disk_tests/nvme_smart/disklist
deleted file mode 100644 (file)
index d00b90e..0000000
+++ /dev/null
@@ -1 +0,0 @@
-nvme0n1
diff --git a/test/disk_tests/nvme_smart/disklist_expected.json b/test/disk_tests/nvme_smart/disklist_expected.json
deleted file mode 100644 (file)
index 4d1c92f..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-{
-    "nvme0n1" : {
-       "wearout" : 69,
-       "vendor" : "unknown",
-       "size" : 512000,
-       "health" : "PASSED",
-       "serial" : "unknown",
-       "model" : "NVME MODEL 1",
-       "rpm" : 0,
-       "osdid" : -1,
-       "devpath" : "/dev/nvme0n1",
-       "gpt" : 0,
-       "wwn" : "unknown",
-       "type" : "nvme"
-    }
-}
diff --git a/test/disk_tests/nvme_smart/nvme0n1/device/model b/test/disk_tests/nvme_smart/nvme0n1/device/model
deleted file mode 100644 (file)
index 9bd6eba..0000000
+++ /dev/null
@@ -1 +0,0 @@
-NVME MODEL 1
diff --git a/test/disk_tests/nvme_smart/nvme0n1/queue/rotational b/test/disk_tests/nvme_smart/nvme0n1/queue/rotational
deleted file mode 100644 (file)
index 573541a..0000000
+++ /dev/null
@@ -1 +0,0 @@
-0
diff --git a/test/disk_tests/nvme_smart/nvme0n1/size b/test/disk_tests/nvme_smart/nvme0n1/size
deleted file mode 100644 (file)
index 83b33d2..0000000
+++ /dev/null
@@ -1 +0,0 @@
-1000
diff --git a/test/disk_tests/nvme_smart/nvme0n1_smart b/test/disk_tests/nvme_smart/nvme0n1_smart
deleted file mode 100644 (file)
index f371d5b..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-smartctl 6.6 2016-05-31 r4324 [x86_64-linux-4.4.19-1-pve] (local build)
-Copyright (C) 2002-16, Bruce Allen, Christian Franke, www.smartmontools.org
-
-=== START OF SMART DATA SECTION ===
-SMART overall-health self-assessment test result: PASSED
-
-SMART/Health Information (NVMe Log 0x02, NSID 0xffffffff)
-Critical Warning:                   0x00
-Temperature:                        32 Celsius
-Available Spare:                    100%
-Available Spare Threshold:          10%
-Percentage Used:                    31%
-Data Units Read:                    1,299,288 [665 GB]
-Data Units Written:                 5,592,478 [2.86 TB]
-Host Read Commands:                 30,360,807
-Host Write Commands:                470,356,196
-Controller Busy Time:               12
-Power Cycles:                       98
-Power On Hours:                     687
-Unsafe Shutdowns:                   21
-Media and Data Integrity Errors:    0
-Error Information Log Entries:      0
diff --git a/test/disk_tests/nvme_smart/nvme0n1_smart_expected.json b/test/disk_tests/nvme_smart/nvme0n1_smart_expected.json
deleted file mode 100644 (file)
index af8eade..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-{
-    "text" : "\nSMART/Health Information (NVMe Log 0x02, NSID 0xffffffff)\nCritical Warning:                   0x00\nTemperature:                        32 Celsius\nAvailable Spare:                    100%\nAvailable Spare Threshold:          10%\nPercentage Used:                    31%\nData Units Read:                    1,299,288 [665 GB]\nData Units Written:                 5,592,478 [2.86 TB]\nHost Read Commands:                 30,360,807\nHost Write Commands:                470,356,196\nController Busy Time:               12\nPower Cycles:                       98\nPower On Hours:                     687\nUnsafe Shutdowns:                   21\nMedia and Data Integrity Errors:    0\nError Information Log Entries:      0\n",
-    "health" : "PASSED",
-    "type" : "text",
-    "wearout": 69
-}
diff --git a/test/disk_tests/nvme_smart/nvme0n1_udevadm b/test/disk_tests/nvme_smart/nvme0n1_udevadm
deleted file mode 100644 (file)
index 36c78ce..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-
-P: /devices/pci0000:00/0000:00:01.1/0000:02:00.0/nvme/nvme0/nvme0n1
-N: nvme0n1
-S: disk/by-id/lvm-pv-uuid-Py4eod-qfzj-i8Q3-Dxu6-xf0Q-H3Wr-w5Fo8V
-E: DEVLINKS=/dev/disk/by-id/lvm-pv-uuid-Py4eod-qfzj-i8Q3-Dxu6-xf0Q-H3Wr-w5Fo8V
-E: DEVNAME=/dev/nvme0n1
-E: DEVPATH=/devices/pci0000:00/0000:00:01.1/0000:02:00.0/nvme/nvme0/nvme0n1
-E: DEVTYPE=disk
-E: ID_FS_TYPE=LVM2_member
-E: ID_FS_USAGE=raid
-E: ID_FS_UUID=Py4eod-qfzj-i8Q3-Dxu6-xf0Q-H3Wr-w5Fo8V
-E: ID_FS_UUID_ENC=Py4eod-qfzj-i8Q3-Dxu6-xf0Q-H3Wr-w5Fo8V
-E: ID_FS_VERSION=LVM2 001
-E: MAJOR=259
-E: MINOR=0
-E: SUBSYSTEM=block
-E: TAGS=:systemd:
-E: USEC_INITIALIZED=3842
diff --git a/test/disk_tests/sas/disklist b/test/disk_tests/sas/disklist
deleted file mode 100644 (file)
index 9191c61..0000000
+++ /dev/null
@@ -1 +0,0 @@
-sda
diff --git a/test/disk_tests/sas/disklist_expected.json b/test/disk_tests/sas/disklist_expected.json
deleted file mode 100644 (file)
index 39b14a5..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-{
-    "sda" : {
-       "gpt" : 1,
-       "devpath" : "/dev/sda",
-       "type" : "unknown",
-       "model" : "MODEL1",
-       "health" : "UNKNOWN",
-       "osdid" : -1,
-       "wwn" : "0x0000000000000000",
-       "vendor" : "VENDOR1",
-       "rpm" : -1,
-       "size" : 5120000,
-       "serial" : "SER2",
-       "wearout" : "N/A",
-       "by_id_link" : "/dev/disk/by-id/scsi-00000000000000000"
-    }
-}
diff --git a/test/disk_tests/sas/sda/device/model b/test/disk_tests/sas/sda/device/model
deleted file mode 100644 (file)
index 6b69674..0000000
+++ /dev/null
@@ -1 +0,0 @@
-MODEL1
diff --git a/test/disk_tests/sas/sda/device/vendor b/test/disk_tests/sas/sda/device/vendor
deleted file mode 100644 (file)
index a7894eb..0000000
+++ /dev/null
@@ -1 +0,0 @@
-VENDOR1
diff --git a/test/disk_tests/sas/sda/queue/rotational b/test/disk_tests/sas/sda/queue/rotational
deleted file mode 100644 (file)
index d00491f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-1
diff --git a/test/disk_tests/sas/sda/size b/test/disk_tests/sas/sda/size
deleted file mode 100644 (file)
index 5caff40..0000000
+++ /dev/null
@@ -1 +0,0 @@
-10000
diff --git a/test/disk_tests/sas/sda_smart b/test/disk_tests/sas/sda_smart
deleted file mode 100644 (file)
index 856af39..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-=== START OF READ SMART DATA SECTION ===
-SMART Health Status: OK
-
-Percentage used endurance indicator: 0%
-Current Drive Temperature:     20 C
-Drive Trip Temperature:        70 C
-
-Manufactured in week 47 of year 2012
-Specified cycle count over device lifetime:  0
-Accumulated start-stop cycles:  0
-Specified load-unload count over device lifetime:  0
-Accumulated load-unload cycles:  0
-Elements in grown defect list: 0
-
-Vendor (Seagate) cache information
-  Blocks sent to initiator = 1286675833552896
-
-Vendor (Seagate/Hitachi) factory information
-  number of hours powered up = 7127.12
-  number of minutes until next internal SMART test = 0
diff --git a/test/disk_tests/sas/sda_smart_expected.json b/test/disk_tests/sas/sda_smart_expected.json
deleted file mode 100644 (file)
index 2964372..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-{
-    "health" : "OK",
-    "text" : "\nPercentage used endurance indicator: 0%\nCurrent Drive Temperature:     20 C\nDrive Trip Temperature:        70 C\n\nManufactured in week 47 of year 2012\nSpecified cycle count over device lifetime:  0\nAccumulated start-stop cycles:  0\nSpecified load-unload count over device lifetime:  0\nAccumulated load-unload cycles:  0\nElements in grown defect list: 0\n\nVendor (Seagate) cache information\n  Blocks sent to initiator = 1286675833552896\n\nVendor (Seagate/Hitachi) factory information\n  number of hours powered up = 7127.12\n  number of minutes until next internal SMART test = 0\n",
-    "type" : "text",
-    "wearout": 100
-}
diff --git a/test/disk_tests/sas/sda_udevadm b/test/disk_tests/sas/sda_udevadm
deleted file mode 100644 (file)
index ac0744d..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-P: /devices/pci0000:00/0000:00:03.0/0000:02:00.0/host4/port-4:0/end_device-4:0/target4:0:0/4:0:0:0/block/sda
-N: sda
-S: disk/by-id/scsi-00000000000000000
-S: disk/by-id/wwn-0x0000000000000000
-S: disk/by-path/pci-0000:02:00.0-sas-0x0000000000000000-lun-0
-E: DEVLINKS=/dev/disk/by-id/scsi-00000000000000000 /dev/disk/by-id/wwn-0x0000000000000000 /dev/disk/by-path/pci-0000:02:00.0-sas-0x0000000000000000-lun-0
-E: DEVNAME=/dev/sda
-E: DEVPATH=/devices/pci0000:00/0000:00:03.0/0000:02:00.0/host4/port-4:0/end_device-4:0/target4:0:0/4:0:0:0/block/sda
-E: DEVTYPE=disk
-E: ID_BUS=scsi
-E: ID_MODEL=MODEL1
-E: ID_MODEL_ENC=MODEL1\x20\x20\x20\x20\x20\x20
-E: ID_PART_TABLE_TYPE=gpt
-E: ID_PART_TABLE_UUID=605740f0-44a1-4dc5-9fea-bde166df963e
-E: ID_PATH=pci-0000:02:00.0-sas-0x0000000000000000-lun-0
-E: ID_PATH_TAG=pci-0000_02_00_0-sas-0x0000000000000000-lun-0
-E: ID_REVISION=ES64
-E: ID_SCSI=1
-E: ID_SCSI_SERIAL=SERIAL
-E: ID_SERIAL=SERIAL2
-E: ID_SERIAL_SHORT=SER2
-E: ID_TYPE=disk
-E: ID_VENDOR=VENDOR1
-E: ID_VENDOR_ENC=VENDOR1\x20
-E: ID_WWN=0x0000000000000000
-E: ID_WWN_WITH_EXTENSION=0x0000000000000000
-E: MAJOR=8
-E: MINOR=0
-E: SUBSYSTEM=block
-E: TAGS=:systemd:
-E: USEC_INITIALIZED=667541
diff --git a/test/disk_tests/sas_ssd/disklist b/test/disk_tests/sas_ssd/disklist
deleted file mode 100644 (file)
index 9191c61..0000000
+++ /dev/null
@@ -1 +0,0 @@
-sda
diff --git a/test/disk_tests/sas_ssd/disklist_expected.json b/test/disk_tests/sas_ssd/disklist_expected.json
deleted file mode 100644 (file)
index dd9b748..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-{
-    "sda" : {
-       "gpt" : 1,
-       "devpath" : "/dev/sda",
-       "type" : "ssd",
-       "model" : "MODEL1",
-       "health" : "OK",
-       "osdid" : -1,
-       "wwn" : "0x0000000000000000",
-       "vendor" : "VENDOR1",
-       "rpm" : 0,
-       "size" : 5120000,
-       "serial" : "SER2",
-       "wearout" : 100,
-       "by_id_link" : "/dev/disk/by-id/scsi-00000000000000000"
-    }
-}
diff --git a/test/disk_tests/sas_ssd/sda/device/model b/test/disk_tests/sas_ssd/sda/device/model
deleted file mode 100644 (file)
index 6b69674..0000000
+++ /dev/null
@@ -1 +0,0 @@
-MODEL1
diff --git a/test/disk_tests/sas_ssd/sda/device/vendor b/test/disk_tests/sas_ssd/sda/device/vendor
deleted file mode 100644 (file)
index a7894eb..0000000
+++ /dev/null
@@ -1 +0,0 @@
-VENDOR1
diff --git a/test/disk_tests/sas_ssd/sda/queue/rotational b/test/disk_tests/sas_ssd/sda/queue/rotational
deleted file mode 100644 (file)
index 573541a..0000000
+++ /dev/null
@@ -1 +0,0 @@
-0
diff --git a/test/disk_tests/sas_ssd/sda/size b/test/disk_tests/sas_ssd/sda/size
deleted file mode 100644 (file)
index 5caff40..0000000
+++ /dev/null
@@ -1 +0,0 @@
-10000
diff --git a/test/disk_tests/sas_ssd/sda_smart b/test/disk_tests/sas_ssd/sda_smart
deleted file mode 100644 (file)
index 856af39..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-=== START OF READ SMART DATA SECTION ===
-SMART Health Status: OK
-
-Percentage used endurance indicator: 0%
-Current Drive Temperature:     20 C
-Drive Trip Temperature:        70 C
-
-Manufactured in week 47 of year 2012
-Specified cycle count over device lifetime:  0
-Accumulated start-stop cycles:  0
-Specified load-unload count over device lifetime:  0
-Accumulated load-unload cycles:  0
-Elements in grown defect list: 0
-
-Vendor (Seagate) cache information
-  Blocks sent to initiator = 1286675833552896
-
-Vendor (Seagate/Hitachi) factory information
-  number of hours powered up = 7127.12
-  number of minutes until next internal SMART test = 0
diff --git a/test/disk_tests/sas_ssd/sda_smart_expected.json b/test/disk_tests/sas_ssd/sda_smart_expected.json
deleted file mode 100644 (file)
index 2964372..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-{
-    "health" : "OK",
-    "text" : "\nPercentage used endurance indicator: 0%\nCurrent Drive Temperature:     20 C\nDrive Trip Temperature:        70 C\n\nManufactured in week 47 of year 2012\nSpecified cycle count over device lifetime:  0\nAccumulated start-stop cycles:  0\nSpecified load-unload count over device lifetime:  0\nAccumulated load-unload cycles:  0\nElements in grown defect list: 0\n\nVendor (Seagate) cache information\n  Blocks sent to initiator = 1286675833552896\n\nVendor (Seagate/Hitachi) factory information\n  number of hours powered up = 7127.12\n  number of minutes until next internal SMART test = 0\n",
-    "type" : "text",
-    "wearout": 100
-}
diff --git a/test/disk_tests/sas_ssd/sda_udevadm b/test/disk_tests/sas_ssd/sda_udevadm
deleted file mode 100644 (file)
index ac0744d..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-P: /devices/pci0000:00/0000:00:03.0/0000:02:00.0/host4/port-4:0/end_device-4:0/target4:0:0/4:0:0:0/block/sda
-N: sda
-S: disk/by-id/scsi-00000000000000000
-S: disk/by-id/wwn-0x0000000000000000
-S: disk/by-path/pci-0000:02:00.0-sas-0x0000000000000000-lun-0
-E: DEVLINKS=/dev/disk/by-id/scsi-00000000000000000 /dev/disk/by-id/wwn-0x0000000000000000 /dev/disk/by-path/pci-0000:02:00.0-sas-0x0000000000000000-lun-0
-E: DEVNAME=/dev/sda
-E: DEVPATH=/devices/pci0000:00/0000:00:03.0/0000:02:00.0/host4/port-4:0/end_device-4:0/target4:0:0/4:0:0:0/block/sda
-E: DEVTYPE=disk
-E: ID_BUS=scsi
-E: ID_MODEL=MODEL1
-E: ID_MODEL_ENC=MODEL1\x20\x20\x20\x20\x20\x20
-E: ID_PART_TABLE_TYPE=gpt
-E: ID_PART_TABLE_UUID=605740f0-44a1-4dc5-9fea-bde166df963e
-E: ID_PATH=pci-0000:02:00.0-sas-0x0000000000000000-lun-0
-E: ID_PATH_TAG=pci-0000_02_00_0-sas-0x0000000000000000-lun-0
-E: ID_REVISION=ES64
-E: ID_SCSI=1
-E: ID_SCSI_SERIAL=SERIAL
-E: ID_SERIAL=SERIAL2
-E: ID_SERIAL_SHORT=SER2
-E: ID_TYPE=disk
-E: ID_VENDOR=VENDOR1
-E: ID_VENDOR_ENC=VENDOR1\x20
-E: ID_WWN=0x0000000000000000
-E: ID_WWN_WITH_EXTENSION=0x0000000000000000
-E: MAJOR=8
-E: MINOR=0
-E: SUBSYSTEM=block
-E: TAGS=:systemd:
-E: USEC_INITIALIZED=667541
diff --git a/test/disk_tests/ssd_smart/disklist b/test/disk_tests/ssd_smart/disklist
deleted file mode 100644 (file)
index 18048d5..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-sda
-sdb
-sdc
-sdd
-sde
diff --git a/test/disk_tests/ssd_smart/disklist_expected.json b/test/disk_tests/ssd_smart/disklist_expected.json
deleted file mode 100644 (file)
index d84b9dc..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-{
-    "sda" : {
-       "serial" : "000000000000",
-       "vendor" : "ATA",
-       "rpm" : 0,
-       "gpt" : 1,
-       "health" : "PASSED",
-       "wearout" : "100",
-       "osdid" : -1,
-       "size" : 512000,
-       "type" : "ssd",
-       "devpath" : "/dev/sda",
-       "model" : "Crucial_CT500MX200SSD1",
-       "wwn" : "0x0000000000000000"
-    },
-    "sdb" : {
-       "model" : "INTEL_SSDSC2BB080G6",
-       "devpath" : "/dev/sdb",
-       "osdid" : -1,
-       "type" : "ssd",
-       "size" : 512000,
-       "wwn" : "0x0000000000000000",
-       "gpt" : 1,
-       "rpm" : 0,
-       "vendor" : "ATA",
-       "serial" : "000000000000000000",
-       "wearout" : "97",
-       "health" : "PASSED"
-    },
-    "sdc" : {
-       "wwn" : "0x0000000000000000",
-       "devpath" : "/dev/sdc",
-       "model" : "Samsung SSD 850 PRO 512GB",
-       "osdid" : -1,
-       "type" : "ssd",
-       "size" : 512000,
-       "wearout" : "99",
-       "health" : "PASSED",
-       "gpt" : 1,
-       "rpm" : 0,
-       "vendor" : "ATA",
-       "serial" : "000000000000"
-    },
-    "sdd" : {
-       "rpm" : 0,
-       "gpt" : 1,
-       "serial" : "000000000000",
-       "vendor" : "ATA",
-       "wearout" : "100",
-       "health" : "PASSED",
-       "devpath" : "/dev/sdd",
-       "model" : "SanDisk SD8SB8U1T001122",
-       "size" : 512000,
-       "osdid" : -1,
-       "type" : "ssd",
-       "wwn" : "0x0000000000000000"
-    },
-    "sde" : {
-       "type" : "ssd",
-       "osdid" : -1,
-       "size" : 512000,
-       "model" : "KINGSTON SHFS37A120G",
-       "devpath" : "/dev/sde",
-       "wwn" : "0x0000000000000000",
-       "vendor" : "ATA",
-       "serial" : "000000000000",
-       "gpt" : 1,
-       "rpm" : 0,
-       "health" : "PASSED",
-       "wearout" : "91"
-    }
-}
diff --git a/test/disk_tests/ssd_smart/sda/device/vendor b/test/disk_tests/ssd_smart/sda/device/vendor
deleted file mode 100644 (file)
index 531030d..0000000
+++ /dev/null
@@ -1 +0,0 @@
-ATA
diff --git a/test/disk_tests/ssd_smart/sda/queue/rotational b/test/disk_tests/ssd_smart/sda/queue/rotational
deleted file mode 100644 (file)
index 573541a..0000000
+++ /dev/null
@@ -1 +0,0 @@
-0
diff --git a/test/disk_tests/ssd_smart/sda/size b/test/disk_tests/ssd_smart/sda/size
deleted file mode 100644 (file)
index 83b33d2..0000000
+++ /dev/null
@@ -1 +0,0 @@
-1000
diff --git a/test/disk_tests/ssd_smart/sda_smart b/test/disk_tests/ssd_smart/sda_smart
deleted file mode 100644 (file)
index 2857588..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-smartctl 6.6 2016-05-31 r4324 [x86_64-linux-4.4.21-1-pve] (local build)
-Copyright (C) 2002-16, Bruce Allen, Christian Franke, www.smartmontools.org
-
-=== START OF READ SMART DATA SECTION ===
-SMART overall-health self-assessment test result: PASSED
-
-SMART Attributes Data Structure revision number: 16
-Vendor Specific SMART Attributes with Thresholds:
-ID# ATTRIBUTE_NAME          FLAGS    VALUE WORST THRESH FAIL RAW_VALUE
-  1 Raw_Read_Error_Rate     POSR-K   100   100   000    -    0
-  5 Reallocate_NAND_Blk_Cnt -O--CK   100   100   010    -    0
-  9 Power_On_Hours          -O--CK   100   100   000    -    1309
- 12 Power_Cycle_Count       -O--CK   100   100   000    -    200
-171 Program_Fail_Count      -O--CK   100   100   000    -    0
-172 Erase_Fail_Count        -O--CK   100   100   000    -    0
-173 Ave_Block-Erase_Count   -O--CK   100   100   000    -    12
-174 Unexpect_Power_Loss_Ct  -O--CK   100   100   000    -    53
-180 Unused_Reserve_NAND_Blk PO--CK   000   000   000    -    5565
-183 SATA_Interfac_Downshift -O--CK   100   100   000    -    0
-184 Error_Correction_Count  -O--CK   100   100   000    -    0
-187 Reported_Uncorrect      -O--CK   100   100   000    -    0
-194 Temperature_Celsius     -O---K   068   054   000    -    32 (Min/Max 22/46)
-196 Reallocated_Event_Count -O--CK   100   100   000    -    0
-197 Current_Pending_Sector  -O--CK   100   100   000    -    0
-198 Offline_Uncorrectable   ----CK   100   100   000    -    0
-199 UDMA_CRC_Error_Count    -O--CK   100   100   000    -    0
-202 Percent_Lifetime_Used   ----CK   100   100   001    -    0
-206 Write_Error_Rate        -OSR--   100   100   000    -    0
-210 Success_RAIN_Recov_Cnt  -O--CK   100   100   000    -    0
-246 Total_Host_Sector_Write -O--CK   100   100   000    -    6751830403
-247 Host_Program_Page_Count -O--CK   100   100   000    -    211228065
-248 Bckgnd_Program_Page_Cnt -O--CK   100   100   000    -    253276904
-                            ||||||_ K auto-keep
-                            |||||__ C event count
-                            ||||___ R error rate
-                            |||____ S speed/performance
-                            ||_____ O updated online
-                            |______ P prefailure warning
-
diff --git a/test/disk_tests/ssd_smart/sda_smart_expected.json b/test/disk_tests/ssd_smart/sda_smart_expected.json
deleted file mode 100644 (file)
index 2b42cf8..0000000
+++ /dev/null
@@ -1,259 +0,0 @@
-{
-    "type" : "ata",
-    "health" : "PASSED",
-    "attributes" : [
-       {
-           "worst" : 100,
-           "threshold" : 0,
-           "name" : "Raw_Read_Error_Rate",
-           "id" : "  1",
-           "flags" : "POSR-K",
-           "raw" : "0",
-           "fail" : "-",
-           "normalized" : 100,
-           "value" : 100
-       },
-       {
-           "worst" : 100,
-           "threshold" : 10,
-           "name" : "Reallocate_NAND_Blk_Cnt",
-           "id" : "  5",
-           "flags" : "-O--CK",
-           "raw" : "0",
-           "fail" : "-",
-           "normalized" : 100,
-           "value" : 100
-       },
-       {
-           "name" : "Power_On_Hours",
-           "threshold" : 0,
-           "worst" : 100,
-           "normalized" : 100,
-           "value" : 100,
-           "fail" : "-",
-           "raw" : "1309",
-           "flags" : "-O--CK",
-           "id" : "  9"
-       },
-       {
-           "threshold" : 0,
-           "name" : "Power_Cycle_Count",
-           "worst" : 100,
-           "raw" : "200",
-           "flags" : "-O--CK",
-           "fail" : "-",
-           "id" : " 12",
-           "normalized" : 100,
-           "value" : 100
-       },
-       {
-           "normalized" : 100,
-           "value" : 100,
-           "raw" : "0",
-           "flags" : "-O--CK",
-           "fail" : "-",
-           "id" : "171",
-           "threshold" : 0,
-           "name" : "Program_Fail_Count",
-           "worst" : 100
-       },
-       {
-           "normalized" : 100,
-           "value" : 100,
-           "id" : "172",
-           "raw" : "0",
-           "flags" : "-O--CK",
-           "fail" : "-",
-           "worst" : 100,
-           "threshold" : 0,
-           "name" : "Erase_Fail_Count"
-       },
-       {
-           "threshold" : 0,
-           "name" : "Ave_Block-Erase_Count",
-           "worst" : 100,
-           "raw" : "12",
-           "flags" : "-O--CK",
-           "fail" : "-",
-           "id" : "173",
-           "normalized" : 100,
-           "value" : 100
-       },
-       {
-           "name" : "Unexpect_Power_Loss_Ct",
-           "threshold" : 0,
-           "worst" : 100,
-           "normalized" : 100,
-           "value" : 100,
-           "fail" : "-",
-           "raw" : "53",
-           "flags" : "-O--CK",
-           "id" : "174"
-       },
-       {
-           "raw" : "5565",
-           "flags" : "PO--CK",
-           "fail" : "-",
-           "id" : "180",
-           "normalized" : 0,
-           "value" : 0,
-           "threshold" : 0,
-           "name" : "Unused_Reserve_NAND_Blk",
-           "worst" : 0
-       },
-       {
-           "name" : "SATA_Interfac_Downshift",
-           "threshold" : 0,
-           "worst" : 100,
-           "fail" : "-",
-           "raw" : "0",
-           "flags" : "-O--CK",
-           "id" : "183",
-           "normalized" : 100,
-           "value" : 100
-       },
-       {
-           "worst" : 100,
-           "threshold" : 0,
-           "name" : "Error_Correction_Count",
-           "id" : "184",
-           "flags" : "-O--CK",
-           "raw" : "0",
-           "fail" : "-",
-           "normalized" : 100,
-           "value" : 100
-       },
-       {
-           "name" : "Reported_Uncorrect",
-           "threshold" : 0,
-           "worst" : 100,
-           "normalized" : 100,
-           "value" : 100,
-           "fail" : "-",
-           "flags" : "-O--CK",
-           "raw" : "0",
-           "id" : "187"
-       },
-       {
-           "worst" : 54,
-           "name" : "Temperature_Celsius",
-           "threshold" : 0,
-           "id" : "194",
-           "fail" : "-",
-           "flags" : "-O---K",
-           "raw" : "32 (Min/Max 22/46)",
-           "normalized" : 68,
-           "value" : 68
-       },
-       {
-           "raw" : "0",
-           "flags" : "-O--CK",
-           "fail" : "-",
-           "id" : "196",
-           "normalized" : 100,
-           "value" : 100,
-           "threshold" : 0,
-           "name" : "Reallocated_Event_Count",
-           "worst" : 100
-       },
-       {
-           "id" : "197",
-           "fail" : "-",
-           "flags" : "-O--CK",
-           "raw" : "0",
-           "normalized" : 100,
-           "value" : 100,
-           "worst" : 100,
-           "name" : "Current_Pending_Sector",
-           "threshold" : 0
-       },
-       {
-           "threshold" : 0,
-           "name" : "Offline_Uncorrectable",
-           "worst" : 100,
-           "flags" : "----CK",
-           "raw" : "0",
-           "fail" : "-",
-           "id" : "198",
-           "normalized" : 100,
-           "value" : 100
-       },
-       {
-           "worst" : 100,
-           "name" : "UDMA_CRC_Error_Count",
-           "threshold" : 0,
-           "id" : "199",
-           "fail" : "-",
-           "raw" : "0",
-           "flags" : "-O--CK",
-           "normalized" : 100,
-           "value" : 100
-       },
-       {
-           "normalized" : 100,
-           "value" : 100,
-           "id" : "202",
-           "flags" : "----CK",
-           "raw" : "0",
-           "fail" : "-",
-           "worst" : 100,
-           "threshold" : 1,
-           "name" : "Percent_Lifetime_Used"
-       },
-       {
-           "name" : "Write_Error_Rate",
-           "threshold" : 0,
-           "worst" : 100,
-           "normalized" : 100,
-           "value" : 100,
-           "fail" : "-",
-           "raw" : "0",
-           "flags" : "-OSR--",
-           "id" : "206"
-       },
-       {
-           "name" : "Success_RAIN_Recov_Cnt",
-           "threshold" : 0,
-           "worst" : 100,
-           "fail" : "-",
-           "raw" : "0",
-           "flags" : "-O--CK",
-           "id" : "210",
-           "normalized" : 100,
-           "value" : 100
-       },
-       {
-           "normalized" : 100,
-           "value" : 100,
-           "id" : "246",
-           "fail" : "-",
-           "raw" : "6751830403",
-           "flags" : "-O--CK",
-           "worst" : 100,
-           "name" : "Total_Host_Sector_Write",
-           "threshold" : 0
-       },
-       {
-           "name" : "Host_Program_Page_Count",
-           "threshold" : 0,
-           "worst" : 100,
-           "fail" : "-",
-           "raw" : "211228065",
-           "flags" : "-O--CK",
-           "id" : "247",
-           "normalized" : 100,
-           "value" : 100
-       },
-       {
-           "normalized" : 100,
-           "value" : 100,
-           "fail" : "-",
-           "raw" : "253276904",
-           "flags" : "-O--CK",
-           "id" : "248",
-           "name" : "Bckgnd_Program_Page_Cnt",
-           "threshold" : 0,
-           "worst" : 100
-       }
-    ]
-}
diff --git a/test/disk_tests/ssd_smart/sda_udevadm b/test/disk_tests/ssd_smart/sda_udevadm
deleted file mode 100644 (file)
index 3290a13..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-E: DEVNAME=/dev/sda
-E: DEVTYPE=disk
-E: ID_ATA_ROTATION_RATE_RPM=0
-E: ID_BUS=ata
-E: ID_MODEL=Crucial_CT500MX200SSD1
-E: ID_PART_TABLE_TYPE=gpt
-E: ID_SERIAL=Crucial_CT500MX200SSD1_000000000000
-E: ID_SERIAL_SHORT=000000000000
-E: ID_TYPE=disk
-E: ID_WWN=0x0000000000000000
-
diff --git a/test/disk_tests/ssd_smart/sdb/device/vendor b/test/disk_tests/ssd_smart/sdb/device/vendor
deleted file mode 100644 (file)
index 531030d..0000000
+++ /dev/null
@@ -1 +0,0 @@
-ATA
diff --git a/test/disk_tests/ssd_smart/sdb/queue/rotational b/test/disk_tests/ssd_smart/sdb/queue/rotational
deleted file mode 100644 (file)
index 573541a..0000000
+++ /dev/null
@@ -1 +0,0 @@
-0
diff --git a/test/disk_tests/ssd_smart/sdb/size b/test/disk_tests/ssd_smart/sdb/size
deleted file mode 100644 (file)
index 83b33d2..0000000
+++ /dev/null
@@ -1 +0,0 @@
-1000
diff --git a/test/disk_tests/ssd_smart/sdb_smart b/test/disk_tests/ssd_smart/sdb_smart
deleted file mode 100644 (file)
index 79eea84..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-smartctl 6.6 2016-05-31 r4324 [x86_64-linux-4.4.21-1-pve] (local build)
-Copyright (C) 2002-16, Bruce Allen, Christian Franke, www.smartmontools.org
-
-=== START OF READ SMART DATA SECTION ===
-SMART overall-health self-assessment test result: PASSED
-
-SMART Attributes Data Structure revision number: 1
-Vendor Specific SMART Attributes with Thresholds:
-ID# ATTRIBUTE_NAME          FLAGS    VALUE WORST THRESH FAIL RAW_VALUE
-  5 Reallocated_Sector_Ct   -O--CK   100   100   000    -    0
-  9 Power_On_Hours          -O--CK   100   100   000    -    1259
- 12 Power_Cycle_Count       -O--CK   100   100   000    -    191
-170 Available_Reservd_Space PO--CK   100   100   010    -    0
-171 Program_Fail_Count      -O--CK   100   100   000    -    0
-172 Erase_Fail_Count        -O--CK   100   100   000    -    0
-174 Unsafe_Shutdown_Count   -O--CK   100   100   000    -    164
-175 Power_Loss_Cap_Test     PO--CK   100   100   010    -    5670 (1 343)
-183 SATA_Downshift_Count    -O--CK   100   100   000    -    0
-184 End-to-End_Error        PO--CK   100   100   090    -    0
-187 Reported_Uncorrect      -O--CK   100   100   000    -    0
-190 Temperature_Case        -O---K   072   071   000    -    28 (Min/Max 21/30)
-192 Unsafe_Shutdown_Count   -O--CK   100   100   000    -    164
-194 Temperature_Internal    -O---K   100   100   000    -    28
-197 Current_Pending_Sector  -O--C-   100   100   000    -    0
-199 CRC_Error_Count         -OSRCK   100   100   000    -    0
-225 Host_Writes_32MiB       -O--CK   100   100   000    -    296600
-226 Workld_Media_Wear_Indic -O--CK   100   100   000    -    3747
-227 Workld_Host_Reads_Perc  -O--CK   100   100   000    -    0
-228 Workload_Minutes        -O--CK   100   100   000    -    75111
-232 Available_Reservd_Space PO--CK   100   100   010    -    0
-233 Media_Wearout_Indicator -O--CK   097   097   000    -    0
-234 Thermal_Throttle        -O--CK   100   100   000    -    0/0
-241 Host_Writes_32MiB       -O--CK   100   100   000    -    296600
-242 Host_Reads_32MiB        -O--CK   100   100   000    -    1207
-                            ||||||_ K auto-keep
-                            |||||__ C event count
-                            ||||___ R error rate
-                            |||____ S speed/performance
-                            ||_____ O updated online
-                            |______ P prefailure warning
-
diff --git a/test/disk_tests/ssd_smart/sdb_smart_expected.json b/test/disk_tests/ssd_smart/sdb_smart_expected.json
deleted file mode 100644 (file)
index 0614754..0000000
+++ /dev/null
@@ -1,281 +0,0 @@
-{
-    "health" : "PASSED",
-    "type" : "ata",
-    "attributes" : [
-       {
-           "name" : "Reallocated_Sector_Ct",
-           "threshold" : 0,
-           "worst" : 100,
-           "fail" : "-",
-           "flags" : "-O--CK",
-           "raw" : "0",
-           "id" : "  5",
-           "normalized" : 100,
-           "value" : 100
-       },
-       {
-           "worst" : 100,
-           "name" : "Power_On_Hours",
-           "threshold" : 0,
-           "id" : "  9",
-           "fail" : "-",
-           "raw" : "1259",
-           "flags" : "-O--CK",
-           "normalized" : 100,
-           "value" : 100
-       },
-       {
-           "normalized" : 100,
-           "value" : 100,
-           "id" : " 12",
-           "raw" : "191",
-           "flags" : "-O--CK",
-           "fail" : "-",
-           "worst" : 100,
-           "threshold" : 0,
-           "name" : "Power_Cycle_Count"
-       },
-       {
-           "id" : "170",
-           "raw" : "0",
-           "flags" : "PO--CK",
-           "fail" : "-",
-           "normalized" : 100,
-           "value" : 100,
-           "worst" : 100,
-           "threshold" : 10,
-           "name" : "Available_Reservd_Space"
-       },
-       {
-           "threshold" : 0,
-           "name" : "Program_Fail_Count",
-           "worst" : 100,
-           "normalized" : 100,
-           "value" : 100,
-           "raw" : "0",
-           "flags" : "-O--CK",
-           "fail" : "-",
-           "id" : "171"
-       },
-       {
-           "worst" : 100,
-           "name" : "Erase_Fail_Count",
-           "threshold" : 0,
-           "id" : "172",
-           "fail" : "-",
-           "flags" : "-O--CK",
-           "raw" : "0",
-           "normalized" : 100,
-           "value" : 100
-       },
-       {
-           "worst" : 100,
-           "threshold" : 0,
-           "name" : "Unsafe_Shutdown_Count",
-           "normalized" : 100,
-           "value" : 100,
-           "id" : "174",
-           "raw" : "164",
-           "flags" : "-O--CK",
-           "fail" : "-"
-       },
-       {
-           "normalized" : 100,
-           "value" : 100,
-           "fail" : "-",
-           "flags" : "PO--CK",
-           "raw" : "5670 (1 343)",
-           "id" : "175",
-           "name" : "Power_Loss_Cap_Test",
-           "threshold" : 10,
-           "worst" : 100
-       },
-       {
-           "worst" : 100,
-           "threshold" : 0,
-           "name" : "SATA_Downshift_Count",
-           "normalized" : 100,
-           "value" : 100,
-           "id" : "183",
-           "raw" : "0",
-           "flags" : "-O--CK",
-           "fail" : "-"
-       },
-       {
-           "worst" : 100,
-           "name" : "End-to-End_Error",
-           "threshold" : 90,
-           "normalized" : 100,
-           "value" : 100,
-           "id" : "184",
-           "fail" : "-",
-           "raw" : "0",
-           "flags" : "PO--CK"
-       },
-       {
-           "worst" : 100,
-           "name" : "Reported_Uncorrect",
-           "threshold" : 0,
-           "normalized" : 100,
-           "value" : 100,
-           "id" : "187",
-           "fail" : "-",
-           "raw" : "0",
-           "flags" : "-O--CK"
-       },
-       {
-           "worst" : 71,
-           "name" : "Temperature_Case",
-           "threshold" : 0,
-           "id" : "190",
-           "fail" : "-",
-           "raw" : "28 (Min/Max 21/30)",
-           "flags" : "-O---K",
-           "normalized" : 72,
-           "value" : 72
-       },
-       {
-           "worst" : 100,
-           "threshold" : 0,
-           "name" : "Unsafe_Shutdown_Count",
-           "normalized" : 100,
-           "value" : 100,
-           "id" : "192",
-           "raw" : "164",
-           "flags" : "-O--CK",
-           "fail" : "-"
-       },
-       {
-           "id" : "194",
-           "fail" : "-",
-           "raw" : "28",
-           "flags" : "-O---K",
-           "normalized" : 100,
-           "value" : 100,
-           "worst" : 100,
-           "name" : "Temperature_Internal",
-           "threshold" : 0
-       },
-       {
-           "worst" : 100,
-           "threshold" : 0,
-           "name" : "Current_Pending_Sector",
-           "normalized" : 100,
-           "value" : 100,
-           "id" : "197",
-           "raw" : "0",
-           "flags" : "-O--C-",
-           "fail" : "-"
-       },
-       {
-           "worst" : 100,
-           "threshold" : 0,
-           "name" : "CRC_Error_Count",
-           "normalized" : 100,
-           "value" : 100,
-           "id" : "199",
-           "flags" : "-OSRCK",
-           "raw" : "0",
-           "fail" : "-"
-       },
-       {
-           "worst" : 100,
-           "name" : "Host_Writes_32MiB",
-           "threshold" : 0,
-           "id" : "225",
-           "fail" : "-",
-           "flags" : "-O--CK",
-           "raw" : "296600",
-           "normalized" : 100,
-           "value" : 100
-       },
-       {
-           "normalized" : 100,
-           "value" : 100,
-           "id" : "226",
-           "raw" : "3747",
-           "flags" : "-O--CK",
-           "fail" : "-",
-           "worst" : 100,
-           "threshold" : 0,
-           "name" : "Workld_Media_Wear_Indic"
-       },
-       {
-           "worst" : 100,
-           "threshold" : 0,
-           "name" : "Workld_Host_Reads_Perc",
-           "id" : "227",
-           "raw" : "0",
-           "flags" : "-O--CK",
-           "fail" : "-",
-           "normalized" : 100,
-           "value" : 100
-       },
-       {
-           "threshold" : 0,
-           "name" : "Workload_Minutes",
-           "worst" : 100,
-           "raw" : "75111",
-           "flags" : "-O--CK",
-           "fail" : "-",
-           "id" : "228",
-           "normalized" : 100,
-           "value" : 100
-       },
-       {
-           "fail" : "-",
-           "raw" : "0",
-           "flags" : "PO--CK",
-           "id" : "232",
-           "normalized" : 100,
-           "value" : 100,
-           "name" : "Available_Reservd_Space",
-           "threshold" : 10,
-           "worst" : 100
-       },
-       {
-           "normalized" : 97,
-           "value" : 97,
-           "fail" : "-",
-           "flags" : "-O--CK",
-           "raw" : "0",
-           "id" : "233",
-           "name" : "Media_Wearout_Indicator",
-           "threshold" : 0,
-           "worst" : 97
-       },
-       {
-           "normalized" : 100,
-           "value" : 100,
-           "fail" : "-",
-           "raw" : "0/0",
-           "flags" : "-O--CK",
-           "id" : "234",
-           "name" : "Thermal_Throttle",
-           "threshold" : 0,
-           "worst" : 100
-       },
-       {
-           "worst" : 100,
-           "name" : "Host_Writes_32MiB",
-           "threshold" : 0,
-           "id" : "241",
-           "fail" : "-",
-           "raw" : "296600",
-           "flags" : "-O--CK",
-           "normalized" : 100,
-           "value" : 100
-       },
-       {
-           "normalized" : 100,
-           "value" : 100,
-           "id" : "242",
-           "fail" : "-",
-           "raw" : "1207",
-           "flags" : "-O--CK",
-           "worst" : 100,
-           "name" : "Host_Reads_32MiB",
-           "threshold" : 0
-       }
-    ]
-}
diff --git a/test/disk_tests/ssd_smart/sdb_udevadm b/test/disk_tests/ssd_smart/sdb_udevadm
deleted file mode 100644 (file)
index bb723c8..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-E: DEVNAME=/dev/sdb
-E: DEVTYPE=disk
-E: ID_ATA_ROTATION_RATE_RPM=0
-E: ID_BUS=ata
-E: ID_MODEL=INTEL_SSDSC2BB080G6
-E: ID_PART_TABLE_TYPE=gpt
-E: ID_PART_TABLE_UUID=21c18951-8568-488c-a2a8-24441eb4b165
-E: ID_SERIAL=INTEL_SSDSC2BB080G6_000000000000000000
-E: ID_SERIAL_SHORT=000000000000000000
-E: ID_TYPE=disk
-E: ID_WWN=0x0000000000000000
-E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/test/disk_tests/ssd_smart/sdc/device/vendor b/test/disk_tests/ssd_smart/sdc/device/vendor
deleted file mode 100644 (file)
index 531030d..0000000
+++ /dev/null
@@ -1 +0,0 @@
-ATA
diff --git a/test/disk_tests/ssd_smart/sdc/queue/rotational b/test/disk_tests/ssd_smart/sdc/queue/rotational
deleted file mode 100644 (file)
index 573541a..0000000
+++ /dev/null
@@ -1 +0,0 @@
-0
diff --git a/test/disk_tests/ssd_smart/sdc/size b/test/disk_tests/ssd_smart/sdc/size
deleted file mode 100644 (file)
index 83b33d2..0000000
+++ /dev/null
@@ -1 +0,0 @@
-1000
diff --git a/test/disk_tests/ssd_smart/sdc_smart b/test/disk_tests/ssd_smart/sdc_smart
deleted file mode 100644 (file)
index 927f6d2..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-smartctl 6.4 2014-10-07 r4002 [x86_64-linux-4.4.8-1-pve] (local build)
-Copyright (C) 2002-14, Bruce Allen, Christian Franke, www.smartmontools.org
-
-=== START OF READ SMART DATA SECTION ===
-SMART overall-health self-assessment test result: PASSED
-
-SMART Attributes Data Structure revision number: 1
-Vendor Specific SMART Attributes with Thresholds:
-ID# ATTRIBUTE_NAME          FLAGS    VALUE WORST THRESH FAIL RAW_VALUE
-177 Wear_Leveling_Count     PO--CK   099   099   000    -    34
-                            ||||||_ K auto-keep
-                            |||||__ C event count
-                            ||||___ R error rate
-                            |||____ S speed/performance
-                            ||_____ O updated onlinie
-                            |______ P prefailure warning
diff --git a/test/disk_tests/ssd_smart/sdc_smart_expected.json b/test/disk_tests/ssd_smart/sdc_smart_expected.json
deleted file mode 100644 (file)
index fad095b..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-{
-    "type" : "ata",
-    "health" : "PASSED",
-    "attributes" : [
-       {
-           "normalized" : 99,
-           "value" : 99,
-           "fail" : "-",
-           "raw" : "34",
-           "flags" : "PO--CK",
-           "id" : "177",
-           "name" : "Wear_Leveling_Count",
-           "threshold" : 0,
-           "worst" : 99
-       }
-    ]
-}
diff --git a/test/disk_tests/ssd_smart/sdc_udevadm b/test/disk_tests/ssd_smart/sdc_udevadm
deleted file mode 100644 (file)
index 5adb3ef..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-E: DEVNAME=/dev/sdc
-E: DEVTYPE=disk
-E: ID_ATA_ROTATION_RATE_RPM=0
-E: ID_BUS=ata
-E: ID_MODEL=Samsung SSD 850 PRO 512GB
-E: ID_PART_TABLE_TYPE=gpt
-E: ID_SERIAL=SAMSUNG_00000000
-E: ID_SERIAL_SHORT=000000000000
-E: ID_TYPE=disk
-E: ID_WWN=0x0000000000000000
-
diff --git a/test/disk_tests/ssd_smart/sdd/device/vendor b/test/disk_tests/ssd_smart/sdd/device/vendor
deleted file mode 100644 (file)
index 531030d..0000000
+++ /dev/null
@@ -1 +0,0 @@
-ATA
diff --git a/test/disk_tests/ssd_smart/sdd/queue/rotational b/test/disk_tests/ssd_smart/sdd/queue/rotational
deleted file mode 100644 (file)
index 573541a..0000000
+++ /dev/null
@@ -1 +0,0 @@
-0
diff --git a/test/disk_tests/ssd_smart/sdd/size b/test/disk_tests/ssd_smart/sdd/size
deleted file mode 100644 (file)
index 83b33d2..0000000
+++ /dev/null
@@ -1 +0,0 @@
-1000
diff --git a/test/disk_tests/ssd_smart/sdd_smart b/test/disk_tests/ssd_smart/sdd_smart
deleted file mode 100644 (file)
index 2ad16af..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-smartctl 6.4 2014-10-07 r4002 [x86_64-linux-4.4.19-1-pve] (local build)
-Copyright (C) 2002-14, Bruce Allen, Christian Franke, www.smartmontools.org
-
-=== START OF READ SMART DATA SECTION ===
-SMART overall-health self-assessment test result: PASSED
-
-SMART Attributes Data Structure revision number: 4
-Vendor Specific SMART Attributes with Thresholds:
-ID# ATTRIBUTE_NAME          FLAGS    VALUE WORST THRESH FAIL RAW_VALUE
-  5 Reallocated_Sector_Ct   -O--CK   100   100   ---    -    0
-  9 Power_On_Hours          -O--CK   100   100   ---    -    799
- 12 Power_Cycle_Count       -O--CK   100   100   ---    -    92
-165 Unknown_Attribute       -O--CK   100   100   ---    -    9699447
-166 Unknown_Attribute       -O--CK   100   100   ---    -    1
-167 Unknown_Attribute       -O--CK   100   100   ---    -    46
-168 Unknown_Attribute       -O--CK   100   100   ---    -    5
-169 Unknown_Attribute       -O--CK   100   100   ---    -    1079
-170 Unknown_Attribute       -O--CK   100   100   ---    -    0
-171 Unknown_Attribute       -O--CK   100   100   ---    -    0
-172 Unknown_Attribute       -O--CK   100   100   ---    -    0
-173 Unknown_Attribute       -O--CK   100   100   ---    -    1
-174 Unknown_Attribute       -O--CK   100   100   ---    -    22
-184 End-to-End_Error        -O--CK   100   100   ---    -    0
-187 Reported_Uncorrect      -O--CK   100   100   ---    -    0
-188 Command_Timeout         -O--CK   100   100   ---    -    0
-194 Temperature_Celsius     -O---K   073   064   ---    -    27 (Min/Max 23/64)
-199 UDMA_CRC_Error_Count    -O--CK   100   100   ---    -    0
-230 Unknown_SSD_Attribute   -O--CK   100   100   ---    -    146029805602
-232 Available_Reservd_Space PO--CK   100   100   004    -    100
-233 Media_Wearout_Indicator -O--CK   100   100   ---    -    1574
-234 Unknown_Attribute       -O--CK   100   100   ---    -    2303
-241 Total_LBAs_Written      ----CK   253   253   ---    -    2111
-242 Total_LBAs_Read         ----CK   253   253   ---    -    1542
-244 Unknown_Attribute       -O--CK   000   100   ---    -    0
-                            ||||||_ K auto-keep
-                            |||||__ C event count
-                            ||||___ R error rate
-                            |||____ S speed/performance
-                            ||_____ O updated online
-                            |______ P prefailure warning
diff --git a/test/disk_tests/ssd_smart/sdd_smart_expected.json b/test/disk_tests/ssd_smart/sdd_smart_expected.json
deleted file mode 100644 (file)
index 8e0bebf..0000000
+++ /dev/null
@@ -1,281 +0,0 @@
-{
-    "attributes" : [
-       {
-           "id" : "  5",
-           "flags" : "-O--CK",
-           "fail" : "-",
-           "worst" : 100,
-           "raw" : "0",
-           "threshold" : 0,
-           "normalized" : 100,
-           "value" : 100,
-           "name" : "Reallocated_Sector_Ct"
-       },
-       {
-           "worst" : 100,
-           "fail" : "-",
-           "flags" : "-O--CK",
-           "id" : "  9",
-           "name" : "Power_On_Hours",
-           "threshold" : 0,
-           "normalized" : 100,
-           "value" : 100,
-           "raw" : "799"
-       },
-       {
-           "name" : "Power_Cycle_Count",
-           "raw" : "92",
-           "threshold" : 0,
-           "normalized" : 100,
-           "value" : 100,
-           "fail" : "-",
-           "worst" : 100,
-           "id" : " 12",
-           "flags" : "-O--CK"
-       },
-       {
-           "worst" : 100,
-           "fail" : "-",
-           "flags" : "-O--CK",
-           "id" : "165",
-           "name" : "Unknown_Attribute",
-           "threshold" : 0,
-           "normalized" : 100,
-           "value" : 100,
-           "raw" : "9699447"
-       },
-       {
-           "normalized" : 100,
-           "value" : 100,
-           "threshold" : 0,
-           "raw" : "1",
-           "name" : "Unknown_Attribute",
-           "flags" : "-O--CK",
-           "id" : "166",
-           "worst" : 100,
-           "fail" : "-"
-       },
-       {
-           "id" : "167",
-           "flags" : "-O--CK",
-           "fail" : "-",
-           "worst" : 100,
-           "raw" : "46",
-           "normalized" : 100,
-           "value" : 100,
-           "threshold" : 0,
-           "name" : "Unknown_Attribute"
-       },
-       {
-           "name" : "Unknown_Attribute",
-           "raw" : "5",
-           "normalized" : 100,
-           "value" : 100,
-           "threshold" : 0,
-           "fail" : "-",
-           "worst" : 100,
-           "id" : "168",
-           "flags" : "-O--CK"
-       },
-       {
-           "flags" : "-O--CK",
-           "id" : "169",
-           "worst" : 100,
-           "fail" : "-",
-           "normalized" : 100,
-           "value" : 100,
-           "threshold" : 0,
-           "raw" : "1079",
-           "name" : "Unknown_Attribute"
-       },
-       {
-           "raw" : "0",
-           "threshold" : 0,
-           "normalized" : 100,
-           "value" : 100,
-           "name" : "Unknown_Attribute",
-           "id" : "170",
-           "flags" : "-O--CK",
-           "fail" : "-",
-           "worst" : 100
-       },
-       {
-           "fail" : "-",
-           "worst" : 100,
-           "id" : "171",
-           "flags" : "-O--CK",
-           "name" : "Unknown_Attribute",
-           "raw" : "0",
-           "normalized" : 100,
-           "value" : 100,
-           "threshold" : 0
-       },
-       {
-           "name" : "Unknown_Attribute",
-           "raw" : "0",
-           "normalized" : 100,
-           "value" : 100,
-           "threshold" : 0,
-           "fail" : "-",
-           "worst" : 100,
-           "id" : "172",
-           "flags" : "-O--CK"
-       },
-       {
-           "name" : "Unknown_Attribute",
-           "threshold" : 0,
-           "normalized" : 100,
-           "value" : 100,
-           "raw" : "1",
-           "worst" : 100,
-           "fail" : "-",
-           "flags" : "-O--CK",
-           "id" : "173"
-       },
-       {
-           "name" : "Unknown_Attribute",
-           "normalized" : 100,
-           "value" : 100,
-           "threshold" : 0,
-           "raw" : "22",
-           "worst" : 100,
-           "fail" : "-",
-           "flags" : "-O--CK",
-           "id" : "174"
-       },
-       {
-           "worst" : 100,
-           "fail" : "-",
-           "flags" : "-O--CK",
-           "id" : "184",
-           "name" : "End-to-End_Error",
-           "normalized" : 100,
-           "value" : 100,
-           "threshold" : 0,
-           "raw" : "0"
-       },
-       {
-           "name" : "Reported_Uncorrect",
-           "normalized" : 100,
-           "value" : 100,
-           "threshold" : 0,
-           "raw" : "0",
-           "worst" : 100,
-           "fail" : "-",
-           "flags" : "-O--CK",
-           "id" : "187"
-       },
-       {
-           "name" : "Command_Timeout",
-           "raw" : "0",
-           "normalized" : 100,
-           "value" : 100,
-           "threshold" : 0,
-           "fail" : "-",
-           "worst" : 100,
-           "id" : "188",
-           "flags" : "-O--CK"
-       },
-       {
-           "threshold" : 0,
-           "normalized" : 73,
-           "value" : 73,
-           "raw" : "27 (Min/Max 23/64)",
-           "name" : "Temperature_Celsius",
-           "flags" : "-O---K",
-           "id" : "194",
-           "worst" : 64,
-           "fail" : "-"
-       },
-       {
-           "worst" : 100,
-           "fail" : "-",
-           "flags" : "-O--CK",
-           "id" : "199",
-           "name" : "UDMA_CRC_Error_Count",
-           "normalized" : 100,
-           "value" : 100,
-           "threshold" : 0,
-           "raw" : "0"
-       },
-       {
-           "name" : "Unknown_SSD_Attribute",
-           "threshold" : 0,
-           "normalized" : 100,
-           "value" : 100,
-           "raw" : "146029805602",
-           "worst" : 100,
-           "fail" : "-",
-           "flags" : "-O--CK",
-           "id" : "230"
-       },
-       {
-           "raw" : "100",
-           "normalized" : 100,
-           "value" : 100,
-           "threshold" : 4,
-           "name" : "Available_Reservd_Space",
-           "id" : "232",
-           "flags" : "PO--CK",
-           "fail" : "-",
-           "worst" : 100
-       },
-       {
-           "threshold" : 0,
-           "normalized" : 100,
-           "value" : 100,
-           "raw" : "1574",
-           "name" : "Media_Wearout_Indicator",
-           "flags" : "-O--CK",
-           "id" : "233",
-           "worst" : 100,
-           "fail" : "-"
-       },
-       {
-           "id" : "234",
-           "flags" : "-O--CK",
-           "fail" : "-",
-           "worst" : 100,
-           "raw" : "2303",
-           "normalized" : 100,
-           "value" : 100,
-           "threshold" : 0,
-           "name" : "Unknown_Attribute"
-       },
-       {
-           "fail" : "-",
-           "worst" : 253,
-           "id" : "241",
-           "flags" : "----CK",
-           "name" : "Total_LBAs_Written",
-           "raw" : "2111",
-           "normalized" : 253,
-           "value" : 253,
-           "threshold" : 0
-       },
-       {
-           "worst" : 253,
-           "fail" : "-",
-           "flags" : "----CK",
-           "id" : "242",
-           "name" : "Total_LBAs_Read",
-           "threshold" : 0,
-           "normalized" : 253,
-           "value" : 253,
-           "raw" : "1542"
-       },
-       {
-           "name" : "Unknown_Attribute",
-           "normalized" : 0,
-           "value" : 0,
-           "threshold" : 0,
-           "raw" : "0",
-           "worst" : 100,
-           "fail" : "-",
-           "flags" : "-O--CK",
-           "id" : "244"
-       }
-    ],
-    "type" : "ata",
-    "health" : "PASSED"
-}
diff --git a/test/disk_tests/ssd_smart/sdd_udevadm b/test/disk_tests/ssd_smart/sdd_udevadm
deleted file mode 100644 (file)
index d46a7d4..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-E: DEVNAME=/dev/sdd
-E: DEVTYPE=disk
-E: ID_ATA_ROTATION_RATE_RPM=0
-E: ID_BUS=ata
-E: ID_MODEL=SanDisk SD8SB8U1T001122
-E: ID_PART_TABLE_TYPE=gpt
-E: ID_SERIAL=SANDISK_00000000000
-E: ID_SERIAL_SHORT=000000000000
-E: ID_TYPE=disk
-E: ID_WWN=0x0000000000000000
-
diff --git a/test/disk_tests/ssd_smart/sde/device/vendor b/test/disk_tests/ssd_smart/sde/device/vendor
deleted file mode 100644 (file)
index 531030d..0000000
+++ /dev/null
@@ -1 +0,0 @@
-ATA
diff --git a/test/disk_tests/ssd_smart/sde/queue/rotational b/test/disk_tests/ssd_smart/sde/queue/rotational
deleted file mode 100644 (file)
index 573541a..0000000
+++ /dev/null
@@ -1 +0,0 @@
-0
diff --git a/test/disk_tests/ssd_smart/sde/size b/test/disk_tests/ssd_smart/sde/size
deleted file mode 100644 (file)
index 83b33d2..0000000
+++ /dev/null
@@ -1 +0,0 @@
-1000
diff --git a/test/disk_tests/ssd_smart/sde_smart b/test/disk_tests/ssd_smart/sde_smart
deleted file mode 100644 (file)
index f6f01d6..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-smartctl 7.1 2019-12-30 r5022 [x86_64-linux-5.4.65-1-pve] (local build)
-Copyright (C) 2002-19, Bruce Allen, Christian Franke, www.smartmontools.org
-
-=== START OF READ SMART DATA SECTION ===
-SMART overall-health self-assessment test result: PASSED
-
-SMART Attributes Data Structure revision number: 10
-Vendor Specific SMART Attributes with Thresholds:
-ID# ATTRIBUTE_NAME          FLAGS    VALUE WORST THRESH FAIL RAW_VALUE
-  1 Raw_Read_Error_Rate     -O--CK   120   120   050    -    0/0
-  5 Retired_Block_Count     PO--CK   100   100   003    -    0
-  9 Power_On_Hours_and_Msec -O--CK   091   091   000    -    7963h+07m+54.620s
- 12 Power_Cycle_Count       -O--CK   099   099   000    -    1153
-171 Program_Fail_Count      -O-R--   100   100   000    -    0
-172 Erase_Fail_Count        -O--CK   100   100   000    -    0
-174 Unexpect_Power_Loss_Ct  ----CK   000   000   000    -    113
-177 Wear_Range_Delta        ------   000   000   000    -    1
-181 Program_Fail_Count      -O-R--   100   100   000    -    0
-182 Erase_Fail_Count        -O--CK   100   100   000    -    0
-187 Reported_Uncorrect      -O--C-   100   100   000    -    0
-189 Airflow_Temperature_Cel ------   027   049   000    -    27 (Min/Max 2/49)
-194 Temperature_Celsius     -O---K   027   049   000    -    27 (Min/Max 2/49)
-195 ECC_Uncorr_Error_Count  --SRC-   120   120   000    -    0/0
-196 Reallocated_Event_Count PO--CK   100   100   003    -    0
-201 Unc_Soft_Read_Err_Rate  --SRC-   120   120   000    -    0/0
-204 Soft_ECC_Correct_Rate   --SRC-   120   120   000    -    0/0
-230 Life_Curve_Status       PO--C-   100   100   000    -    100
-231 SSD_Life_Left           ------   091   091   011    -    4294967296
-233 SandForce_Internal      -O--CK   000   000   000    -    6317
-234 SandForce_Internal      -O--CK   000   000   000    -    4252
-241 Lifetime_Writes_GiB     -O--CK   000   000   000    -    4252
-242 Lifetime_Reads_GiB      -O--CK   000   000   000    -    34599
-244 Unknown_Attribute       ------   099   099   010    -    4063273
-                            ||||||_ K auto-keep
-                            |||||__ C event count
-                            ||||___ R error rate
-                            |||____ S speed/performance
-                            ||_____ O updated online
-                            |______ P prefailure warning
-
diff --git a/test/disk_tests/ssd_smart/sde_smart_expected.json b/test/disk_tests/ssd_smart/sde_smart_expected.json
deleted file mode 100644 (file)
index 0fa1214..0000000
+++ /dev/null
@@ -1,270 +0,0 @@
-{
-    "health": "PASSED",
-    "type": "ata",
-    "attributes": [
-       {
-           "fail": "-",
-           "id": "  1",
-           "raw": "0/0",
-           "flags": "-O--CK",
-           "name": "Raw_Read_Error_Rate",
-           "threshold": 50,
-           "normalized": 120,
-           "value": 120,
-           "worst": 120
-       },
-       {
-           "id": "  5",
-           "fail": "-",
-           "normalized": 100,
-           "value": 100,
-           "worst": 100,
-           "threshold": 3,
-           "name": "Retired_Block_Count",
-           "flags": "PO--CK",
-           "raw": "0"
-       },
-       {
-           "fail": "-",
-           "id": "  9",
-           "raw": "7963h+07m+54.620s",
-           "flags": "-O--CK",
-           "worst": 91,
-           "normalized": 91,
-           "value": 91,
-           "name": "Power_On_Hours_and_Msec",
-           "threshold": 0
-       },
-       {
-           "id": " 12",
-           "fail": "-",
-           "threshold": 0,
-           "name": "Power_Cycle_Count",
-           "worst": 99,
-           "normalized": 99,
-           "value": 99,
-           "flags": "-O--CK",
-           "raw": "1153"
-       },
-       {
-           "flags": "-O-R--",
-           "raw": "0",
-           "worst": 100,
-           "normalized": 100,
-           "value": 100,
-           "threshold": 0,
-           "name": "Program_Fail_Count",
-           "fail": "-",
-           "id": "171"
-       },
-       {
-           "fail": "-",
-           "id": "172",
-           "flags": "-O--CK",
-           "raw": "0",
-           "name": "Erase_Fail_Count",
-           "threshold": 0,
-           "worst": 100,
-           "normalized": 100,
-           "value": 100
-       },
-       {
-           "fail": "-",
-           "id": "174",
-           "raw": "113",
-           "flags": "----CK",
-           "normalized": 0,
-           "value": 0,
-           "worst": 0,
-           "threshold": 0,
-           "name": "Unexpect_Power_Loss_Ct"
-       },
-       {
-           "id": "177",
-           "fail": "-",
-           "normalized": 0,
-           "value": 0,
-           "worst": 0,
-           "name": "Wear_Range_Delta",
-           "threshold": 0,
-           "flags": "------",
-           "raw": "1"
-       },
-       {
-           "flags": "-O-R--",
-           "raw": "0",
-           "threshold": 0,
-           "name": "Program_Fail_Count",
-           "worst": 100,
-           "normalized": 100,
-           "value": 100,
-           "fail": "-",
-           "id": "181"
-       },
-       {
-           "threshold": 0,
-           "name": "Erase_Fail_Count",
-           "normalized": 100,
-           "value": 100,
-           "worst": 100,
-           "flags": "-O--CK",
-           "raw": "0",
-           "id": "182",
-           "fail": "-"
-       },
-       {
-           "flags": "-O--C-",
-           "raw": "0",
-           "normalized": 100,
-           "value": 100,
-           "worst": 100,
-           "threshold": 0,
-           "name": "Reported_Uncorrect",
-           "fail": "-",
-           "id": "187"
-       },
-       {
-           "normalized": 27,
-           "value": 27,
-           "worst": 49,
-           "name": "Airflow_Temperature_Cel",
-           "threshold": 0,
-           "flags": "------",
-           "raw": "27 (Min/Max 2/49)",
-           "id": "189",
-           "fail": "-"
-       },
-       {
-           "threshold": 0,
-           "name": "Temperature_Celsius",
-           "worst": 49,
-           "normalized": 27,
-           "value": 27,
-           "flags": "-O---K",
-           "raw": "27 (Min/Max 2/49)",
-           "id": "194",
-           "fail": "-"
-       },
-       {
-           "id": "195",
-           "fail": "-",
-           "worst": 120,
-           "normalized": 120,
-           "value": 120,
-           "threshold": 0,
-           "name": "ECC_Uncorr_Error_Count",
-           "raw": "0/0",
-           "flags": "--SRC-"
-       },
-       {
-           "fail": "-",
-           "id": "196",
-           "raw": "0",
-           "flags": "PO--CK",
-           "threshold": 3,
-           "name": "Reallocated_Event_Count",
-           "normalized": 100,
-           "value": 100,
-           "worst": 100
-       },
-       {
-           "normalized": 120,
-           "value": 120,
-           "worst": 120,
-           "threshold": 0,
-           "name": "Unc_Soft_Read_Err_Rate",
-           "flags": "--SRC-",
-           "raw": "0/0",
-           "id": "201",
-           "fail": "-"
-       },
-       {
-           "raw": "0/0",
-           "flags": "--SRC-",
-           "normalized": 120,
-           "value": 120,
-           "worst": 120,
-           "threshold": 0,
-           "name": "Soft_ECC_Correct_Rate",
-           "fail": "-",
-           "id": "204"
-       },
-       {
-           "normalized": 100,
-           "value": 100,
-           "worst": 100,
-           "threshold": 0,
-           "name": "Life_Curve_Status",
-           "raw": "100",
-           "flags": "PO--C-",
-           "id": "230",
-           "fail": "-"
-       },
-       {
-           "id": "231",
-           "fail": "-",
-           "worst": 91,
-           "normalized": 91,
-           "value": 91,
-           "name": "SSD_Life_Left",
-           "threshold": 11,
-           "flags": "------",
-           "raw": "4294967296"
-       },
-       {
-           "raw": "6317",
-           "flags": "-O--CK",
-           "name": "SandForce_Internal",
-           "threshold": 0,
-           "normalized": 0,
-           "value": 0,
-           "worst": 0,
-           "fail": "-",
-           "id": "233"
-       },
-       {
-           "normalized": 0,
-           "value": 0,
-           "worst": 0,
-           "name": "SandForce_Internal",
-           "threshold": 0,
-           "flags": "-O--CK",
-           "raw": "4252",
-           "id": "234",
-           "fail": "-"
-       },
-       {
-           "worst": 0,
-           "normalized": 0,
-           "value": 0,
-           "name": "Lifetime_Writes_GiB",
-           "threshold": 0,
-           "flags": "-O--CK",
-           "raw": "4252",
-           "id": "241",
-           "fail": "-"
-       },
-       {
-           "flags": "-O--CK",
-           "raw": "34599",
-           "normalized": 0,
-           "value": 0,
-           "worst": 0,
-           "threshold": 0,
-           "name": "Lifetime_Reads_GiB",
-           "fail": "-",
-           "id": "242"
-       },
-       {
-           "threshold": 10,
-           "name": "Unknown_Attribute",
-           "worst": 99,
-           "normalized": 99,
-           "value": 99,
-           "flags": "------",
-           "raw": "4063273",
-           "id": "244",
-           "fail": "-"
-       }
-    ]
-}
diff --git a/test/disk_tests/ssd_smart/sde_udevadm b/test/disk_tests/ssd_smart/sde_udevadm
deleted file mode 100644 (file)
index 440c33a..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-E: DEVNAME=/dev/sde
-E: DEVTYPE=disk
-E: ID_ATA_ROTATION_RATE_RPM=0
-E: ID_BUS=ata
-E: ID_MODEL=KINGSTON SHFS37A120G
-E: ID_PART_TABLE_TYPE=gpt
-E: ID_SERIAL=KINGSTON_00000000000
-E: ID_SERIAL_SHORT=000000000000
-E: ID_TYPE=disk
-E: ID_WWN=0x0000000000000000
-
diff --git a/test/disk_tests/usages/disklist b/test/disk_tests/usages/disklist
deleted file mode 100644 (file)
index 648bea5..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-sda
-sdb
-sdc
-sdd
-sde
-sdf
-sdg
-sdh
-sdi
-sdj
-sdk
-sdl
-sdm
-sdn
diff --git a/test/disk_tests/usages/disklist_expected.json b/test/disk_tests/usages/disklist_expected.json
deleted file mode 100644 (file)
index e9c5e5d..0000000
+++ /dev/null
@@ -1,223 +0,0 @@
-{
-    "sdf" : {
-       "gpt" : 1,
-       "rpm" : 0,
-       "size" : 1536000,
-       "type" : "hdd",
-       "osdencrypted": 0,
-       "osdid" : "444",
-       "bluestore" : "0",
-       "health" : "UNKNOWN",
-       "model" : "MODEL1",
-       "used" : "mounted",
-       "wearout" : "N/A",
-       "wwn" : "0x0000000000000000",
-       "devpath" : "/dev/sdf",
-       "vendor" : "ATA",
-       "serial" : "SERIAL1"
-    },
-    "sde" : {
-       "wwn" : "0x0000000000000000",
-       "devpath" : "/dev/sde",
-       "serial" : "SERIAL1",
-       "vendor" : "ATA",
-       "health" : "UNKNOWN",
-       "rpm" : 0,
-       "size" : 1536000,
-       "gpt" : 1,
-       "osdid" : -1,
-       "type" : "hdd",
-       "model" : "MODEL1",
-       "used" : "Device Mapper",
-       "wearout" : "N/A"
-    },
-    "sdb" : {
-       "serial" : "SERIAL1",
-       "vendor" : "ATA",
-       "wwn" : "0x0000000000000000",
-       "devpath" : "/dev/sdb",
-       "model" : "MODEL1",
-       "used" : "LVM",
-       "wearout" : "N/A",
-       "health" : "UNKNOWN",
-       "gpt" : 1,
-       "size" : 1536000,
-       "rpm" : 0,
-       "type" : "hdd",
-       "osdid" : -1
-    },
-    "sda" : {
-       "model" : "MODEL1",
-       "used" : "mounted",
-       "mounted": 1,
-       "wearout" : "N/A",
-       "health" : "UNKNOWN",
-       "gpt" : 1,
-       "size" : 1536000,
-       "rpm" : 0,
-       "type" : "hdd",
-       "osdid" : -1,
-       "serial" : "SERIAL1",
-       "vendor" : "ATA",
-       "wwn" : "0x0000000000000000",
-       "devpath" : "/dev/sda"
-    },
-    "sdc" : {
-       "serial" : "SERIAL1",
-       "vendor" : "ATA",
-       "devpath" : "/dev/sdc",
-       "wwn" : "0x0000000000000000",
-       "used" : "ZFS",
-       "wearout" : "N/A",
-       "model" : "MODEL1",
-       "health" : "UNKNOWN",
-       "type" : "hdd",
-       "osdid" : -1,
-       "gpt" : 1,
-       "rpm" : 0,
-       "size" : 1536000
-    },
-    "sdd" : {
-       "model" : "MODEL1",
-       "wearout" : "N/A",
-       "health" : "UNKNOWN",
-       "size" : 1536000,
-       "rpm" : 0,
-       "gpt" : 1,
-       "type" : "hdd",
-       "osdid" : -1,
-       "serial" : "SERIAL1",
-       "used": "ZFS",
-       "vendor" : "ATA",
-       "wwn" : "0x0000000000000000",
-       "devpath" : "/dev/sdd"
-    },
-    "sdg" : {
-       "serial" : "SERIAL1",
-       "vendor" : "ATA",
-       "wwn" : "0x0000000000000000",
-       "devpath" : "/dev/sdg",
-       "model" : "MODEL1",
-       "used" : "LVM",
-       "wearout" : "N/A",
-       "health" : "UNKNOWN",
-       "gpt" : 1,
-       "size" : 1536000,
-       "rpm" : 0,
-       "type" : "hdd",
-       "bluestore": 1,
-       "osdencrypted": 0,
-       "osdid" : 1
-    },
-    "sdh" : {
-       "serial" : "SERIAL1",
-       "vendor" : "ATA",
-       "wwn" : "0x0000000000000000",
-       "devpath" : "/dev/sdh",
-       "model" : "MODEL1",
-       "used" : "LVM",
-       "wearout" : "N/A",
-       "health" : "UNKNOWN",
-       "gpt" : 1,
-       "journals": 1,
-       "size" : 1536000,
-       "rpm" : 0,
-       "type" : "hdd",
-       "osdid" : -1
-    },
-    "sdi" : {
-       "serial" : "SERIAL1",
-       "vendor" : "ATA",
-       "wwn" : "0x0000000000000000",
-       "devpath" : "/dev/sdi",
-       "model" : "MODEL1",
-       "used" : "LVM",
-       "wearout" : "N/A",
-       "health" : "UNKNOWN",
-       "gpt" : 1,
-       "size" : 1536000,
-       "rpm" : 0,
-       "type" : "hdd",
-       "db": 1,
-       "osdid" : -1
-    },
-    "sdj" : {
-       "serial" : "SERIAL1",
-       "vendor" : "ATA",
-       "wwn" : "0x0000000000000000",
-       "devpath" : "/dev/sdj",
-       "model" : "MODEL1",
-       "used" : "LVM",
-       "wearout" : "N/A",
-       "health" : "UNKNOWN",
-       "gpt" : 1,
-       "size" : 1536000,
-       "rpm" : 0,
-       "bluestore": 0,
-       "type" : "hdd",
-       "osdencrypted": 1,
-       "osdid" : 0
-    },
-    "sdk" : {
-       "serial" : "SERIAL1",
-       "vendor" : "ATA",
-       "wwn" : "0x0000000000000000",
-       "devpath" : "/dev/sdk",
-       "model" : "MODEL1",
-       "used" : "LVM",
-       "wearout" : "N/A",
-       "health" : "UNKNOWN",
-       "gpt" : 1,
-       "size" : 1536000,
-       "rpm" : 0,
-       "bluestore": 0,
-       "type" : "hdd",
-       "osdencrypted": 0,
-       "osdid" : 230
-    },
-    "sdl" : {
-       "serial" : "SERIAL1",
-       "vendor" : "ATA",
-       "wwn" : "0x0000000000000000",
-       "devpath" : "/dev/sdl",
-       "model" : "MODEL1",
-       "used" : "LVM",
-       "wearout" : "N/A",
-       "health" : "UNKNOWN",
-       "gpt" : 1,
-       "size" : 1536000,
-       "rpm" : 0,
-       "type" : "hdd",
-       "osdid" : -1
-    },
-    "sdm" : {
-       "serial" : "SERIAL1",
-       "vendor" : "ATA",
-       "wwn" : "0x0000000000000000",
-       "devpath" : "/dev/sdm",
-       "model" : "MODEL1",
-       "used" : "ZFS",
-       "wearout" : "N/A",
-       "health" : "UNKNOWN",
-       "gpt" : 1,
-       "size" : 1536000,
-       "rpm" : 0,
-       "type" : "hdd",
-       "osdid" : -1
-    },
-    "sdn" : {
-       "serial" : "SERIAL1",
-       "vendor" : "ATA",
-       "wwn" : "0x0000000000000000",
-       "devpath" : "/dev/sdn",
-       "model" : "MODEL1",
-       "used" : "xfs",
-       "wearout" : "N/A",
-       "health" : "UNKNOWN",
-       "gpt" : 0,
-       "size" : 1536000,
-       "rpm" : 0,
-       "type" : "hdd",
-       "osdid" : -1
-    }
-}
diff --git a/test/disk_tests/usages/lsblk b/test/disk_tests/usages/lsblk
deleted file mode 100644 (file)
index 6a725ab..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-{
-   "blockdevices": [
-      {"path":"/dev/sdm", "parttype":null, "fstype":null},
-      {"path":"/dev/sdm1", "parttype":"6a898cc3-1dd2-11b2-99a6-080020736631", "fstype":"zfs_member"},
-      {"path":"/dev/sdm9", "parttype":"6a945a3b-1dd2-11b2-99a6-080020736631", "fstype":null},
-      {"path":"/dev/sdn", "parttype":null, "fstype":"xfs"}
-   ]
-}
diff --git a/test/disk_tests/usages/lvs b/test/disk_tests/usages/lvs
deleted file mode 100644 (file)
index 3720a80..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-/dev/sdg(0);osd-block-01234;ceph.osd_id=1
-/dev/sdh(0);osd-journal-01234;ceph.osd_id=1
-/dev/sdi(0);osd-db-01234;ceph.osd_id=1,dasdf
-/dev/sdj(0);osd-data-01234;ceph.osd_id=0,asfd,ceph.encrypted=1
-/dev/sdk(0);osd-data-231231;ceph.osd_id=230,ceph.fsid=test
-/dev/sdl(0);osd-data-234132;ceph.osd_id=,bar
diff --git a/test/disk_tests/usages/mounts b/test/disk_tests/usages/mounts
deleted file mode 100644 (file)
index f8c1cd3..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-/dev/sda /mountpoint1
-/dev/sdf1 /var/lib/ceph/osd/ceph-444
diff --git a/test/disk_tests/usages/partlist b/test/disk_tests/usages/partlist
deleted file mode 100644 (file)
index 43c1f68..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-sdd1
-sdd2
-sde1
-sdf1
-sdm1
-sdm9
diff --git a/test/disk_tests/usages/pvs b/test/disk_tests/usages/pvs
deleted file mode 100644 (file)
index 86ec3d4..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-  /dev/sdb
-  /dev/sdg
-  /dev/sdh
-  /dev/sdi
-  /dev/sdj
-  /dev/sdk
-  /dev/sdl
diff --git a/test/disk_tests/usages/sda/device/vendor b/test/disk_tests/usages/sda/device/vendor
deleted file mode 100644 (file)
index 531030d..0000000
+++ /dev/null
@@ -1 +0,0 @@
-ATA
diff --git a/test/disk_tests/usages/sda/queue/rotational b/test/disk_tests/usages/sda/queue/rotational
deleted file mode 100644 (file)
index d00491f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-1
diff --git a/test/disk_tests/usages/sda/size b/test/disk_tests/usages/sda/size
deleted file mode 100644 (file)
index 13de30f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-3000
diff --git a/test/disk_tests/usages/sda_udevadm b/test/disk_tests/usages/sda_udevadm
deleted file mode 100644 (file)
index ab04390..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-E: DEVNAME=/dev/sda
-E: DEVTYPE=disk
-E: ID_ATA_ROTATION_RATE_RPM=0
-E: ID_BUS=ata
-E: ID_MODEL=MODEL1
-E: ID_PART_TABLE_TYPE=gpt
-E: ID_PART_TABLE_UUID=8417b93f-eff9-4e8f-8d84-dc2e77fc07a2
-E: ID_SERIAL=SERIAL1
-E: ID_SERIAL_SHORT=SERIAL1
-E: ID_TYPE=disk
-E: ID_WWN=0x0000000000000000
-E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/test/disk_tests/usages/sdb/device/vendor b/test/disk_tests/usages/sdb/device/vendor
deleted file mode 100644 (file)
index 531030d..0000000
+++ /dev/null
@@ -1 +0,0 @@
-ATA
diff --git a/test/disk_tests/usages/sdb/queue/rotational b/test/disk_tests/usages/sdb/queue/rotational
deleted file mode 100644 (file)
index d00491f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-1
diff --git a/test/disk_tests/usages/sdb/size b/test/disk_tests/usages/sdb/size
deleted file mode 100644 (file)
index 13de30f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-3000
diff --git a/test/disk_tests/usages/sdb_udevadm b/test/disk_tests/usages/sdb_udevadm
deleted file mode 100644 (file)
index ada1ca8..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-E: DEVNAME=/dev/sdb
-E: DEVTYPE=disk
-E: ID_ATA_ROTATION_RATE_RPM=0
-E: ID_BUS=ata
-E: ID_MODEL=MODEL1
-E: ID_PART_TABLE_TYPE=gpt
-E: ID_PART_TABLE_UUID=8417b93f-eff9-4e8f-8d84-dc2e77fc07a2
-E: ID_SERIAL=SERIAL1
-E: ID_SERIAL_SHORT=SERIAL1
-E: ID_TYPE=disk
-E: ID_WWN=0x0000000000000000
-E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/test/disk_tests/usages/sdc/device/vendor b/test/disk_tests/usages/sdc/device/vendor
deleted file mode 100644 (file)
index 531030d..0000000
+++ /dev/null
@@ -1 +0,0 @@
-ATA
diff --git a/test/disk_tests/usages/sdc/queue/rotational b/test/disk_tests/usages/sdc/queue/rotational
deleted file mode 100644 (file)
index d00491f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-1
diff --git a/test/disk_tests/usages/sdc/size b/test/disk_tests/usages/sdc/size
deleted file mode 100644 (file)
index 13de30f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-3000
diff --git a/test/disk_tests/usages/sdc_udevadm b/test/disk_tests/usages/sdc_udevadm
deleted file mode 100644 (file)
index 42845f3..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-E: DEVNAME=/dev/sdc
-E: DEVTYPE=disk
-E: ID_ATA_ROTATION_RATE_RPM=0
-E: ID_BUS=ata
-E: ID_MODEL=MODEL1
-E: ID_PART_TABLE_TYPE=gpt
-E: ID_PART_TABLE_UUID=8417b93f-eff9-4e8f-8d84-dc2e77fc07a2
-E: ID_SERIAL=SERIAL1
-E: ID_SERIAL_SHORT=SERIAL1
-E: ID_TYPE=disk
-E: ID_WWN=0x0000000000000000
-E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/test/disk_tests/usages/sdd/device/vendor b/test/disk_tests/usages/sdd/device/vendor
deleted file mode 100644 (file)
index 531030d..0000000
+++ /dev/null
@@ -1 +0,0 @@
-ATA
diff --git a/test/disk_tests/usages/sdd/queue/rotational b/test/disk_tests/usages/sdd/queue/rotational
deleted file mode 100644 (file)
index d00491f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-1
diff --git a/test/disk_tests/usages/sdd/sdd1/size b/test/disk_tests/usages/sdd/sdd1/size
deleted file mode 100644 (file)
index 83b33d2..0000000
+++ /dev/null
@@ -1 +0,0 @@
-1000
diff --git a/test/disk_tests/usages/sdd/sdd2/size b/test/disk_tests/usages/sdd/sdd2/size
deleted file mode 100644 (file)
index 8bd1af1..0000000
+++ /dev/null
@@ -1 +0,0 @@
-2000
diff --git a/test/disk_tests/usages/sdd/size b/test/disk_tests/usages/sdd/size
deleted file mode 100644 (file)
index 13de30f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-3000
diff --git a/test/disk_tests/usages/sdd_udevadm b/test/disk_tests/usages/sdd_udevadm
deleted file mode 100644 (file)
index 65e880f..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-E: DEVNAME=/dev/sdd
-E: DEVTYPE=disk
-E: ID_ATA_ROTATION_RATE_RPM=0
-E: ID_BUS=ata
-E: ID_MODEL=MODEL1
-E: ID_PART_TABLE_TYPE=gpt
-E: ID_PART_TABLE_UUID=8417b93f-eff9-4e8f-8d84-dc2e77fc07a2
-E: ID_SERIAL=SERIAL1
-E: ID_SERIAL_SHORT=SERIAL1
-E: ID_TYPE=disk
-E: ID_WWN=0x0000000000000000
-E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/test/disk_tests/usages/sde/device/vendor b/test/disk_tests/usages/sde/device/vendor
deleted file mode 100644 (file)
index 531030d..0000000
+++ /dev/null
@@ -1 +0,0 @@
-ATA
diff --git a/test/disk_tests/usages/sde/queue/rotational b/test/disk_tests/usages/sde/queue/rotational
deleted file mode 100644 (file)
index d00491f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-1
diff --git a/test/disk_tests/usages/sde/sde1/size b/test/disk_tests/usages/sde/sde1/size
deleted file mode 100644 (file)
index 13de30f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-3000
diff --git a/test/disk_tests/usages/sde/size b/test/disk_tests/usages/sde/size
deleted file mode 100644 (file)
index 13de30f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-3000
diff --git a/test/disk_tests/usages/sde_udevadm b/test/disk_tests/usages/sde_udevadm
deleted file mode 100644 (file)
index db77725..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-E: DEVNAME=/dev/sde
-E: DEVTYPE=disk
-E: ID_ATA_ROTATION_RATE_RPM=0
-E: ID_BUS=ata
-E: ID_MODEL=MODEL1
-E: ID_PART_TABLE_TYPE=gpt
-E: ID_PART_TABLE_UUID=8417b93f-eff9-4e8f-8d84-dc2e77fc07a2
-E: ID_SERIAL=SERIAL1
-E: ID_SERIAL_SHORT=SERIAL1
-E: ID_TYPE=disk
-E: ID_WWN=0x0000000000000000
-E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/test/disk_tests/usages/sdf/device/vendor b/test/disk_tests/usages/sdf/device/vendor
deleted file mode 100644 (file)
index 531030d..0000000
+++ /dev/null
@@ -1 +0,0 @@
-ATA
diff --git a/test/disk_tests/usages/sdf/queue/rotational b/test/disk_tests/usages/sdf/queue/rotational
deleted file mode 100644 (file)
index d00491f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-1
diff --git a/test/disk_tests/usages/sdf/sdf1/size b/test/disk_tests/usages/sdf/sdf1/size
deleted file mode 100644 (file)
index 13de30f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-3000
diff --git a/test/disk_tests/usages/sdf/size b/test/disk_tests/usages/sdf/size
deleted file mode 100644 (file)
index 13de30f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-3000
diff --git a/test/disk_tests/usages/sdf_udevadm b/test/disk_tests/usages/sdf_udevadm
deleted file mode 100644 (file)
index ad49acf..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-E: DEVNAME=/dev/sdf
-E: DEVTYPE=disk
-E: ID_ATA_ROTATION_RATE_RPM=0
-E: ID_BUS=ata
-E: ID_MODEL=MODEL1
-E: ID_PART_TABLE_TYPE=gpt
-E: ID_PART_TABLE_UUID=8417b93f-eff9-4e8f-8d84-dc2e77fc07a2
-E: ID_SERIAL=SERIAL1
-E: ID_SERIAL_SHORT=SERIAL1
-E: ID_TYPE=disk
-E: ID_WWN=0x0000000000000000
-E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/test/disk_tests/usages/sdg/device/vendor b/test/disk_tests/usages/sdg/device/vendor
deleted file mode 100644 (file)
index 531030d..0000000
+++ /dev/null
@@ -1 +0,0 @@
-ATA
diff --git a/test/disk_tests/usages/sdg/queue/rotational b/test/disk_tests/usages/sdg/queue/rotational
deleted file mode 100644 (file)
index d00491f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-1
diff --git a/test/disk_tests/usages/sdg/size b/test/disk_tests/usages/sdg/size
deleted file mode 100644 (file)
index 13de30f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-3000
diff --git a/test/disk_tests/usages/sdg_udevadm b/test/disk_tests/usages/sdg_udevadm
deleted file mode 100644 (file)
index 6d40d35..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-E: DEVNAME=/dev/sdg
-E: DEVTYPE=disk
-E: ID_ATA_ROTATION_RATE_RPM=0
-E: ID_BUS=ata
-E: ID_MODEL=MODEL1
-E: ID_PART_TABLE_TYPE=gpt
-E: ID_PART_TABLE_UUID=8417b93f-eff9-4e8f-8d84-dc2e77fc07a2
-E: ID_SERIAL=SERIAL1
-E: ID_SERIAL_SHORT=SERIAL1
-E: ID_TYPE=disk
-E: ID_WWN=0x0000000000000000
-E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/test/disk_tests/usages/sdh/device/vendor b/test/disk_tests/usages/sdh/device/vendor
deleted file mode 100644 (file)
index 531030d..0000000
+++ /dev/null
@@ -1 +0,0 @@
-ATA
diff --git a/test/disk_tests/usages/sdh/queue/rotational b/test/disk_tests/usages/sdh/queue/rotational
deleted file mode 100644 (file)
index d00491f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-1
diff --git a/test/disk_tests/usages/sdh/size b/test/disk_tests/usages/sdh/size
deleted file mode 100644 (file)
index 13de30f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-3000
diff --git a/test/disk_tests/usages/sdh_udevadm b/test/disk_tests/usages/sdh_udevadm
deleted file mode 100644 (file)
index 3ff1a9e..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-E: DEVNAME=/dev/sdh
-E: DEVTYPE=disk
-E: ID_ATA_ROTATION_RATE_RPM=0
-E: ID_BUS=ata
-E: ID_MODEL=MODEL1
-E: ID_PART_TABLE_TYPE=gpt
-E: ID_PART_TABLE_UUID=8417b93f-eff9-4e8f-8d84-dc2e77fc07a2
-E: ID_SERIAL=SERIAL1
-E: ID_SERIAL_SHORT=SERIAL1
-E: ID_TYPE=disk
-E: ID_WWN=0x0000000000000000
-E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/test/disk_tests/usages/sdi/device/vendor b/test/disk_tests/usages/sdi/device/vendor
deleted file mode 100644 (file)
index 531030d..0000000
+++ /dev/null
@@ -1 +0,0 @@
-ATA
diff --git a/test/disk_tests/usages/sdi/queue/rotational b/test/disk_tests/usages/sdi/queue/rotational
deleted file mode 100644 (file)
index d00491f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-1
diff --git a/test/disk_tests/usages/sdi/size b/test/disk_tests/usages/sdi/size
deleted file mode 100644 (file)
index 13de30f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-3000
diff --git a/test/disk_tests/usages/sdi_udevadm b/test/disk_tests/usages/sdi_udevadm
deleted file mode 100644 (file)
index a9eae5e..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-E: DEVNAME=/dev/sdi
-E: DEVTYPE=disk
-E: ID_ATA_ROTATION_RATE_RPM=0
-E: ID_BUS=ata
-E: ID_MODEL=MODEL1
-E: ID_PART_TABLE_TYPE=gpt
-E: ID_PART_TABLE_UUID=8417b93f-eff9-4e8f-8d84-dc2e77fc07a2
-E: ID_SERIAL=SERIAL1
-E: ID_SERIAL_SHORT=SERIAL1
-E: ID_TYPE=disk
-E: ID_WWN=0x0000000000000000
-E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/test/disk_tests/usages/sdj/device/vendor b/test/disk_tests/usages/sdj/device/vendor
deleted file mode 100644 (file)
index 531030d..0000000
+++ /dev/null
@@ -1 +0,0 @@
-ATA
diff --git a/test/disk_tests/usages/sdj/queue/rotational b/test/disk_tests/usages/sdj/queue/rotational
deleted file mode 100644 (file)
index d00491f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-1
diff --git a/test/disk_tests/usages/sdj/size b/test/disk_tests/usages/sdj/size
deleted file mode 100644 (file)
index 13de30f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-3000
diff --git a/test/disk_tests/usages/sdj_udevadm b/test/disk_tests/usages/sdj_udevadm
deleted file mode 100644 (file)
index 39d0cf3..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-E: DEVNAME=/dev/sdj
-E: DEVTYPE=disk
-E: ID_ATA_ROTATION_RATE_RPM=0
-E: ID_BUS=ata
-E: ID_MODEL=MODEL1
-E: ID_PART_TABLE_TYPE=gpt
-E: ID_PART_TABLE_UUID=8417b93f-eff9-4e8f-8d84-dc2e77fc07a2
-E: ID_SERIAL=SERIAL1
-E: ID_SERIAL_SHORT=SERIAL1
-E: ID_TYPE=disk
-E: ID_WWN=0x0000000000000000
-E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/test/disk_tests/usages/sdk/device/vendor b/test/disk_tests/usages/sdk/device/vendor
deleted file mode 100644 (file)
index 531030d..0000000
+++ /dev/null
@@ -1 +0,0 @@
-ATA
diff --git a/test/disk_tests/usages/sdk/queue/rotational b/test/disk_tests/usages/sdk/queue/rotational
deleted file mode 100644 (file)
index d00491f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-1
diff --git a/test/disk_tests/usages/sdk/size b/test/disk_tests/usages/sdk/size
deleted file mode 100644 (file)
index 13de30f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-3000
diff --git a/test/disk_tests/usages/sdk_udevadm b/test/disk_tests/usages/sdk_udevadm
deleted file mode 100644 (file)
index 3baef2f..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-E: DEVNAME=/dev/sdk
-E: DEVTYPE=disk
-E: ID_ATA_ROTATION_RATE_RPM=0
-E: ID_BUS=ata
-E: ID_MODEL=MODEL1
-E: ID_PART_TABLE_TYPE=gpt
-E: ID_PART_TABLE_UUID=8417b93f-eff9-4e8f-8d84-dc2e77fc07a2
-E: ID_SERIAL=SERIAL1
-E: ID_SERIAL_SHORT=SERIAL1
-E: ID_TYPE=disk
-E: ID_WWN=0x0000000000000000
-E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/test/disk_tests/usages/sdl/device/vendor b/test/disk_tests/usages/sdl/device/vendor
deleted file mode 100644 (file)
index 531030d..0000000
+++ /dev/null
@@ -1 +0,0 @@
-ATA
diff --git a/test/disk_tests/usages/sdl/queue/rotational b/test/disk_tests/usages/sdl/queue/rotational
deleted file mode 100644 (file)
index d00491f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-1
diff --git a/test/disk_tests/usages/sdl/size b/test/disk_tests/usages/sdl/size
deleted file mode 100644 (file)
index 13de30f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-3000
diff --git a/test/disk_tests/usages/sdl_udevadm b/test/disk_tests/usages/sdl_udevadm
deleted file mode 100644 (file)
index ead0622..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-E: DEVNAME=/dev/sdl
-E: DEVTYPE=disk
-E: ID_ATA_ROTATION_RATE_RPM=0
-E: ID_BUS=ata
-E: ID_MODEL=MODEL1
-E: ID_PART_TABLE_TYPE=gpt
-E: ID_PART_TABLE_UUID=8417b93f-eff9-4e8f-8d84-dc2e77fc07a2
-E: ID_SERIAL=SERIAL1
-E: ID_SERIAL_SHORT=SERIAL1
-E: ID_TYPE=disk
-E: ID_WWN=0x0000000000000000
-E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/test/disk_tests/usages/sdm/device/vendor b/test/disk_tests/usages/sdm/device/vendor
deleted file mode 100644 (file)
index 531030d..0000000
+++ /dev/null
@@ -1 +0,0 @@
-ATA
diff --git a/test/disk_tests/usages/sdm/queue/rotational b/test/disk_tests/usages/sdm/queue/rotational
deleted file mode 100644 (file)
index d00491f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-1
diff --git a/test/disk_tests/usages/sdm/sdm1/size b/test/disk_tests/usages/sdm/sdm1/size
deleted file mode 100644 (file)
index 83b33d2..0000000
+++ /dev/null
@@ -1 +0,0 @@
-1000
diff --git a/test/disk_tests/usages/sdm/sdm9/size b/test/disk_tests/usages/sdm/sdm9/size
deleted file mode 100644 (file)
index 8bd1af1..0000000
+++ /dev/null
@@ -1 +0,0 @@
-2000
diff --git a/test/disk_tests/usages/sdm/size b/test/disk_tests/usages/sdm/size
deleted file mode 100644 (file)
index 13de30f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-3000
diff --git a/test/disk_tests/usages/sdm_udevadm b/test/disk_tests/usages/sdm_udevadm
deleted file mode 100644 (file)
index 9317b9f..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-E: DEVNAME=/dev/sdm
-E: DEVTYPE=disk
-E: ID_ATA_ROTATION_RATE_RPM=0
-E: ID_BUS=ata
-E: ID_MODEL=MODEL1
-E: ID_PART_TABLE_TYPE=gpt
-E: ID_PART_TABLE_UUID=8417b93f-eff9-4e8f-8d84-dc2e77fc07a2
-E: ID_SERIAL=SERIAL1
-E: ID_SERIAL_SHORT=SERIAL1
-E: ID_TYPE=disk
-E: ID_WWN=0x0000000000000000
-E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/test/disk_tests/usages/sdn/device/vendor b/test/disk_tests/usages/sdn/device/vendor
deleted file mode 100644 (file)
index 531030d..0000000
+++ /dev/null
@@ -1 +0,0 @@
-ATA
diff --git a/test/disk_tests/usages/sdn/queue/rotational b/test/disk_tests/usages/sdn/queue/rotational
deleted file mode 100644 (file)
index d00491f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-1
diff --git a/test/disk_tests/usages/sdn/size b/test/disk_tests/usages/sdn/size
deleted file mode 100644 (file)
index 13de30f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-3000
diff --git a/test/disk_tests/usages/sdn_udevadm b/test/disk_tests/usages/sdn_udevadm
deleted file mode 100644 (file)
index 5ec4a92..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-E: DEVNAME=/dev/sdn
-E: DEVTYPE=disk
-E: ID_ATA_ROTATION_RATE_RPM=0
-E: ID_BUS=ata
-E: ID_MODEL=MODEL1
-E: ID_SERIAL=SERIAL1
-E: ID_SERIAL_SHORT=SERIAL1
-E: ID_TYPE=disk
-E: ID_FS_UUID=ab54fba8-48fe-4d37-bbe7-b403f94d3bed
-E: ID_FS_UUID_ENC=ab54fba8-48fe-4d37-bbe7-b403f94d3bed
-E: ID_FS_TYPE=xfs
-E: ID_FS_USAGE=filesystem
-E: ID_WWN=0x0000000000000000
-E: ID_WWN_WITH_EXTENSION=0x0000000000000000
diff --git a/test/disk_tests/usages/zpool b/test/disk_tests/usages/zpool
deleted file mode 100644 (file)
index 3431792..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-testpool       2.00T   1.00T   2.60T   -       5%      28%     1.00x   ONLINE  -
-       /dev/sdc        3.62T   1.02T   2.60T   -       5%      28%
-log      -      -      -         -      -      -
-       /dev/sdd1       15.9G   2.79M   15.9G   -       82%     0%
-cache      -      -      -         -      -      -
-       /dev/sdd2       42.5G   36.2G   6.36G   -       0%      85%
diff --git a/test/disklist_test.pm b/test/disklist_test.pm
deleted file mode 100644 (file)
index 97f11fc..0000000
+++ /dev/null
@@ -1,265 +0,0 @@
-package PVE::Diskmanage::Test;
-
-use strict;
-use warnings;
-
-use lib qw(..);
-
-use PVE::Diskmanage;
-use PVE::Tools;
-
-use Test::MockModule;
-use Test::More;
-use JSON;
-use Data::Dumper;
-
-my $testcasedir; # current case directory
-my $testcount = 0; # testcount for TAP::Harness
-my $diskmanage_module; # mockmodule for PVE::Diskmanage
-my $print = 0;
-
-sub mocked_run_command {
-    my ($cmd, %param) = @_;
-
-    my $outputlines = [];
-    if (my $ref = ref($cmd)) {
-       if ($cmd->[0] =~ m/udevadm/i) {
-           # simulate udevadm output
-           my $dev = $cmd->[3];
-           $dev =~ s|/sys/block/||;
-           @$outputlines = split(/\n/, read_test_file("${dev}_udevadm"));
-
-       } elsif ($cmd->[0] =~ m/smartctl/i) {
-           # simulate smartctl output
-           my $dev;
-           my $type;
-           if (@$cmd > 3) {
-               $dev = $cmd->[5];
-               $type = 'smart';
-           } else {
-               $dev = $cmd->[2];
-               $type = 'health';
-           }
-           $dev =~ s|/dev/||;
-           @$outputlines = split(/\n/, read_test_file("${dev}_${type}"));
-       } elsif ($cmd->[0] =~ m/sgdisk/i) {
-           # simulate sgdisk
-           die "implement me: @$cmd\n";
-       } elsif ($cmd->[0] =~ m/zpool/i) {
-           # simulate zpool output
-           @$outputlines = split(/\n/, read_test_file('zpool'));
-
-       } elsif ($cmd->[0] =~ m/pvs/i) {
-           # simulate lvs output
-           @$outputlines = split(/\n/, read_test_file('pvs'));
-       } elsif ($cmd->[0] =~ m/lvs/i) {
-           @$outputlines = split(/\n/, read_test_file('lvs'));
-       } elsif ($cmd->[0] =~ m/lsblk/i) {
-           my $content = read_test_file('lsblk');
-           if ($content eq '') {
-               $content = '{}';
-           }
-           @$outputlines = split(/\n/, $content);
-       } else {
-           die "unexpected run_command call: '@$cmd', aborting\n";
-       }
-    } else {
-       print "unexpected run_command call: '@$cmd', aborting\n";
-       die;
-    }
-
-    my $outfunc;
-    if ($param{outfunc}) {
-       $outfunc = $param{outfunc};
-       map { &$outfunc(($_)) } @$outputlines;
-
-       return 0;
-    }
-}
-
-sub mocked_get_sysdir_info {
-    my ($param) = @_;
-
-    my $originalsub = $diskmanage_module->original('get_sysdir_info');
-
-    $param =~ s|/sys/block|disk_tests/$testcasedir|;
-
-    return &$originalsub($param);
-}
-
-sub mocked_get_sysdir_size {
-    my ($param) = @_;
-
-    my $originalsub = $diskmanage_module->original('get_sysdir_size');
-
-    $param =~ s|/sys/block|disk_tests/$testcasedir|;
-
-    return &$originalsub($param);
-}
-
-sub mocked_is_iscsi {
-    return 0;
-}
-
-sub mocked_dir_glob_foreach {
-    my ($dir, $regex, $sub) = @_;
-
-    my $lines = [];
-
-    # read lines in from file
-    if ($dir =~ m{^/sys/block$} ) {
-       @$lines = split(/\n/, read_test_file('disklist'));
-    } elsif ($dir =~ m{^/sys/block/([^/]+)}) {
-       @$lines = split(/\n/, read_test_file('partlist'));
-    }
-
-    foreach my $line (@$lines) {
-       if ($line =~ m/$regex/) {
-           &$sub($line);
-       }
-    }
-}
-
-sub mocked_parse_proc_mounts {
-    my $text = read_test_file('mounts');
-
-    my $mounts = [];
-
-    foreach my $line(split(/\n/, $text)) {
-       push @$mounts, [split(/\s+/, $line)];
-    }
-
-    return $mounts;
-}
-
-sub read_test_file {
-    my ($filename) = @_;
-
-    if (!-f  "disk_tests/$testcasedir/$filename") {
-       print "file '$testcasedir/$filename' not found\n";
-       return '';
-    }
-    open (my $fh, '<', "disk_tests/$testcasedir/$filename")
-       or die "Cannot open disk_tests/$testcasedir/$filename: $!";
-
-    my $output = <$fh> // '';
-    chomp $output if $output;
-    while (my $line = <$fh>) {
-       chomp $line;
-       $output .= "\n$line";
-    }
-
-    return $output;
-}
-
-
-sub test_disk_list {
-    my ($testdir) = @_;
-    subtest "Test '$testdir'" => sub {
-       my $testcount = 0;
-       $testcasedir = $testdir;
-
-       my $disks;
-       my $expected_disk_list;
-       eval {
-           $disks = PVE::Diskmanage::get_disks();
-       };
-       warn $@ if $@;
-       $expected_disk_list = decode_json(read_test_file('disklist_expected.json'));
-
-       print Dumper($disks) if $print;
-       $testcount++;
-       is_deeply($disks, $expected_disk_list, 'disk list should be the same');
-
-       foreach my $disk (sort keys %$disks) {
-           my $smart;
-           my $expected_smart;
-           eval {
-               $smart = PVE::Diskmanage::get_smart_data("/dev/$disk");
-               print Dumper($smart) if $print;
-               $expected_smart = decode_json(read_test_file("${disk}_smart_expected.json"));
-           };
-
-           if ($smart && $expected_smart) {
-               $testcount++;
-               is_deeply($smart, $expected_smart, "smart data for '$disk' should be the same");
-           } elsif ($smart && -f  "disk_tests/$testcasedir/${disk}_smart_expected.json") {
-               $testcount++;
-               ok(0,  "could not parse expected smart for '$disk'\n");
-           }
-           my $disk_tmp = {};
-
-           # test single disk parameter
-           $disk_tmp = PVE::Diskmanage::get_disks($disk);
-           warn $@ if $@;
-           $testcount++;
-           print Dumper $disk_tmp if $print;
-           is_deeply($disk_tmp->{$disk}, $expected_disk_list->{$disk}, "disk $disk should be the same");
-
-
-           # test wrong parameter
-           eval {
-               PVE::Diskmanage::get_disks( { test => 1 } );
-           };
-           my $err = $@;
-           $testcount++;
-           is_deeply($err, "disks is not a string or array reference\n", "error message should be the same");
-
-       }
-           # test multi disk parameter
-           $disks = PVE::Diskmanage::get_disks( [ keys %$disks ] );
-           $testcount++;
-           is_deeply($disks, $expected_disk_list, 'disk list should be the same');
-
-       done_testing($testcount);
-    };
-}
-
-# start reading tests:
-
-if (@ARGV && $ARGV[0] eq 'print') {
-    $print = 1;
-}
-
-print("Setting up Mocking\n");
-$diskmanage_module = Test::MockModule->new('PVE::Diskmanage', no_auto => 1);
-$diskmanage_module->mock('run_command' => \&mocked_run_command);
-print("\tMocked run_command\n");
-$diskmanage_module->mock('dir_glob_foreach' => \&mocked_dir_glob_foreach);
-print("\tMocked dir_glob_foreach\n");
-$diskmanage_module->mock('get_sysdir_info' => \&mocked_get_sysdir_info);
-print("\tMocked get_sysdir_info\n");
-$diskmanage_module->mock('get_sysdir_size' => \&mocked_get_sysdir_size);
-print("\tMocked get_sysdir_size\n");
-$diskmanage_module->mock('is_iscsi' => \&mocked_is_iscsi);
-print("\tMocked is_iscsi\n");
-$diskmanage_module->mock('assert_blockdev' => sub { return 1; });
-print("\tMocked assert_blockdev\n");
-$diskmanage_module->mock('dir_is_empty' => sub {
-       # all partitions have a holder dir
-       my $val = shift;
-       if ($val =~ m|^/sys/block/.+/.+/|) {
-           return 0;
-       }
-       return 1;
-    });
-print("\tMocked dir_is_empty\n");
-$diskmanage_module->mock('check_bin' => sub { return 1; });
-print("\tMocked check_bin\n");
-my $tools_module= Test::MockModule->new('PVE::ProcFSTools', no_auto => 1);
-$tools_module->mock('parse_proc_mounts' => \&mocked_parse_proc_mounts);
-print("\tMocked parse_proc_mounts\n");
-print("Done Setting up Mocking\n\n");
-
-print("Beginning Tests:\n\n");
-opendir (my $dh, 'disk_tests')
-    or die "Cannot open disk_tests: $!";
-
-while (readdir $dh) {
-    my $dir = $_;
-    next if $dir eq '.' or $dir eq '..';
-    $testcount++;
-    test_disk_list($dir);
-}
-
-done_testing($testcount);
diff --git a/test/filesystem_path_test.pm b/test/filesystem_path_test.pm
deleted file mode 100644 (file)
index c1b6d90..0000000
+++ /dev/null
@@ -1,91 +0,0 @@
-package PVE::Storage::TestFilesystemPath;
-
-use strict;
-use warnings;
-
-use lib qw(..);
-
-use PVE::Storage;
-use Test::More;
-
-my $path = '/some/path';
-
-# each array entry is a test that consists of the following keys:
-# volname  => image name that is passed to parse_volname
-# snapname => to test the die condition
-# expected => the array of return values; or the die message
-my $tests = [
-    {
-       volname  => '1234/vm-1234-disk-0.raw',
-       snapname => undef,
-       expected => [
-           "$path/images/1234/vm-1234-disk-0.raw",
-           '1234',
-           'images'
-       ],
-    },
-    {
-       volname  => '1234/vm-1234-disk-0.raw',
-       snapname => 'my_snap',
-       expected => "can't snapshot this image format\n"
-    },
-    {
-       volname  => '1234/vm-1234-disk-0.qcow2',
-       snapname => undef,
-       expected => [
-           "$path/images/1234/vm-1234-disk-0.qcow2",
-           '1234',
-           'images'
-       ],
-    },
-    {
-       volname  => '1234/vm-1234-disk-0.qcow2',
-       snapname => 'my_snap',
-       expected => [
-           "$path/images/1234/vm-1234-disk-0.qcow2",
-           '1234',
-           'images'
-       ],
-    },
-    {
-       volname  => 'iso/my-awesome-proxmox.iso',
-       snapname => undef,
-       expected => [
-           "$path/template/iso/my-awesome-proxmox.iso",
-           undef,
-           'iso'
-       ],
-    },
-    {
-       volname  => "backup/vzdump-qemu-1234-2020_03_30-21_12_40.vma",
-       snapname => undef,
-       expected => [
-           "$path/dump/vzdump-qemu-1234-2020_03_30-21_12_40.vma",
-           1234,
-           'backup'
-       ],
-    },
-];
-
-plan tests => scalar @$tests;
-
-foreach my $tt (@$tests) {
-    my $volname = $tt->{volname};
-    my $snapname = $tt->{snapname};
-    my $expected = $tt->{expected};
-    my $scfg = { path => $path };
-    my $got;
-
-    eval {
-       $got = [ PVE::Storage::Plugin->filesystem_path($scfg, $volname, $snapname) ];
-    };
-    $got = $@ if $@;
-
-    is_deeply($got, $expected, "wantarray: filesystem_path for $volname")
-    || diag(explain($got));
-
-}
-
-done_testing();
-
-1;
diff --git a/test/get_subdir_test.pm b/test/get_subdir_test.pm
deleted file mode 100644 (file)
index b9d61d5..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-package PVE::Storage::TestGetSubdir;
-
-use strict;
-use warnings;
-
-use lib qw(..);
-
-use PVE::Storage::Plugin;
-use Test::More;
-
-my $scfg_with_path = { path => '/some/path' };
-my $vtype_subdirs = PVE::Storage::Plugin::get_vtype_subdirs();
-
-# each test is comprised of the following array keys:
-# [0] => storage config; positive with path key
-# [1] => storage type;  see $vtype_subdirs
-# [2] => expected return from get_subdir
-my $tests = [
-    # failed matches
-    [ $scfg_with_path, 'none', "unknown vtype 'none'\n" ],
-    [ {}, 'iso', "storage definition has no path\n" ],
-];
-
-# creates additional positive tests
-foreach my $type (keys %$vtype_subdirs) {
-    my $path = "$scfg_with_path->{path}/$vtype_subdirs->{$type}";
-    push @$tests, [ $scfg_with_path, $type, $path ];
-}
-
-# creates additional tests for overrides
-foreach my $type (keys %$vtype_subdirs) {
-    my $override = "${type}_override";
-    my $scfg_with_override = { path => '/some/path', 'content-dirs' => { $type => $override } };
-    push @$tests, [ $scfg_with_override, $type, "$scfg_with_override->{path}/$scfg_with_override->{'content-dirs'}->{$type}" ];
-}
-
-plan tests => scalar @$tests;
-
-foreach my $tt (@$tests) {
-    my ($scfg, $type, $expected) = @$tt;
-
-    my $got;
-    eval { $got = PVE::Storage::Plugin->get_subdir($scfg, $type) };
-    $got = $@ if $@;
-
-    is ($got, $expected, "get_subdir for $type") || diag(explain($got));
-}
-
-done_testing();
-
-1;
diff --git a/test/list_volumes_test.pm b/test/list_volumes_test.pm
deleted file mode 100644 (file)
index d155cb9..0000000
+++ /dev/null
@@ -1,548 +0,0 @@
-package PVE::Storage::TestListVolumes;
-
-use strict;
-use warnings;
-
-use lib qw(..);
-
-use PVE::Storage;
-use PVE::Cluster;
-use PVE::Tools qw(run_command);
-
-use Test::More;
-use Test::MockModule;
-
-use Cwd;
-use File::Basename;
-use File::Path qw(make_path remove_tree);
-use File::stat qw();
-use File::Temp;
-use Storable qw(dclone);
-
-use constant DEFAULT_SIZE => 131072; # 128 kiB
-use constant DEFAULT_USED => 262144; # 256 kiB
-use constant DEFAULT_CTIME => 1234567890;
-
-# get_vmlist() return values
-my $mocked_vmlist = {
-    'version' => 1,
-    'ids' => {
-       '16110' => {
-           'node'    => 'x42',
-           'type'    => 'qemu',
-           'version' => 4,
-       },
-       '16112' => {
-           'node'    => 'x42',
-           'type'    => 'lxc',
-           'version' => 7,
-       },
-       '16114' => {
-           'node'    => 'x42',
-           'type'    => 'qemu',
-           'version' => 2,
-       },
-       '16113' => {
-           'node'    => 'x42',
-           'type'    => 'qemu',
-           'version' => 5,
-       },
-       '16115' => {
-           'node'    => 'x42',
-           'type'    => 'qemu',
-           'version' => 1,
-       },
-       '9004' => {
-           'node'    => 'x42',
-           'type'    => 'qemu',
-           'version' => 6,
-       }
-    }
-};
-
-my $storage_dir = File::Temp->newdir();
-my $scfg = {
-    'type'     => 'dir',
-    'maxfiles' => 0,
-    'path'     => $storage_dir,
-    'shared'   => 0,
-    'content'  => {
-       'iso'      => 1,
-       'rootdir'  => 1,
-       'vztmpl'   => 1,
-       'images'   => 1,
-       'snippets' => 1,
-       'backup'   => 1,
-    },
-};
-
-# The test cases are comprised of an arry of hashes with the following keys:
-# description => displayed on error by Test::More
-# vmid        => used for image matches by list_volume
-# files       => array of files for qemu-img to create
-# expected    => returned result hash
-#                (content, ctime, format, parent, size, used, vimd, volid)
-my @tests = (
-    {
-       description => 'VMID: 16110, VM, qcow2, backup, snippets',
-       vmid => '16110',
-       files => [
-           "$storage_dir/images/16110/vm-16110-disk-0.qcow2",
-           "$storage_dir/images/16110/vm-16110-disk-1.raw",
-           "$storage_dir/images/16110/vm-16110-disk-2.vmdk",
-           "$storage_dir/dump/vzdump-qemu-16110-2020_03_30-21_11_40.vma.gz",
-           "$storage_dir/dump/vzdump-qemu-16110-2020_03_30-21_12_45.vma.lzo",
-           "$storage_dir/dump/vzdump-qemu-16110-2020_03_30-21_13_55.vma",
-           "$storage_dir/dump/vzdump-qemu-16110-2020_03_30-21_13_55.vma.zst",
-           "$storage_dir/snippets/userconfig.yaml",
-           "$storage_dir/snippets/hookscript.pl",
-       ],
-       expected => [
-           {
-               'content' => 'images',
-               'ctime'   => DEFAULT_CTIME,
-               'format'  => 'qcow2',
-               'parent'  => undef,
-               'size'    => DEFAULT_SIZE,
-               'used'    => DEFAULT_USED,
-               'vmid'    => '16110',
-               'volid'   => 'local:16110/vm-16110-disk-0.qcow2',
-           },
-           {
-               'content' => 'images',
-               'ctime'   => DEFAULT_CTIME,
-               'format'  => 'raw',
-               'parent'  => undef,
-               'size'    => DEFAULT_SIZE,
-               'used'    => DEFAULT_USED,
-               'vmid'    => '16110',
-               'volid'   => 'local:16110/vm-16110-disk-1.raw',
-           },
-           {
-               'content' => 'images',
-               'ctime'   => DEFAULT_CTIME,
-               'format'  => 'vmdk',
-               'parent'  => undef,
-               'size'    => DEFAULT_SIZE,
-               'used'    => DEFAULT_USED,
-               'vmid'    => '16110',
-               'volid'   => 'local:16110/vm-16110-disk-2.vmdk',
-           },
-           {
-               'content' => 'backup',
-               'ctime'   => 1585602700,
-               'format'  => 'vma.gz',
-               'size'    => DEFAULT_SIZE,
-               'subtype' => 'qemu',
-               'vmid'    => '16110',
-               'volid'   => 'local:backup/vzdump-qemu-16110-2020_03_30-21_11_40.vma.gz',
-           },
-           {
-               'content' => 'backup',
-               'ctime'   => 1585602765,
-               'format'  => 'vma.lzo',
-               'size'    => DEFAULT_SIZE,
-               'subtype' => 'qemu',
-               'vmid'    => '16110',
-               'volid'   => 'local:backup/vzdump-qemu-16110-2020_03_30-21_12_45.vma.lzo',
-           },
-           {
-               'content' => 'backup',
-               'ctime'   => 1585602835,
-               'format'  => 'vma',
-               'size'    => DEFAULT_SIZE,
-               'subtype' => 'qemu',
-               'vmid'    => '16110',
-               'volid'   => 'local:backup/vzdump-qemu-16110-2020_03_30-21_13_55.vma',
-           },
-           {
-               'content' => 'backup',
-               'ctime'   => 1585602835,
-               'format'  => 'vma.zst',
-               'size'    => DEFAULT_SIZE,
-               'subtype' => 'qemu',
-               'vmid'    => '16110',
-               'volid'   => 'local:backup/vzdump-qemu-16110-2020_03_30-21_13_55.vma.zst',
-           },
-           {
-               'content' => 'snippets',
-               'ctime'   => DEFAULT_CTIME,
-               'format'  => 'snippet',
-               'size'    => DEFAULT_SIZE,
-               'volid'   => 'local:snippets/hookscript.pl',
-           },
-           {
-               'content' => 'snippets',
-               'ctime'   => DEFAULT_CTIME,
-               'format'  => 'snippet',
-               'size'    => DEFAULT_SIZE,
-               'volid'   => 'local:snippets/userconfig.yaml',
-           },
-       ],
-    },
-    {
-       description => 'VMID: 16112, lxc, raw, backup',
-       vmid => '16112',
-       files => [
-           "$storage_dir/images/16112/vm-16112-disk-0.raw",
-           "$storage_dir/dump/vzdump-lxc-16112-2020_03_30-21_39_30.tar.lzo",
-           "$storage_dir/dump/vzdump-lxc-16112-2020_03_30-21_49_30.tar.gz",
-           "$storage_dir/dump/vzdump-lxc-16112-2020_03_30-21_49_30.tar.zst",
-           "$storage_dir/dump/vzdump-lxc-16112-2020_03_30-21_59_30.tgz",
-       ],
-       expected => [
-           {
-               'content' => 'rootdir',
-               'ctime'   => DEFAULT_CTIME,
-               'format'  => 'raw',
-               'parent'  => undef,
-               'size'    => DEFAULT_SIZE,
-               'used'    => DEFAULT_USED,
-               'vmid'    => '16112',
-               'volid'   => 'local:16112/vm-16112-disk-0.raw',
-           },
-           {
-               'content' => 'backup',
-               'ctime'   => 1585604370,
-               'format'  => 'tar.lzo',
-               'size'    => DEFAULT_SIZE,
-               'subtype' => 'lxc',
-               'vmid'    => '16112',
-               'volid'   => 'local:backup/vzdump-lxc-16112-2020_03_30-21_39_30.tar.lzo',
-           },
-           {
-               'content' => 'backup',
-               'ctime'   => 1585604970,
-               'format'  => 'tar.gz',
-               'size'    => DEFAULT_SIZE,
-               'subtype' => 'lxc',
-               'vmid'    => '16112',
-               'volid'   => 'local:backup/vzdump-lxc-16112-2020_03_30-21_49_30.tar.gz',
-           },
-           {
-               'content' => 'backup',
-               'ctime'   => 1585604970,
-               'format'  => 'tar.zst',
-               'size'    => DEFAULT_SIZE,
-               'subtype' => 'lxc',
-               'vmid'    => '16112',
-               'volid'   => 'local:backup/vzdump-lxc-16112-2020_03_30-21_49_30.tar.zst',
-           },
-           {
-               'content' => 'backup',
-               'ctime'   => 1585605570,
-               'format'  => 'tgz',
-               'size'    => DEFAULT_SIZE,
-               'subtype' => 'lxc',
-               'vmid'    => '16112',
-               'volid'   => 'local:backup/vzdump-lxc-16112-2020_03_30-21_59_30.tgz',
-           },
-       ],
-    },
-    {
-       description => 'VMID: 16114, VM, qcow2, linked clone',
-       vmid => '16114',
-       files => [
-           "$storage_dir/images/16114/vm-16114-disk-0.qcow2",
-           "$storage_dir/images/16114/vm-16114-disk-1.qcow2",
-       ],
-       parent => [
-           "../9004/base-9004-disk-0.qcow2",
-           "../9004/base-9004-disk-1.qcow2",
-       ],
-       expected => [
-           {
-               'content' => 'images',
-               'ctime'   => DEFAULT_CTIME,
-               'format'  => 'qcow2',
-               'parent'  => '../9004/base-9004-disk-0.qcow2',
-               'size'    => DEFAULT_SIZE,
-               'used'    => DEFAULT_USED,
-               'vmid'    => '16114',
-               'volid'   => 'local:9004/base-9004-disk-0.qcow2/16114/vm-16114-disk-0.qcow2',
-           },
-           {
-               'content' => 'images',
-               'ctime'   => DEFAULT_CTIME,
-               'format'  => 'qcow2',
-               'parent'  => '../9004/base-9004-disk-1.qcow2',
-               'size'    => DEFAULT_SIZE,
-               'used'    => DEFAULT_USED,
-               'vmid'    => '16114',
-               'volid'   => 'local:9004/base-9004-disk-1.qcow2/16114/vm-16114-disk-1.qcow2',
-           },
-       ],
-    },
-    {
-       description => 'VMID: 9004, VM, template, qcow2',
-       vmid => '9004',
-       files => [
-           "$storage_dir/images/9004/base-9004-disk-0.qcow2",
-           "$storage_dir/images/9004/base-9004-disk-1.qcow2",
-       ],
-       expected => [
-           {
-               'content' => 'images',
-               'ctime'   => DEFAULT_CTIME,
-               'format'  => 'qcow2',
-               'parent'  => undef,
-               'size'    => DEFAULT_SIZE,
-               'used'    => DEFAULT_USED,
-               'vmid'    => '9004',
-               'volid'   => 'local:9004/base-9004-disk-0.qcow2',
-           },
-           {
-               'content' => 'images',
-               'ctime'   => DEFAULT_CTIME,
-               'format'  => 'qcow2',
-               'parent'  => undef,
-               'size'    => DEFAULT_SIZE,
-               'used'    => DEFAULT_USED,
-               'vmid'    => '9004',
-               'volid'   => 'local:9004/base-9004-disk-1.qcow2',
-           },
-       ],
-    },
-    {
-       description => 'VMID: none, templates, snippets, backup',
-       vmid => undef,
-       files => [
-           "$storage_dir/dump/vzdump-lxc-19253-2020_02_03-19_57_43.tar.gz",
-           "$storage_dir/dump/vzdump-lxc-19254-2019_01_21-19_29_19.tar",
-           "$storage_dir/template/iso/archlinux-2020.02.01-x86_64.iso",
-           "$storage_dir/template/iso/debian-8.11.1-amd64-DVD-1.iso",
-           "$storage_dir/template/iso/debian-9.12.0-amd64-netinst.iso",
-           "$storage_dir/template/iso/proxmox-ve_6.1-1.iso",
-           "$storage_dir/template/cache/archlinux-base_20190924-1_amd64.tar.gz",
-           "$storage_dir/template/cache/debian-10.0-standard_10.0-1_amd64.tar.gz",
-           "$storage_dir/template/cache/alpine-3.10-default_20190626_amd64.tar.xz",
-           "$storage_dir/snippets/userconfig.yaml",
-           "$storage_dir/snippets/hookscript.pl",
-           "$storage_dir/private/1234/", # fileparse needs / at the end
-           "$storage_dir/private/1234/subvol-1234-disk-0.subvol/", # fileparse needs / at the end
-       ],
-       expected => [
-           {
-               'content' => 'vztmpl',
-               'ctime'   => DEFAULT_CTIME,
-               'format'  => 'txz',
-               'size'    => DEFAULT_SIZE,
-               'volid'   => 'local:vztmpl/alpine-3.10-default_20190626_amd64.tar.xz',
-           },
-           {
-               'content' => 'vztmpl',
-               'ctime'   => DEFAULT_CTIME,
-               'format'  => 'tgz',
-               'size'    => DEFAULT_SIZE,
-               'volid'   => 'local:vztmpl/archlinux-base_20190924-1_amd64.tar.gz',
-           },
-           {
-               'content' => 'vztmpl',
-               'ctime'   => DEFAULT_CTIME,
-               'format'  => 'tgz',
-               'size'    => DEFAULT_SIZE,
-               'volid'   => 'local:vztmpl/debian-10.0-standard_10.0-1_amd64.tar.gz',
-           },
-           {
-               'content' => 'iso',
-               'ctime'   => DEFAULT_CTIME,
-               'format'  => 'iso',
-               'size'    => DEFAULT_SIZE,
-               'volid'   => 'local:iso/archlinux-2020.02.01-x86_64.iso',
-           },
-           {
-               'content' => 'iso',
-               'ctime'   => DEFAULT_CTIME,
-               'format'  => 'iso',
-               'size'    => DEFAULT_SIZE,
-               'volid'   => 'local:iso/debian-8.11.1-amd64-DVD-1.iso',
-           },
-           {
-               'content' => 'iso',
-               'ctime'   => DEFAULT_CTIME,
-               'format'  => 'iso',
-               'size'    => DEFAULT_SIZE,
-               'volid'   => 'local:iso/debian-9.12.0-amd64-netinst.iso',
-           },
-           {
-               'content' => 'iso',
-               'ctime'   => DEFAULT_CTIME,
-               'format'  => 'iso',
-               'size'    => DEFAULT_SIZE,
-               'volid'   => 'local:iso/proxmox-ve_6.1-1.iso',
-           },
-           {
-               'content' => 'backup',
-               'ctime'   => 1580759863,
-               'format'  => 'tar.gz',
-               'size'    => DEFAULT_SIZE,
-               'subtype' => 'lxc',
-               'vmid'    => '19253',
-               'volid'   => 'local:backup/vzdump-lxc-19253-2020_02_03-19_57_43.tar.gz',
-           },
-           {
-               'content' => 'backup',
-               'ctime'   => 1548098959,
-               'format'  => 'tar',
-               'size'    => DEFAULT_SIZE,
-               'subtype' => 'lxc',
-               'vmid'    => '19254',
-               'volid'   => 'local:backup/vzdump-lxc-19254-2019_01_21-19_29_19.tar',
-           },
-           {
-               'content' => 'snippets',
-               'ctime'   => DEFAULT_CTIME,
-               'format'  => 'snippet',
-               'size'    => DEFAULT_SIZE,
-               'volid'   => 'local:snippets/hookscript.pl',
-           },
-           {
-               'content' => 'snippets',
-               'ctime'   => DEFAULT_CTIME,
-               'format'  => 'snippet',
-               'size'    => DEFAULT_SIZE,
-               'volid'   => 'local:snippets/userconfig.yaml',
-           },
-       ],
-    },
-    {
-       description => 'VMID: none, parent, non-matching',
-       # string instead of vmid in folder
-       #"$storage_dir/images/ssss/base-4321-disk-0.qcow2/1234/vm-1234-disk-0.qcow2",
-       vmid => undef,
-       files => [
-           "$storage_dir/images/1234/vm-1234-disk-0.qcow2",
-       ],
-       parent => [
-           "../ssss/base-4321-disk-0.qcow2",
-       ],
-       expected => [
-           {
-               'content' => 'images',
-               'ctime'   => DEFAULT_CTIME,
-               'format'  => 'qcow2',
-               'parent'  => '../ssss/base-4321-disk-0.qcow2',
-               'size'    => DEFAULT_SIZE,
-               'used'    => DEFAULT_USED,
-               'vmid'    => '1234',
-               'volid'   => 'local:1234/vm-1234-disk-0.qcow2',
-           }
-       ],
-    },
-    {
-       description => 'VMID: none, non-matching',
-       # failed matches
-       vmid => undef,
-       files => [
-           "$storage_dir/images/ssss/base-4321-disk-0.raw",
-           "$storage_dir/images/ssss/vm-1234-disk-0.qcow2",
-           "$storage_dir/template/iso/yet-again-a-installation-disk.dvd",
-           "$storage_dir/template/cache/debian-10.0-standard_10.0-1_amd64.zip.gz",
-           "$storage_dir/template/cache/debian-10.0-standard_10.0-1_amd64.tar.bz2",
-           "$storage_dir/private/subvol-19254-disk-0/19254",
-           "$storage_dir/dump/vzdump-openvz-16112-2020_03_30-21_39_30.tar.bz2",
-           "$storage_dir/dump/vzdump-openvz-16112-2020_03_30-21_39_30.zip.gz",
-           "$storage_dir/dump/vzdump-openvz-16112-2020_03_30-21_39_30.tgz.lzo",
-           "$storage_dir/dump/vzdump-qemu-16110-2020_03_30-21_12_40.vma.xz",
-           "$storage_dir/dump/vzdump-qemu-16110-2020_03_30-21_12_40.vms.gz",
-       ],
-       expected => [], # returns empty list
-    },
-);
-
-
-# provide static vmlist for tests
-my $mock_cluster = Test::MockModule->new('PVE::Cluster', no_auto => 1);
-$mock_cluster->redefine(get_vmlist => sub { return $mocked_vmlist; });
-
-# populate is File::stat's method to fill all information from CORE::stat into
-# an blessed array.
-my $mock_stat = Test::MockModule->new('File::stat', no_auto => 1);
-$mock_stat->redefine(populate => sub {
-       my (@st) = @_;
-       $st[7] = DEFAULT_SIZE;
-       $st[10] = DEFAULT_CTIME;
-
-       my $result = $mock_stat->original('populate')->(@st);
-
-       return $result;
-});
-
-# override info provided by qemu-img in file_size_info
-my $mock_fsi = Test::MockModule->new('PVE::Storage::Plugin', no_auto => 1);
-$mock_fsi->redefine(file_size_info => sub {
-       my ($filename, $timeout) = @_;
-       my ($size, $format, $used, $parent, $ctime) = $mock_fsi->original('file_size_info')->($filename, $timeout);
-
-       $size = DEFAULT_SIZE;
-       $used = DEFAULT_USED;
-
-       return wantarray ? ($size, $format, $used, $parent, $ctime) : $size;
-});
-
-my $plan = scalar @tests;
-plan tests => $plan + 1;
-
-{
-    # don't accidentally modify vmlist, see bug report
-    # https://pve.proxmox.com/pipermail/pve-devel/2020-January/041096.html
-    my $scfg_with_type = { path => $storage_dir, type => 'dir' };
-    my $original_vmlist = { ids => {} };
-    my $tested_vmlist = dclone($original_vmlist);
-
-    PVE::Storage::Plugin->list_volumes('sid', $scfg_with_type, undef, ['images']);
-
-    is_deeply ($tested_vmlist, $original_vmlist,
-       'PVE::Cluster::vmlist remains unmodified')
-    || diag ("Expected vmlist to remain\n", explain($original_vmlist),
-       "but it turned to\n", explain($tested_vmlist));
-}
-
-
-{
-    my $sid = 'local';
-    my $types = [ 'rootdir', 'images', 'vztmpl', 'iso', 'backup', 'snippets' ];
-    my @suffixes = ( 'qcow2', 'raw', 'vmdk', 'vhdx' );
-
-    # run through test cases
-    foreach my $tt (@tests) {
-       my $vmid = $tt->{vmid};
-       my $files = $tt->{files};
-       my $expected = $tt->{expected};
-       my $description = $tt->{description};
-       my $parent = $tt->{parent};
-
-       # prepare environment
-       my $num = 0; #parent disks
-       for my $file (@$files) {
-           my ($name, $dir, $suffix) = fileparse($file, @suffixes);
-
-           make_path($dir, { verbose => 1, mode => 0755 });
-
-           if ($name) {
-               # using qemu-img to also be able to represent the backing device
-               my @cmd = ( '/usr/bin/qemu-img', 'create', "$file", DEFAULT_SIZE );
-               push @cmd, ( '-f', $suffix ) if $suffix;
-               push @cmd, ( '-u', '-b', @$parent[$num] ) if $parent;
-               push @cmd, ( '-F', $suffix ) if $parent && $suffix;
-               $num++;
-
-               run_command([@cmd]);
-           }
-       }
-
-       my $got;
-       eval { $got = PVE::Storage::Plugin->list_volumes($sid, $scfg, $vmid, $types) };
-       $got = $@ if $@;
-
-       is_deeply($got, $expected, $description) || diag(explain($got));
-
-       # clean up after each test case, otherwise
-       # we get wrong results from leftover files
-       remove_tree($storage_dir, { verbose => 1 });
-    }
-}
-
-done_testing();
-
-1;
diff --git a/test/parse_volname_test.pm b/test/parse_volname_test.pm
deleted file mode 100644 (file)
index d6ac885..0000000
+++ /dev/null
@@ -1,253 +0,0 @@
-package PVE::Storage::TestParseVolname;
-
-use strict;
-use warnings;
-
-use lib qw(..);
-
-use PVE::Storage;
-use Test::More;
-
-my $vmid = 1234;
-
-# an array of test cases, each test is comprised of the following keys:
-# description => to identify a single test
-# volname     => the input for parse_volname
-# expected    => the array that parse_volname returns
-my $tests = [
-    #
-    # VM images
-    #
-    {
-       description => 'VM disk image, linked, qcow2, vm- as base-',
-       volname     => "$vmid/vm-$vmid-disk-0.qcow2/$vmid/vm-$vmid-disk-0.qcow2",
-       expected    => [ 'images', "vm-$vmid-disk-0.qcow2", "$vmid", "vm-$vmid-disk-0.qcow2", "$vmid", undef, 'qcow2', ],
-    },
-    #
-    # iso
-    #
-    {
-       description => 'ISO image, iso',
-       volname     => 'iso/some-installation-disk.iso',
-       expected    => ['iso', 'some-installation-disk.iso'],
-    },
-    {
-       description => 'ISO image, img',
-       volname     => 'iso/some-other-installation-disk.img',
-       expected    => ['iso', 'some-other-installation-disk.img'],
-    },
-    #
-    # container templates
-    #
-    {
-       description => 'Container template tar.gz',
-       volname     => 'vztmpl/debian-10.0-standard_10.0-1_amd64.tar.gz',
-       expected    => ['vztmpl', 'debian-10.0-standard_10.0-1_amd64.tar.gz'],
-    },
-    {
-       description => 'Container template tar.xz',
-       volname     => 'vztmpl/debian-10.0-standard_10.0-1_amd64.tar.xz',
-       expected    => ['vztmpl', 'debian-10.0-standard_10.0-1_amd64.tar.xz'],
-    },
-    #
-    # container rootdir
-    #
-    {
-       description => 'Container rootdir, sub directory',
-       volname     => "rootdir/$vmid",
-       expected    => ['rootdir', "$vmid", "$vmid"],
-    },
-    {
-       description => 'Container rootdir, subvol',
-       volname     => "$vmid/subvol-$vmid-disk-0.subvol",
-       expected    => [ 'images', "subvol-$vmid-disk-0.subvol", "$vmid", undef, undef, undef, 'subvol' ],
-    },
-    {
-       description => 'Backup archive, no virtualization type',
-       volname     => "backup/vzdump-none-$vmid-2020_03_30-21_39_30.tar",
-       expected    => ['backup', "vzdump-none-$vmid-2020_03_30-21_39_30.tar"],
-    },
-    #
-    # Snippets
-    #
-    {
-       description => 'Snippets, yaml',
-       volname     => 'snippets/userconfig.yaml',
-       expected    => ['snippets', 'userconfig.yaml'],
-    },
-    {
-       description => 'Snippets, perl',
-       volname     => 'snippets/hookscript.pl',
-       expected    => ['snippets', 'hookscript.pl'],
-    },
-    #
-    # failed matches
-    #
-    {
-       description => "Failed match: VM disk image, base, raw",
-       volname     => "ssss/base-$vmid-disk-0.raw",
-       expected    => "unable to parse directory volume name 'ssss/base-$vmid-disk-0.raw'\n",
-    },
-    {
-       description => 'Failed match: ISO image, dvd',
-       volname     => 'iso/yet-again-a-installation-disk.dvd',
-       expected    => "unable to parse directory volume name 'iso/yet-again-a-installation-disk.dvd'\n",
-    },
-    {
-       description => 'Failed match: Container template, zip.gz',
-       volname     => 'vztmpl/debian-10.0-standard_10.0-1_amd64.zip.gz',
-       expected    => "unable to parse directory volume name 'vztmpl/debian-10.0-standard_10.0-1_amd64.zip.gz'\n",
-    },
-    {
-       description => 'Failed match: Container template, tar.bz2',
-       volname     => 'vztmpl/debian-10.0-standard_10.0-1_amd64.tar.bz2',
-       expected    => "unable to parse directory volume name 'vztmpl/debian-10.0-standard_10.0-1_amd64.tar.bz2'\n",
-    },
-    {
-       description => 'Failed match: Container rootdir, subvol',
-       volname     => "rootdir/subvol-$vmid-disk-0",
-       expected    => "unable to parse directory volume name 'rootdir/subvol-$vmid-disk-0'\n",
-    },
-    {
-       description => 'Failed match: VM disk image, linked, vhdx',
-       volname     => "$vmid/base-$vmid-disk-0.vhdx/$vmid/vm-$vmid-disk-0.vhdx",
-       expected    => "unable to parse volume filename 'base-$vmid-disk-0.vhdx'\n",
-    },
-    {
-       description => 'Failed match: VM disk image, linked, qcow2, first vmid',
-       volname     => "ssss/base-$vmid-disk-0.qcow2/$vmid/vm-$vmid-disk-0.qcow2",
-       expected    => "unable to parse directory volume name 'ssss/base-$vmid-disk-0.qcow2/$vmid/vm-$vmid-disk-0.qcow2'\n",
-    },
-    {
-       description => 'Failed match: VM disk image, linked, qcow2, second vmid',
-       volname     => "$vmid/base-$vmid-disk-0.qcow2/ssss/vm-$vmid-disk-0.qcow2",
-       expected    => "unable to parse volume filename 'base-$vmid-disk-0.qcow2/ssss/vm-$vmid-disk-0.qcow2'\n",
-    },
-];
-
-# create more test cases for VM disk images matches
-my $disk_suffix = [ 'raw', 'qcow2', 'vmdk' ];
-foreach my $s (@$disk_suffix) {
-    my @arr = (
-       {
-           description => "VM disk image, $s",
-           volname     => "$vmid/vm-$vmid-disk-1.$s",
-           expected    => [
-               'images',
-               "vm-$vmid-disk-1.$s",
-               "$vmid",
-               undef,
-               undef,
-               undef,
-               "$s",
-           ],
-       },
-       {
-           description => "VM disk image, linked, $s",
-           volname     => "$vmid/base-$vmid-disk-0.$s/$vmid/vm-$vmid-disk-0.$s",
-           expected    => [
-               'images',
-               "vm-$vmid-disk-0.$s",
-               "$vmid",
-               "base-$vmid-disk-0.$s",
-               "$vmid",
-               undef,
-               "$s",
-           ],
-       },
-       {
-           description => "VM disk image, base, $s",
-           volname     => "$vmid/base-$vmid-disk-0.$s",
-           expected    => [
-               'images',
-               "base-$vmid-disk-0.$s",
-               "$vmid",
-               undef,
-               undef,
-               'base-',
-               "$s"
-           ],
-       },
-    );
-
-    push @$tests, @arr;
-}
-
-
-# create more test cases for backup files matches
-my $bkp_suffix = {
-    qemu   => [ 'vma', 'vma.gz', 'vma.lzo', 'vma.zst' ],
-    lxc    => [ 'tar', 'tgz', 'tar.gz', 'tar.lzo', 'tar.zst' ],
-    openvz => [ 'tar', 'tgz', 'tar.gz', 'tar.lzo', 'tar.zst' ],
-};
-
-foreach my $virt (keys %$bkp_suffix) {
-    my $suffix = $bkp_suffix->{$virt};
-    foreach my $s (@$suffix) {
-       my @arr = (
-           {
-               description => "Backup archive, $virt, $s",
-               volname     => "backup/vzdump-$virt-$vmid-2020_03_30-21_12_40.$s",
-               expected    => [
-                   'backup',
-                   "vzdump-$virt-$vmid-2020_03_30-21_12_40.$s",
-                   "$vmid"
-               ],
-           },
-       );
-
-       push @$tests, @arr;
-    }
-}
-
-
-# create more test cases for failed backup files matches
-my $non_bkp_suffix = {
-    qemu   => [ 'vms.gz', 'vma.xz' ],
-    lxc    => [ 'tar.bz2', 'zip.gz', 'tgz.lzo' ],
-};
-foreach my $virt (keys %$non_bkp_suffix) {
-    my $suffix = $non_bkp_suffix->{$virt};
-    foreach my $s (@$suffix) {
-       my @arr = (
-           {
-               description => "Failed match: Backup archive, $virt, $s",
-               volname     => "backup/vzdump-$virt-$vmid-2020_03_30-21_12_40.$s",
-               expected    => "unable to parse directory volume name 'backup/vzdump-$virt-$vmid-2020_03_30-21_12_40.$s'\n",
-           },
-       );
-
-       push @$tests, @arr;
-    }
-}
-
-
-#
-# run through test case array
-#
-plan tests => scalar @$tests + 1;
-
-my $seen_vtype;
-my $vtype_subdirs = { map { $_ => 1 } keys %{ PVE::Storage::Plugin::get_vtype_subdirs() } };
-
-foreach my $t (@$tests) {
-    my $description = $t->{description};
-    my $volname = $t->{volname};
-    my $expected = $t->{expected};
-
-    my $got;
-    eval { $got = [ PVE::Storage::Plugin->parse_volname($volname) ] };
-    $got = $@ if $@;
-
-    is_deeply($got, $expected, $description);
-
-    $seen_vtype->{@$expected[0]} = 1 if ref $expected eq 'ARRAY';
-}
-
-# to check if all $vtype_subdirs are defined in path_to_volume_id
-# or have a test
-is_deeply($seen_vtype, $vtype_subdirs, "vtype_subdir check");
-
-done_testing();
-
-1;
diff --git a/test/path_to_volume_id_test.pm b/test/path_to_volume_id_test.pm
deleted file mode 100644 (file)
index 8149c88..0000000
+++ /dev/null
@@ -1,275 +0,0 @@
-package PVE::Storage::TestPathToVolumeId;
-
-use strict;
-use warnings;
-
-use lib qw(..);
-
-use PVE::Storage;
-
-use Test::More;
-
-use Cwd;
-use File::Basename;
-use File::Path qw(make_path remove_tree);
-use File::Temp;
-
-my $storage_dir = File::Temp->newdir();
-my $scfg = {
-    'digest' => 'd29306346b8b25b90a4a96165f1e8f52d1af1eda',
-    'ids'    => {
-       'local' => {
-           'shared'   => 0,
-           'path'     => "$storage_dir",
-           'type'     => 'dir',
-           'maxfiles' => 0,
-           'content'  => {
-               'snippets' => 1,
-               'rootdir'  => 1,
-               'images'   => 1,
-               'iso'      => 1,
-               'backup'   => 1,
-               'vztmpl'   => 1,
-           },
-       },
-    },
-    'order' => {
-       'local' => 1,
-    },
-};
-
-# the tests array consists of hashes with the following keys:
-# description => to identify the test case
-# volname     => to create the test file
-# expected    => the result that path_to_volume_id should return
-my @tests = (
-    {
-       description => 'Image, qcow2',
-       volname     => "$storage_dir/images/16110/vm-16110-disk-0.qcow2",
-       expected    => [
-           'images',
-           'local:16110/vm-16110-disk-0.qcow2',
-       ],
-    },
-    {
-       description => 'Image, raw',
-       volname     => "$storage_dir/images/16112/vm-16112-disk-0.raw",
-       expected    => [
-           'images',
-           'local:16112/vm-16112-disk-0.raw',
-       ],
-    },
-    {
-       description => 'Image template, qcow2',
-       volname     => "$storage_dir/images/9004/base-9004-disk-0.qcow2",
-       expected    => [
-           'images',
-           'local:9004/base-9004-disk-0.qcow2',
-       ],
-    },
-
-    {
-       description => 'Backup, vma.gz',
-       volname     => "$storage_dir/dump/vzdump-qemu-16110-2020_03_30-21_11_40.vma.gz",
-       expected    => [
-           'backup',
-           'local:backup/vzdump-qemu-16110-2020_03_30-21_11_40.vma.gz',
-       ],
-    },
-    {
-       description => 'Backup, vma.lzo',
-       volname     => "$storage_dir/dump/vzdump-qemu-16110-2020_03_30-21_12_45.vma.lzo",
-       expected    => [
-           'backup',
-           'local:backup/vzdump-qemu-16110-2020_03_30-21_12_45.vma.lzo',
-       ],
-    },
-    {
-       description => 'Backup, vma',
-       volname     => "$storage_dir/dump/vzdump-qemu-16110-2020_03_30-21_13_55.vma",
-       expected    => [
-           'backup',
-           'local:backup/vzdump-qemu-16110-2020_03_30-21_13_55.vma',
-       ],
-    },
-    {
-       description => 'Backup, tar.lzo',
-       volname     => "$storage_dir/dump/vzdump-lxc-16112-2020_03_30-21_39_30.tar.lzo",
-       expected    => [
-           'backup',
-           'local:backup/vzdump-lxc-16112-2020_03_30-21_39_30.tar.lzo',
-       ],
-    },
-    {
-       description => 'Backup, vma.zst',
-       volname     => "$storage_dir/dump/vzdump-qemu-16110-2020_03_30-21_13_55.vma.zst",
-       expected    => [
-           'backup',
-           'local:backup/vzdump-qemu-16110-2020_03_30-21_13_55.vma.zst'
-       ],
-    },
-    {
-       description => 'Backup, tar.zst',
-       volname     => "$storage_dir/dump/vzdump-lxc-16112-2020_03_30-21_39_30.tar.zst",
-       expected    => [
-           'backup',
-           'local:backup/vzdump-lxc-16112-2020_03_30-21_39_30.tar.zst'
-       ],
-    },
-
-    {
-       description => 'ISO file',
-       volname     => "$storage_dir/template/iso/yet-again-a-installation-disk.iso",
-       expected    => [
-           'iso',
-           'local:iso/yet-again-a-installation-disk.iso',
-       ],
-    },
-    {
-       description => 'CT template, tar.gz',
-       volname     => "$storage_dir/template/cache/debian-10.0-standard_10.0-1_amd64.tar.gz",
-       expected    => [
-           'vztmpl',
-           'local:vztmpl/debian-10.0-standard_10.0-1_amd64.tar.gz',
-       ],
-    },
-
-    {
-       description => 'Rootdir',
-       volname     => "$storage_dir/private/1234/", # fileparse needs / at the end
-       expected    => [
-           'rootdir',
-           'local:rootdir/1234',
-       ],
-    },
-    {
-       description => 'Rootdir, folder subvol',
-       volname     => "$storage_dir/images/1234/subvol-1234-disk-0.subvol/", # fileparse needs / at the end
-       expected    => [
-           'images',
-           'local:1234/subvol-1234-disk-0.subvol'
-       ],
-    },
-    {
-       description => 'Snippets, yaml',
-       volname => "$storage_dir/snippets/userconfig.yaml",
-       expected => [
-           'snippets',
-           'local:snippets/userconfig.yaml',
-       ],
-    },
-    {
-       description => 'Snippets, hookscript',
-       volname     => "$storage_dir/snippets/hookscript.pl",
-       expected    => [
-           'snippets',
-           'local:snippets/hookscript.pl',
-       ],
-    },
-    {
-       description => 'CT template, tar.xz',
-       volname     => "$storage_dir/template/cache/debian-10.0-standard_10.0-1_amd64.tar.xz",
-       expected    => [
-           'vztmpl',
-           'local:vztmpl/debian-10.0-standard_10.0-1_amd64.tar.xz',
-       ],
-    },
-
-    # no matches, path or files with failures
-    {
-       description => 'Base template, string as vmid in folder name',
-       volname     => "$storage_dir/images/ssss/base-4321-disk-0.raw",
-       expected    => [''],
-    },
-    {
-       description => 'ISO file, wrong ending',
-       volname     => "$storage_dir/template/iso/yet-again-a-installation-disk.dvd",
-       expected    => [''],
-    },
-    {
-       description => 'CT template, wrong ending, zip.gz',
-       volname     => "$storage_dir/template/cache/debian-10.0-standard_10.0-1_amd64.zip.gz",
-       expected    => [''],
-    },
-    {
-       description => 'CT template, wrong ending, tar bz2',
-       volname     => "$storage_dir/template/cache/debian-10.0-standard_10.0-1_amd64.tar.bz2",
-       expected    => [''],
-    },
-    {
-       description => 'Rootdir as subvol, wrong path',
-       volname     => "$storage_dir/private/subvol-19254-disk-0/",
-       expected    => [''],
-    },
-    {
-       description => 'Backup, wrong ending, openvz, tar.bz2',
-       volname     => "$storage_dir/dump/vzdump-openvz-16112-2020_03_30-21_39_30.tar.bz2",
-       expected    => [''],
-    },
-    {
-       description => 'Backup, wrong format, openvz, zip.gz',
-       volname     => "$storage_dir/dump/vzdump-openvz-16112-2020_03_30-21_39_30.zip.gz",
-       expected    => [''],
-    },
-    {
-       description => 'Backup, wrong format, openvz, tgz.lzo',
-       volname     => "$storage_dir/dump/vzdump-openvz-16112-2020_03_30-21_39_30.tgz.lzo",
-       expected    => [''],
-    },
-    {
-       description => 'Backup, wrong ending, qemu, vma.xz',
-       volname     => "$storage_dir/dump/vzdump-qemu-16110-2020_03_30-21_12_40.vma.xz",
-       expected    => [''],
-    },
-    {
-       description => 'Backup, wrong format, qemu, vms.gz',
-       volname     => "$storage_dir/dump/vzdump-qemu-16110-2020_03_30-21_12_40.vms.gz",
-       expected    => [''],
-    },
-    {
-       description => 'Image, string as vmid in folder name',
-       volname     => "$storage_dir/images/ssss/vm-1234-disk-0.qcow2",
-       expected    => [''],
-    },
-);
-
-plan tests => scalar @tests + 1;
-
-my $seen_vtype;
-my $vtype_subdirs = { map { $_ => 1 } keys %{ PVE::Storage::Plugin::get_vtype_subdirs() } };
-
-foreach my $tt (@tests) {
-    my $file = $tt->{volname};
-    my $expected = $tt->{expected};
-    my $description = $tt->{description};
-
-    # prepare environment
-    my ($name, $dir, $suffix) = fileparse($file);
-    make_path($dir, { verbose => 1, mode => 0755 });
-
-    if ($name) {
-       open(my $fh, ">>", "$file") || die "Error open file: $!";
-       close($fh);
-    }
-
-    # run tests
-    my $got;
-    eval { $got = [ PVE::Storage::path_to_volume_id($scfg, $file) ] };
-    $got = $@ if $@;
-
-    is_deeply($got, $expected, $description) || diag(explain($got));
-
-    $seen_vtype->{@$expected[0]} = 1
-       if ( @$expected[0] ne '' && scalar @$expected > 1);
-}
-
-# to check if all $vtype_subdirs are defined in path_to_volume_id
-# or have a test
-is_deeply($seen_vtype, $vtype_subdirs, "vtype_subdir check");
-
-#cleanup
-# File::Temp unlinks tempdir on exit
-
-done_testing();
-
-1;
diff --git a/test/prune_backups_test.pm b/test/prune_backups_test.pm
deleted file mode 100644 (file)
index b57d280..0000000
+++ /dev/null
@@ -1,492 +0,0 @@
-package PVE::Storage::TestPruneBackups;
-
-use strict;
-use warnings;
-
-use lib qw(..);
-
-use PVE::Storage;
-use Test::More;
-use Test::MockModule;
-
-my $storeid = 'BackTest123';
-my @vmids = (1234, 9001);
-
-# only includes the information needed for prune_backups
-my $mocked_backups_lists = {};
-
-my $basetime = 1577881101; # 2020_01_01-12_18_21 UTC
-
-foreach my $vmid (@vmids) {
-    push @{$mocked_backups_lists->{default}}, (
-       {
-           'volid' => "$storeid:backup/vzdump-qemu-$vmid-2018_05_26-11_18_21.tar.zst",
-           'ctime' => $basetime - 585*24*60*60 - 60*60,
-           'vmid'  => $vmid,
-       },
-       {
-           'volid' => "$storeid:backup/vzdump-qemu-$vmid-2019_12_31-11_18_21.tar.zst",
-           'ctime' => $basetime - 24*60*60 - 60*60,
-           'vmid'  => $vmid,
-       },
-       {
-           'volid' => "$storeid:backup/vzdump-qemu-$vmid-2019_12_31-11_18_51.tar.zst",
-           'ctime' => $basetime - 24*60*60 - 60*60 + 30,
-           'vmid'  => $vmid,
-           'protected' => 1,
-       },
-       {
-           'volid' => "$storeid:backup/vzdump-qemu-$vmid-2019_12_31-11_19_21.tar.zst",
-           'ctime' => $basetime - 24*60*60 - 60*60 + 60,
-           'vmid'  => $vmid,
-       },
-       {
-           'volid' => "$storeid:backup/vzdump-qemu-$vmid-2020_01_01-11_18_21.tar.zst",
-           'ctime' => $basetime - 60*60,
-           'vmid'  => $vmid,
-       },
-       {
-           'volid' => "$storeid:backup/vzdump-qemu-$vmid-2020_01_01-12_18_21.tar.zst",
-           'ctime' => $basetime,
-           'vmid'  => $vmid,
-       },
-       {
-           'volid' => "$storeid:backup/vzdump-lxc-$vmid-2020_01_01-12_18_21.tar.zst",
-           'ctime' => $basetime,
-           'vmid'  => $vmid,
-       },
-       {
-           'volid' => "$storeid:backup/vzdump-$vmid-renamed.tar.zst",
-           'ctime' => 1234,
-           'vmid'  => $vmid,
-       },
-    );
-}
-push @{$mocked_backups_lists->{year1970}}, (
-    {
-       'volid' => "$storeid:backup/vzdump-lxc-321-1970_01_01-00_01_23.tar.zst",
-       'ctime' => 83,
-       'vmid'  => 321,
-    },
-    {
-       'volid' => "$storeid:backup/vzdump-lxc-321-2070_01_01-00_01_00.tar.zst",
-       'ctime' => 60*60*24 * (365*100 + 25) + 60,
-       'vmid'  => 321,
-    },
-);
-push @{$mocked_backups_lists->{novmid}}, (
-    {
-       'volid' => "$storeid:backup/vzdump-lxc-novmid.tar.gz",
-       'ctime' => 1234,
-    },
-);
-push @{$mocked_backups_lists->{threeway}}, (
-    {
-       'volid' => "$storeid:backup/vzdump-qemu-7654-2019_12_25-12_18_21.tar.zst",
-       'ctime' => $basetime - 7*24*60*60,
-       'vmid'  => 7654,
-    },
-    {
-       'volid' => "$storeid:backup/vzdump-qemu-7654-2019_12_31-12_18_21.tar.zst",
-       'ctime' => $basetime - 24*60*60,
-       'vmid'  => 7654,
-    },
-    {
-       'volid' => "$storeid:backup/vzdump-qemu-7654-2020_01_01-12_18_21.tar.zst",
-       'ctime' => $basetime,
-       'vmid'  => 7654,
-    },
-);
-push @{$mocked_backups_lists->{weekboundary}}, (
-    {
-       'volid' => "$storeid:backup/vzdump-qemu-7654-2020_12_03-12_18_21.tar.zst",
-       'ctime' => $basetime + (366-31+2)*24*60*60,
-       'vmid'  => 7654,
-    },
-    {
-       'volid' => "$storeid:backup/vzdump-qemu-7654-2020_12_04-12_18_21.tar.zst",
-       'ctime' => $basetime + (366-31+3)*24*60*60,
-       'vmid'  => 7654,
-    },
-    {
-       'volid' => "$storeid:backup/vzdump-qemu-7654-2020_12_07-12_18_21.tar.zst",
-       'ctime' => $basetime + (366-31+6)*24*60*60,
-       'vmid'  => 7654,
-    },
-);
-my $current_list;
-my $mock_plugin = Test::MockModule->new('PVE::Storage::Plugin');
-$mock_plugin->redefine(list_volumes => sub {
-    my ($class, $storeid, $scfg, $vmid, $content_types) = @_;
-
-    my $list = $mocked_backups_lists->{$current_list};
-
-    return $list if !defined($vmid);
-
-    return [ grep { $_->{vmid} eq $vmid } @{$list} ];
-});
-
-sub generate_expected {
-    my ($vmids, $type, $marks) = @_;
-
-    my @expected;
-    foreach my $vmid (@{$vmids}) {
-       push @expected, (
-           {
-               'volid' => "$storeid:backup/vzdump-qemu-$vmid-2018_05_26-11_18_21.tar.zst",
-               'type'  => 'qemu',
-               'ctime' => $basetime - 585*24*60*60 - 60*60,
-               'mark'  => $marks->[0],
-               'vmid'  => $vmid,
-           },
-           {
-               'volid' => "$storeid:backup/vzdump-qemu-$vmid-2019_12_31-11_18_21.tar.zst",
-               'type'  => 'qemu',
-               'ctime' => $basetime - 24*60*60 - 60*60,
-               'mark'  => $marks->[1],
-               'vmid'  => $vmid,
-           },
-           {
-               'volid' => "$storeid:backup/vzdump-qemu-$vmid-2019_12_31-11_18_51.tar.zst",
-               'type'  => 'qemu',
-               'ctime' => $basetime - 24*60*60 - 60*60 + 30,
-               'mark'  => 'protected',
-               'vmid'  => $vmid,
-           },
-           {
-               'volid' => "$storeid:backup/vzdump-qemu-$vmid-2019_12_31-11_19_21.tar.zst",
-               'type'  => 'qemu',
-               'ctime' => $basetime - 24*60*60 - 60*60 + 60,
-               'mark'  => $marks->[2],
-               'vmid'  => $vmid,
-           },
-           {
-               'volid' => "$storeid:backup/vzdump-qemu-$vmid-2020_01_01-11_18_21.tar.zst",
-               'type'  => 'qemu',
-               'ctime' => $basetime - 60*60,
-               'mark'  => $marks->[3],
-               'vmid'  => $vmid,
-           },
-           {
-               'volid' => "$storeid:backup/vzdump-qemu-$vmid-2020_01_01-12_18_21.tar.zst",
-               'type'  => 'qemu',
-               'ctime' => $basetime,
-               'mark'  => $marks->[4],
-               'vmid'  => $vmid,
-           },
-       ) if !defined($type) || $type eq 'qemu';
-       push @expected, (
-           {
-               'volid' => "$storeid:backup/vzdump-lxc-$vmid-2020_01_01-12_18_21.tar.zst",
-               'type'  => 'lxc',
-               'ctime' => $basetime,
-               'mark'  => $marks->[5],
-               'vmid'  => $vmid,
-           },
-       ) if !defined($type) || $type eq 'lxc';
-       push @expected, (
-           {
-               'volid' => "$storeid:backup/vzdump-$vmid-renamed.tar.zst",
-               'type'  => 'unknown',
-               'ctime' => 1234,
-               'mark'  => 'renamed',
-               'vmid'  => $vmid,
-           },
-       ) if !defined($type);
-    }
-    return [ sort { $a->{volid} cmp $b->{volid} } @expected ];
-}
-
-# an array of test cases, each test is comprised of the following keys:
-# description   => to identify a single test
-# vmid          => VMID or undef for all
-# type          => 'qemu' or 'lxc' or undef for all
-# keep          => options describing what to keep
-# list          => backups list to use. defaults to 'default'
-# expected      => what prune_backups should return
-#
-# most of them are created further below
-my $tests = [
-    {
-       description => 'last=3, multiple IDs',
-       keep => {
-           'keep-last' => 3,
-       },
-       expected => generate_expected(\@vmids, undef, ['remove', 'remove', 'keep', 'keep', 'keep', 'keep']),
-    },
-    {
-       description => 'weekly=2, one ID',
-       vmid => $vmids[0],
-       keep => {
-           'keep-weekly' => 2,
-       },
-       expected => generate_expected([$vmids[0]], undef, ['keep', 'remove', 'remove', 'remove', 'keep', 'keep']),
-    },
-    {
-       description => 'daily=weekly=monthly=1, multiple IDs',
-       keep => {
-           'keep-hourly' => 0,
-           'keep-daily' => 1,
-           'keep-weekly' => 1,
-           'keep-monthly' => 1,
-       },
-       expected => generate_expected(\@vmids, undef, ['keep', 'remove', 'keep', 'remove', 'keep', 'keep']),
-    },
-    {
-       description => 'hourly=4, one ID',
-       vmid => $vmids[0],
-       keep => {
-           'keep-hourly' => 4,
-           'keep-daily' => 0,
-       },
-       expected => generate_expected([$vmids[0]], undef, ['keep', 'remove', 'keep', 'keep', 'keep', 'keep']),
-    },
-    {
-       description => 'yearly=2, multiple IDs',
-       keep => {
-           'keep-hourly' => 0,
-           'keep-daily' => 0,
-           'keep-weekly' => 0,
-           'keep-monthly' => 0,
-           'keep-yearly' => 2,
-       },
-       expected => generate_expected(\@vmids, undef, ['remove', 'remove', 'keep', 'remove', 'keep', 'keep']),
-    },
-    {
-       description => 'last=2,hourly=2 one ID',
-       vmid => $vmids[0],
-       keep => {
-           'keep-last' => 2,
-           'keep-hourly' => 2,
-       },
-       expected => generate_expected([$vmids[0]], undef, ['keep', 'remove', 'keep', 'keep', 'keep', 'keep']),
-    },
-    {
-       description => 'last=1,monthly=2, multiple IDs',
-       keep => {
-           'keep-last' => 1,
-           'keep-monthly' => 2,
-       },
-       expected => generate_expected(\@vmids, undef, ['keep', 'remove', 'keep', 'remove', 'keep', 'keep']),
-    },
-    {
-       description => 'monthly=3, one ID',
-       vmid => $vmids[0],
-       keep => {
-           'keep-monthly' => 3,
-       },
-       expected => generate_expected([$vmids[0]], undef, ['keep', 'remove', 'keep', 'remove', 'keep', 'keep']),
-    },
-    {
-       description => 'last=daily=weekly=1, multiple IDs',
-       keep => {
-           'keep-last' => 1,
-           'keep-daily' => 1,
-           'keep-weekly' => 1,
-       },
-       expected => generate_expected(\@vmids, undef, ['keep', 'remove', 'keep', 'remove', 'keep', 'keep']),
-    },
-    {
-       description => 'last=daily=weekly=1, others zero, multiple IDs',
-       keep => {
-           'keep-hourly' => 0,
-           'keep-last' => 1,
-           'keep-daily' => 1,
-           'keep-weekly' => 1,
-           'keep-monthly' => 0,
-           'keep-yearly' => 0,
-       },
-       expected => generate_expected(\@vmids, undef, ['keep', 'remove', 'keep', 'remove', 'keep', 'keep']),
-    },
-    {
-       description => 'daily=2, one ID',
-       vmid => $vmids[0],
-       keep => {
-           'keep-daily' => 2,
-       },
-       expected => generate_expected([$vmids[0]], undef, ['remove', 'remove', 'keep', 'remove', 'keep', 'keep']),
-    },
-    {
-       description => 'weekly=monthly=1, multiple IDs',
-       keep => {
-           'keep-weekly' => 1,
-           'keep-monthly' => 1,
-       },
-       expected => generate_expected(\@vmids, undef, ['keep', 'remove', 'remove', 'remove', 'keep', 'keep']),
-    },
-    {
-       description => 'weekly=yearly=1, one ID',
-       vmid => $vmids[0],
-       keep => {
-           'keep-weekly' => 1,
-           'keep-yearly' => 1,
-       },
-       expected => generate_expected([$vmids[0]], undef, ['keep', 'remove', 'remove', 'remove', 'keep', 'keep']),
-    },
-    {
-       description => 'weekly=yearly=1, one ID, type qemu',
-       vmid => $vmids[0],
-       type => 'qemu',
-       keep => {
-           'keep-weekly' => 1,
-           'keep-yearly' => 1,
-       },
-       expected => generate_expected([$vmids[0]], 'qemu', ['keep', 'remove', 'remove', 'remove', 'keep', '']),
-    },
-    {
-       description => 'week=yearly=1, one ID, type lxc',
-       vmid => $vmids[0],
-       type => 'lxc',
-       keep => {
-           'keep-last' => 1,
-       },
-       expected => generate_expected([$vmids[0]], 'lxc', ['', '', '', '', '', 'keep']),
-    },
-    {
-       description => 'yearly=1, year before 2000',
-       keep => {
-           'keep-yearly' => 1,
-       },
-       list => 'year1970',
-       expected => [
-           {
-               'volid' => "$storeid:backup/vzdump-lxc-321-1970_01_01-00_01_23.tar.zst",
-               'ctime' => 83,
-               'mark'  => 'remove',
-               'type'  => 'lxc',
-               'vmid'  => 321,
-           },
-           {
-               'volid' => "$storeid:backup/vzdump-lxc-321-2070_01_01-00_01_00.tar.zst",
-               'ctime' => 60*60*24 * (365*100 + 25) + 60,
-               'mark'  => 'keep',
-               'type'  => 'lxc',
-               'vmid'  => 321,
-           },
-       ],
-    },
-    {
-       description => 'last=1, ne ID, year before 2000',
-       keep => {
-           'keep-last' => 1,
-       },
-       list => 'novmid',
-       expected => [
-           {
-               'volid' => "$storeid:backup/vzdump-lxc-novmid.tar.gz",
-               'ctime' => 1234,
-               'mark'  => 'renamed',
-               'type'  => 'lxc',
-           },
-       ],
-    },
-    {
-       description => 'all missing, multiple IDs',
-       keep => {},
-       expected => generate_expected(\@vmids, undef, ['keep', 'keep', 'keep', 'keep', 'keep', 'keep']),
-    },
-    {
-       description => 'all zero, multiple IDs',
-       keep => {
-           'keep-last' => 0,
-           'keep-hourly' => 0,
-           'keep-daily' => 0,
-           'keep-weekly' => 0,
-           'keep-monthyl' => 0,
-           'keep-yearly' => 0,
-       },
-       expected => generate_expected(\@vmids, undef, ['keep', 'keep', 'keep', 'keep', 'keep', 'keep']),
-    },
-    {
-       description => 'some zero, some missing, multiple IDs',
-       keep => {
-           'keep-last' => 0,
-           'keep-hourly' => 0,
-           'keep-daily' => 0,
-           'keep-monthyl' => 0,
-           'keep-yearly' => 0,
-       },
-       expected => generate_expected(\@vmids, undef, ['keep', 'keep', 'keep', 'keep', 'keep', 'keep']),
-    },
-    {
-       description => 'daily=weekly=monthly=1',
-       keep => {
-           'keep-daily' => 1,
-           'keep-weekly' => 1,
-           'keep-monthly' => 1,
-       },
-       list => 'threeway',
-       expected => [
-           {
-               'volid' => "$storeid:backup/vzdump-qemu-7654-2019_12_25-12_18_21.tar.zst",
-               'ctime' => $basetime - 7*24*60*60,
-               'type'  => 'qemu',
-               'vmid'  => 7654,
-               'mark'  => 'keep',
-           },
-           {
-               'volid' => "$storeid:backup/vzdump-qemu-7654-2019_12_31-12_18_21.tar.zst",
-               'ctime' => $basetime - 24*60*60,
-               'type'  => 'qemu',
-               'vmid'  => 7654,
-               'mark'  => 'remove', # month is already covered by the backup kept by keep-weekly!
-           },
-           {
-               'volid' => "$storeid:backup/vzdump-qemu-7654-2020_01_01-12_18_21.tar.zst",
-               'ctime' => $basetime,
-               'type'  => 'qemu',
-               'vmid'  => 7654,
-               'mark'  => 'keep',
-           },
-       ],
-    },
-    {
-       description => 'daily=weekly=1,weekboundary',
-       keep => {
-           'keep-daily' => 1,
-           'keep-weekly' => 1,
-       },
-       list => 'weekboundary',
-       expected => [
-           {
-               'volid' => "$storeid:backup/vzdump-qemu-7654-2020_12_03-12_18_21.tar.zst",
-               'ctime' => $basetime + (366-31+2)*24*60*60,
-               'type'  => 'qemu',
-               'vmid'  => 7654,
-               'mark'  => 'remove',
-           },
-           {
-               'volid' => "$storeid:backup/vzdump-qemu-7654-2020_12_04-12_18_21.tar.zst",
-               'ctime' => $basetime + (366-31+3)*24*60*60,
-               'type'  => 'qemu',
-               'vmid'  => 7654,
-               'mark'  => 'keep',
-           },
-           {
-               'volid' => "$storeid:backup/vzdump-qemu-7654-2020_12_07-12_18_21.tar.zst",
-               'ctime' => $basetime + (366-31+6)*24*60*60,
-               'type'  => 'qemu',
-               'vmid'  => 7654,
-               'mark'  => 'keep',
-           },
-       ],
-    },
-];
-
-plan tests => scalar @$tests;
-
-for my $tt (@$tests) {
-
-    my $got = eval {
-       $current_list = $tt->{list} // 'default';
-       my $res = PVE::Storage::Plugin->prune_backups($tt->{scfg}, $storeid, $tt->{keep}, $tt->{vmid}, $tt->{type}, 1);
-       return [ sort { $a->{volid} cmp $b->{volid} } @{$res} ];
-    };
-    $got = $@ if $@;
-
-    is_deeply($got, $tt->{expected}, $tt->{description}) || diag(explain($got));
-}
-
-done_testing();
-
-1;
diff --git a/test/rbd_namespace.pl b/test/rbd_namespace.pl
deleted file mode 100755 (executable)
index 6b115ce..0000000
+++ /dev/null
@@ -1,379 +0,0 @@
-#!/usr/bin/perl
-
-# This script is meant to be run manually on hyperconverged PVE server with a
-# Ceph cluster. It tests how PVE handles RBD namespaces.
-#
-# The pool (default: rbd) must already exist. The namespace and VMs will be
-# created.
-#
-# Parameters like names for the pool an namespace and the VMID can be
-# configured.  The VMIDs for the clones is $vmid -1 and $vmid -2.
-#
-# Cleanup is done after a successful run. Cleanup can also be called manually.
-#
-# Known issues:
-#
-# * Snapshot rollback can sometimes be racy with stopping the VM and Ceph
-#  recognizing that the disk image is not in use anymore.
-
-use strict;
-use warnings;
-
-use Test::More;
-use Getopt::Long;
-use JSON;
-
-use PVE::Tools qw(run_command);
-
-my $pool = "testpool";
-my $use_existing= undef;
-my $namespace = "testspace";
-my $showhelp = '';
-my $vmid = 999999;
-my $cleanup = undef;
-my $DEBUG = 0;
-
-my $helpstring = "usage: $0 [OPTIONS]
-
-Known options are:
-
- --pool <name>         pool name, default: ${pool}
- --use-existing                use existing pool, default: 0, needs --pool set
- --namespace <name>    rbd namespace, default: ${namespace}
- --vmid <id>           VMID of the test VM, default: ${vmid}
- --cleanup             Remove the storage definitions, namespaces and VM afterwards
- -d, --debug           Enable debug output
- -h, --help            Print this help message
-";
-
-GetOptions (
-    "pool=s" => \$pool,
-    "use-existing" => \$use_existing,
-    "namespace=s" => \$namespace,
-    "vmid=i" => \$vmid,
-    "h|help" => \$showhelp,
-    "cleanup" => \$cleanup,
-    "d|debug" => \$DEBUG,
-) or die ($helpstring);
-
-if ($showhelp) {
-    warn $helpstring;
-    exit(0);
-}
-
-my $storage_name = "${pool}-${namespace}";
-
-my $vmid_clone = int($vmid) - 1;
-my $vmid_linked_clone = int($vmid) - 2;
-
-sub jp {
-    print to_json($_[0], { utf8 => 8, pretty => 1, canonical => 1 }) . "\n";
-}
-sub dbgvar {
-    jp(@_) if $DEBUG;
-}
-
-sub run_cmd {
-    my ($cmd, $json, $ignore_errors) = @_;
-
-    my $raw = '';
-    my $parser = sub {$raw .= shift;};
-
-    eval {
-       run_command($cmd, outfunc => $parser);
-    };
-    if (my $err = $@) {
-       die $err if !$ignore_errors;
-    }
-
-    if ($json) {
-       my $result;
-       if ($raw eq '') {
-           $result = [];
-       } elsif ($raw =~ m/^(\[.*\])$/s) { # untaint
-           $result = JSON::decode_json($1);
-       } else {
-           die "got unexpected data from command: '$cmd' -> '$raw'\n";
-       }
-       return $result;
-       }
-    return $raw;
-}
-
-sub run_test_cmd {
-    my ($cmd) = @_;
-
-    my $raw = '';
-    my $out = sub {
-       my $line = shift;
-       $raw .= "${line}\n";
-    };
-
-    eval {
-       run_command($cmd, outfunc => $out);
-    };
-    if (my $err = $@) {
-       print $raw;
-       print $err;
-       return 0;
-    }
-    print $raw;
-    return 1;
-}
-
-sub prepare {
-    print "Preparing test environment\n";
-
-    my $pools = run_cmd("ceph osd pool ls --format json", 1);
-
-    my %poolnames = map {$_ => 1} @$pools;
-    die "Pool '$pool' does not exist!\n"
-       if !exists($poolnames{$pool}) && $use_existing;
-
-    run_cmd(['pveceph', 'pool', 'create', ${pool}, '--add_storages', 1])
-       if !$use_existing;
-
-    my $namespaces = run_cmd(['rbd', '-p', ${pool}, 'namespace', 'ls', '--format', 'json'], 1);
-    dbgvar($namespace);
-    my $ns_found = 0;
-    for my $i (@$namespaces) {
-       $ns_found = 1 if $i->{name} eq $namespace;
-    }
-
-    if (!$ns_found) {
-       print "Create namespace '${namespace}' in pool '${pool}'\n";
-       run_cmd(['rbd', 'namespace', 'create', "${pool}/${namespace}"]);
-    }
-
-    my $storages = run_cmd(['pvesh', 'get', 'storage', '--output-format', 'json'], 1);
-    dbgvar($storages);
-    my $rbd_found = 0;
-    my $pool_found = 0;
-
-    print "Create storage definition\n";
-    for my $stor (@$storages) {
-       $pool_found = 1 if $stor->{storage} eq $pool;
-       $rbd_found = 1 if $stor->{storage} eq $storage_name;
-
-       if ($rbd_found) {
-           run_cmd(['pvesm', 'set', ${storage_name}, '--krbd', '0']);
-           die "Enable the storage '$stor->{storage}'!" if $stor->{disable};
-       }
-    }
-    if (!$pool_found) {
-       die "No storage for pool '${pool}' found! Must have same name as pool!\n"
-           if $use_existing;
-
-       run_cmd(['pvesm', 'add', 'rbd', $pool, '--pool', $pool, '--content', 'images,rootdir']);
-    }
-    # create PVE storages (librbd / krbd)
-    run_cmd(['pvesm', 'add', 'rbd', ${storage_name}, '--krbd', '0', '--pool', ${pool}, '--namespace', ${namespace}, '--content', 'images,rootdir'])
-       if !$rbd_found;
-
-
-    # create test VM
-    print "Create test VM ${vmid}\n";
-    my $vms = run_cmd(['pvesh', 'get', 'cluster/resources', '--type', 'vm', '--output-format', 'json'], 1);
-    for my $vm (@$vms) {
-       # TODO: introduce a force flag to make this behaviour configurable
-
-       if ($vm->{vmid} eq $vmid) {
-           print "Test VM '${vmid}' already exists. It will be removed and recreated!\n";
-           run_cmd(['qm', 'stop', ${vmid}], 0, 1);
-           run_cmd(['qm', 'destroy', ${vmid}]);
-       }
-    }
-    run_cmd(['qm', 'create', ${vmid}, '--bios', 'ovmf', '--efidisk0', "${storage_name}:1", '--scsi0', "${storage_name}:2"]);
-}
-
-
-sub cleanup {
-    print "Cleaning up test environment!\n";
-    print "Removing VMs\n";
-    run_cmd(['qm', 'stop', ${vmid}], 0, 1);
-    run_cmd(['qm', 'stop', ${vmid_linked_clone}], 0, 1);
-    run_cmd(['qm', 'stop', ${vmid_clone}], 0, 1);
-    run_cmd(['qm', 'destroy', ${vmid_linked_clone}], 0, 1);
-    run_cmd(['qm', 'destroy', ${vmid_clone}], 0, 1);
-    run_cmd(['for', 'i', 'in', "/dev/rbd/${pool}/${namespace}/*;", 'do', '/usr/bin/rbd', 'unmap', '\$i;', 'done'], 0, 1);
-    run_cmd(['qm', 'unlock', ${vmid}], 0, 1);
-    run_cmd(['qm', 'destroy', ${vmid}], 0, 1);
-
-    print "Removing Storage definition for ${storage_name}\n";
-    run_cmd(['pvesm', 'remove', ${storage_name}], 0, 1);
-
-    print "Removing RBD namespace '${pool}/${namespace}'\n";
-    run_cmd(['rbd', 'namespace', 'remove', "${pool}/${namespace}"], 0, 1);
-
-    if (!$use_existing) {
-       print "Removing Storage definition for ${pool}\n";
-       run_cmd(['pvesm', 'remove', ${pool}], 0, 1);
-       print "Removing test pool\n";
-       run_cmd(['pveceph', 'pool', 'destroy', $pool]);
-    }
-}
-
-my $tests = [
-    # Example structure for tests
-    # {
-    #     name => "name of test section",
-    #     preparations => [
-    #         ['some', 'prep', 'command'],
-    #     ],
-    #     steps => [
-    #         ['test', 'cmd', $vmid],
-    #         ['second', 'step', $vmid],
-    #     ],
-    #     cleanup => [
-    #         ['cleanup', 'command'],
-    #     ],
-    # },
-    {
-       name => 'first VM start',
-       steps => [
-           ['qm', 'start', $vmid],
-       ],
-    },
-    {
-       name => 'snapshot/rollback',
-       steps => [
-           ['qm', 'snapshot', $vmid, 'test'],
-           ['qm', 'rollback', $vmid, 'test'],
-       ],
-       cleanup => [
-           ['qm', 'unlock', $vmid],
-       ],
-    },
-    {
-       name => 'remove snapshot',
-       steps => [
-           ['qm', 'delsnapshot', $vmid, 'test'],
-       ],
-    },
-    {
-       name => 'moving disk between namespaces',
-       steps => [
-           ['qm', 'move_disk', $vmid, 'scsi0', $pool, '--delete', 1],
-           ['qm', 'move_disk', $vmid, 'scsi0', $storage_name, '--delete', 1],
-       ],
-    },
-    {
-       name => 'switch to krbd',
-       preparations => [
-           ['qm', 'stop', $vmid],
-           ['pvesm', 'set', $storage_name, '--krbd', 1]
-       ],
-    },
-    {
-       name => 'start VM with krbd',
-       steps => [
-           ['qm', 'start', $vmid],
-       ],
-    },
-    {
-       name => 'snapshot/rollback with krbd',
-       steps => [
-           ['qm', 'snapshot', $vmid, 'test'],
-           ['qm', 'rollback', $vmid, 'test'],
-       ],
-       cleanup => [
-           ['qm', 'unlock', $vmid],
-       ],
-    },
-    {
-       name => 'remove snapshot with krbd',
-       steps => [
-           ['qm', 'delsnapshot', $vmid, 'test'],
-       ],
-    },
-    {
-       name => 'moving disk between namespaces with krbd',
-       steps => [
-           ['qm', 'move_disk', $vmid, 'scsi0', $pool, '--delete', 1],
-           ['qm', 'move_disk', $vmid, 'scsi0', $storage_name, '--delete', 1],
-       ],
-    },
-    {
-       name => 'clone VM with krbd',
-       steps => [
-           ['qm', 'clone', $vmid, $vmid_clone],
-       ],
-    },
-    {
-       name => 'switch to non krbd',
-       preparations => [
-           ['qm', 'stop', $vmid],
-           ['qm', 'stop', $vmid_clone],
-           ['pvesm', 'set', $storage_name, '--krbd', 0]
-       ],
-    },
-    {
-       name => 'templates and linked clone',
-       steps => [
-           ['qm', 'template', $vmid],
-           ['qm', 'clone', $vmid, $vmid_linked_clone],
-           ['qm', 'start', $vmid_linked_clone],
-           ['qm', 'stop', $vmid_linked_clone],
-       ],
-    },
-    {
-       name => 'start linked clone with krbd',
-       preparations => [
-           ['pvesm', 'set', $storage_name, '--krbd', 1]
-       ],
-       steps => [
-           ['qm', 'start', $vmid_linked_clone],
-           ['qm', 'stop', $vmid_linked_clone],
-       ],
-    },
-];
-
-sub run_prep_cleanup {
-    my ($cmds) = @_;
-
-    for (@$cmds) {
-       print join(' ', @$_). "\n";
-       run_cmd($_);
-    }
-}
-
-sub run_steps {
-    my ($steps) = @_;
-
-    for (@$steps) {
-       ok(run_test_cmd($_), join(' ', @$_));
-    }
-}
-
-sub run_tests {
-    print "Running tests:\n";
-
-    my $num_tests = 0;
-    for (@$tests) {
-       $num_tests += scalar(@{$_->{steps}}) if defined $_->{steps};
-    }
-
-    print("Tests: $num_tests\n");
-    plan tests => $num_tests;
-
-    for my $test (@$tests) {
-       print "Section: $test->{name}\n";
-       run_prep_cleanup($test->{preparations}) if defined $test->{preparations};
-       run_steps($test->{steps}) if defined $test->{steps};
-       run_prep_cleanup($test->{cleanup}) if defined $test->{cleanup};
-    }
-
-    done_testing();
-
-    if (Test::More->builder->is_passing()) {
-       cleanup();
-    }
-}
-
-if ($cleanup) {
-    cleanup();
-} else {
-    prepare();
-    run_tests();
-}
-
diff --git a/test/run_bwlimit_tests.pl b/test/run_bwlimit_tests.pl
deleted file mode 100755 (executable)
index 6ae379c..0000000
+++ /dev/null
@@ -1,211 +0,0 @@
-#!/usr/bin/perl
-
-use strict;
-use warnings;
-
-use Test::MockModule;
-use Test::More;
-
-use lib ('.', '..');
-use PVE::RPCEnvironment;
-use PVE::Cluster;
-use PVE::Storage;
-
-my $datacenter_cfg = <<'EOF';
-bwlimit: default=100,move=80,restore=60
-EOF
-
-my $storage_cfg = <<'EOF';
-dir: nolimit
-       path /dir/a
-
-dir: d50
-       path /dir/b
-       bwlimit default=50
-
-dir: d50m40r30
-       path /dir/c
-       bwlimit default=50,move=40,restore=30
-
-dir: d20m40r30
-       path /dir/c
-       bwlimit default=20,move=40,restore=30
-
-dir: d200m400r300
-       path /dir/c
-       bwlimit default=200,move=400,restore=300
-
-dir: d10
-       path /dir/d
-       bwlimit default=10
-
-dir: m50
-       path /dir/e
-       bwlimit move=50
-
-dir: d200
-       path /dir/f
-       bwlimit default=200
-
-EOF
-
-my $permissions = {
-    'user1@test' => {},
-    'user2@test' => { '/' => ['Sys.Modify'], },
-    'user3@test' => { '/storage' => ['Datastore.Allocate'], },
-    'user4@test' => { '/storage/d20m40r30' => ['Datastore.Allocate'], },
-};
-
-my $pve_cluster_module;
-$pve_cluster_module = Test::MockModule->new('PVE::Cluster');
-$pve_cluster_module->mock(
-    cfs_update => sub {},
-    get_config => sub {
-       my ($file) = @_;
-       if ($file eq 'datacenter.cfg') {
-           return $datacenter_cfg;
-       } elsif ($file eq 'storage.cfg') {
-           return $storage_cfg;
-       }
-       die "TODO: mock get_config($file)\n";
-    },
-);
-
-my $rpcenv_module;
-$rpcenv_module = Test::MockModule->new('PVE::RPCEnvironment');
-$rpcenv_module->mock(
-    check => sub {
-       my ($env, $user, $path, $perms, $noerr) = @_;
-       return 1 if $user eq 'root@pam';
-       my $userperms = $permissions->{$user}
-           or die "no permissions defined for user $user\n";
-       if (defined(my $pathperms = $userperms->{$path})) {
-           foreach my $pp (@$pathperms) {
-               foreach my $reqp (@$perms) {
-                   return 1 if $pp eq $reqp;
-               }
-           }
-       }
-       die "permission denied\n" if !$noerr;
-       return 0;
-    },
-);
-
-my $rpcenv = PVE::RPCEnvironment->init('pub');
-
-my @tests = (
-    [ user => 'root@pam' ],
-    [ ['unknown', ['nolimit'],   undef], 100, 'root / generic default limit, requesting default' ],
-    [ ['move',    ['nolimit'],   undef],  80, 'root / specific default limit, requesting default (move)' ],
-    [ ['restore', ['nolimit'],   undef],  60, 'root / specific default limit, requesting default (restore)' ],
-    [ ['unknown', ['d50m40r30'], undef],  50, 'root / storage default limit' ],
-    [ ['move',    ['d50m40r30'], undef],  40, 'root / specific storage limit (move)' ],
-    [ ['restore', ['d50m40r30'], undef],  30, 'root / specific storage limit (restore)' ],
-    [ ['unknown', ['nolimit'],       0],   0, 'root / generic default limit' ],
-    [ ['move',    ['nolimit'],       0],   0, 'root / specific default limit (move)' ],
-    [ ['restore', ['nolimit'],       0],   0, 'root / specific default limit (restore)' ],
-    [ ['unknown', ['d50m40r30'],     0],   0, 'root / storage default limit' ],
-    [ ['move',    ['d50m40r30'],     0],   0, 'root / specific storage limit (move)' ],
-    [ ['restore', ['d50m40r30'],     0],   0, 'root / specific storage limit (restore)' ],
-    [ ['migrate', undef,           100], 100, 'root / undef storage (migrate)' ],
-    [ ['migrate', [],              100], 100, 'root / no storage (migrate)' ],
-    [ ['migrate', [undef],       undef], 100, 'root / [undef] storage no override (migrate)' ],
-    [ ['migrate', [undef, undef],  200], 200, 'root / list of undef storages with override (migrate)' ],
-
-    [ user => 'user1@test' ],
-    [ ['unknown', ['nolimit'],      undef], 100, 'generic default limit' ],
-    [ ['move',    ['nolimit'],      undef],  80, 'specific default limit (move)' ],
-    [ ['restore', ['nolimit'],      undef],  60, 'specific default limit (restore)' ],
-    [ ['unknown', ['d50m40r30'],    undef],  50, 'storage default limit' ],
-    [ ['move',    ['d50m40r30'],    undef],  40, 'specific storage limit (move)' ],
-    [ ['restore', ['d50m40r30'],    undef],  30, 'specific storage limit (restore)' ],
-    [ ['unknown', ['d200m400r300'], undef], 200, 'storage default limit above datacenter limits' ],
-    [ ['move',    ['d200m400r300'], undef], 400, 'specific storage limit above datacenter limits (move)' ],
-    [ ['restore', ['d200m400r300'], undef], 300, 'specific storage limit above datacenter limits (restore)' ],
-    [ ['unknown', ['d50'],          undef],  50, 'storage default limit' ],
-    [ ['move',    ['d50'],          undef],  50, 'storage default limit (move)' ],
-    [ ['restore', ['d50'],          undef],  50, 'storage default limit (restore)' ],
-
-    [ user => 'user2@test' ],
-    [ ['unknown', ['nolimit'],       0],     0, 'generic default limit with Sys.Modify, passing unlimited' ],
-    [ ['unknown', ['nolimit'],   undef],   100, 'generic default limit with Sys.Modify' ],
-    [ ['move',    ['nolimit'],   undef],    80, 'specific default limit with Sys.Modify (move)' ],
-    [ ['restore', ['nolimit'],   undef],    60, 'specific default limit with Sys.Modify (restore)' ],
-    [ ['restore', ['nolimit'],       0],     0, 'specific default limit with Sys.Modify, passing unlimited (restore)' ],
-    [ ['move',    ['nolimit'],       0],     0, 'specific default limit with Sys.Modify, passing unlimited (move)' ],
-    [ ['unknown', ['d50m40r30'], undef],    50, 'storage default limit with Sys.Modify' ],
-    [ ['restore', ['d50m40r30'], undef],    30, 'specific storage limit with Sys.Modify (restore)' ],
-    [ ['move',    ['d50m40r30'], undef],    40, 'specific storage limit with Sys.Modify (move)' ],
-
-    [ user => 'user3@test' ],
-    [ ['unknown', ['nolimit'],   undef],   100, 'generic default limit with privileges on /' ],
-    [ ['unknown', ['nolimit'],      80],    80, 'generic default limit with privileges on /, passing an override value' ],
-    [ ['unknown', ['nolimit'],       0],     0, 'generic default limit with privileges on /, passing unlimited' ],
-    [ ['move',    ['nolimit'],   undef],    80, 'specific default limit with privileges on / (move)' ],
-    [ ['move',    ['nolimit'],       0],     0, 'specific default limit with privileges on /, passing unlimited (move)' ],
-    [ ['restore', ['nolimit'],   undef],    60, 'specific default limit with privileges on / (restore)' ],
-    [ ['restore', ['nolimit'],       0],     0, 'specific default limit with privileges on /, passing unlimited (restore)' ],
-    [ ['unknown', ['d50m40r30'],     0],     0, 'storage default limit with privileges on /, passing unlimited' ],
-    [ ['unknown', ['d50m40r30'], undef],    50, 'storage default limit with privileges on /' ],
-    [ ['unknown', ['d50m40r30'],     0],     0, 'storage default limit with privileges on, passing unlimited /' ],
-    [ ['move',    ['d50m40r30'], undef],    40, 'specific storage limit with privileges on / (move)' ],
-    [ ['move',    ['d50m40r30'],     0],     0, 'specific storage limit with privileges on, passing unlimited / (move)' ],
-    [ ['restore', ['d50m40r30'], undef],    30, 'specific storage limit with privileges on / (restore)' ],
-    [ ['restore', ['d50m40r30'],     0],     0, 'specific storage limit with privileges on /, passing unlimited (restore)' ],
-
-    [ user => 'user4@test' ],
-    [ ['unknown', ['nolimit'],                   10],     10, 'generic default limit with privileges on a different storage, passing lower override' ],
-    [ ['unknown', ['nolimit'],                undef],    100, 'generic default limit with privileges on a different storage' ],
-    [ ['unknown', ['nolimit'],                    0],    100, 'generic default limit with privileges on a different storage, passing unlimited' ],
-    [ ['move',    ['nolimit'],                undef],     80, 'specific default limit with privileges on a different storage (move)' ],
-    [ ['restore', ['nolimit'],                undef],     60, 'specific default limit with privileges on a different storage (restore)' ],
-    [ ['unknown', ['d50m40r30'],              undef],     50, 'storage default limit with privileges on a different storage' ],
-    [ ['move',    ['d50m40r30'],              undef],     40, 'specific storage limit with privileges on a different storage (move)' ],
-    [ ['restore', ['d50m40r30'],              undef],     30, 'specific storage limit with privileges on a different storage (restore)' ],
-    [ ['unknown', ['d20m40r30'],              undef],     20, 'storage default limit with privileges on that storage' ],
-    [ ['unknown', ['d20m40r30'],                  0],      0, 'storage default limit with privileges on that storage, passing unlimited' ],
-    [ ['move',    ['d20m40r30'],              undef],     40, 'specific storage limit with privileges on that storage (move)' ],
-    [ ['move',    ['d20m40r30'],                  0],      0, 'specific storage limit with privileges on that storage, passing unlimited (move)' ],
-    [ ['move',    ['d20m40r30'],                 10],     10, 'specific storage limit with privileges on that storage, passing low override (move)' ],
-    [ ['move',    ['d20m40r30'],                300],    300, 'specific storage limit with privileges on that storage, passing high override (move)' ],
-    [ ['restore', ['d20m40r30'],              undef],     30, 'specific storage limit with privileges on that storage (restore)' ],
-    [ ['restore', ['d20m40r30'],                  0],      0, 'specific storage limit with privileges on that storage, passing unlimited (restore)' ],
-    [ ['unknown', ['d50m40r30', 'd20m40r30'],     0],     50, 'multiple storages default limit with privileges on one of them, passing unlimited' ],
-    [ ['move',    ['d50m40r30', 'd20m40r30'],     0],     40, 'multiple storages specific limit with privileges on one of them, passing unlimited (move)' ],
-    [ ['restore', ['d50m40r30', 'd20m40r30'],     0],     30, 'multiple storages specific limit with privileges on one of them, passing unlimited (restore)' ],
-    [ ['unknown', ['d50m40r30', 'd20m40r30'], undef],     20, 'multiple storages default limit with privileges on one of them' ],
-    [ ['unknown', ['d10', 'd20m40r30'],       undef],     10, 'multiple storages default limit with privileges on one of them (storage limited)' ],
-    [ ['move',    ['d10', 'd20m40r30'],       undef],     10, 'multiple storages specific limit with privileges on one of them (storage limited) (move)' ],
-    [ ['restore', ['d10', 'd20m40r30'],       undef],     10, 'multiple storages specific limit with privileges on one of them (storage limited) (restore)' ],
-    [ ['restore', ['d10', 'd20m40r30'],           5],      5, 'multiple storages specific limit (storage limited) (restore), passing lower override' ],
-    [ ['restore', ['d200', 'd200m400r300'],      65],     65, 'multiple storages specific limit (storage limited) (restore), passing lower override' ],
-    [ ['restore', ['d200', 'd200m400r300'],     400],    200, 'multiple storages specific limit (storage limited) (restore), passing higher override' ],
-    [ ['restore', ['d200', 'd200m400r300'],       0],    200, 'multiple storages specific limit (storage limited) (restore), passing unlimited' ],
-    [ ['restore', ['d200', 'd200m400r300'],       1],      1, 'multiple storages specific limit (storage limited) (restore), passing 1' ],
-    [ ['restore', ['d10', 'd20m40r30'],         500],     10, 'multiple storages specific limit with privileges on one of them (storage limited) (restore), passing higher override' ],
-    [ ['unknown', ['nolimit', 'd20m40r30'],       0],    100, 'multiple storages default limit with privileges on one of them, passing unlimited (default limited)' ],
-    [ ['move',    ['nolimit', 'd20m40r30'],       0],     80, 'multiple storages specific limit with privileges on one of them, passing unlimited (default limited) (move)' ],
-    [ ['restore', ['nolimit', 'd20m40r30'],       0],     60, 'multiple storages specific limit with privileges on one of them, passing unlimited (default limited) (restore)' ],
-    [ ['unknown', ['nolimit', 'd20m40r30'],   undef],     20, 'multiple storages default limit with privileges on one of them (default limited)' ],
-    [ ['move',    ['nolimit', 'd20m40r30'],   undef],     40, 'multiple storages specific limit with privileges on one of them (default limited) (move)' ],
-    [ ['restore', ['nolimit', 'd20m40r30'],   undef],     30, 'multiple storages specific limit with privileges on one of them (default limited) (restore)' ],
-    [ ['restore', ['d20m40r30', 'm50'],         200],     60, 'multiple storages specific limit with privileges on one of them (global default limited) (restore)' ],
-    [ ['move',    ['nolimit', undef ],          40] ,     40, 'multiple storages one undefined, passing 40 (move)' ],
-    [ ['move',    undef,                       100] ,     80, 'undef storage, passing 100 (move)' ],
-    [ ['move',    [undef],                     100] ,     80, '[undef] storage, passing 100 (move)' ],
-    [ ['move',    [undef],                   undef] ,     80, '[undef] storage, no override (move)' ],
-);
-
-foreach my $t (@tests) {
-    my ($args, $expected, $description) = @$t;
-    if (!ref($args)) {
-       if ($args eq 'user') {
-           $rpcenv->set_user($expected);
-       } else {
-           die "not a test specification\n";
-       }
-       next;
-    }
-    is(PVE::Storage::get_bandwidth_limit(@$args), $expected, $description);
-}
-done_testing();
diff --git a/test/run_disk_tests.pl b/test/run_disk_tests.pl
deleted file mode 100755 (executable)
index c1a698e..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/usr/bin/perl
-
-use strict;
-use warnings;
-
-use TAP::Harness;
-
-my $harness = TAP::Harness->new( { verbosity => -2 });
-my $res = $harness->runtests( "disklist_test.pm" );
-
-exit -1 if !$res || $res->{failed} || $res->{parse_errors};
-
diff --git a/test/run_plugin_tests.pl b/test/run_plugin_tests.pl
deleted file mode 100755 (executable)
index d33429a..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/usr/bin/perl
-
-use strict;
-use warnings;
-
-# to ensure consistent ctime values on all systems
-$ENV{TZ} = 'UTC';
-
-use TAP::Harness;
-
-my $harness = TAP::Harness->new( { verbosity => -1 });
-my $res = $harness->runtests(
-    "archive_info_test.pm",
-    "parse_volname_test.pm",
-    "list_volumes_test.pm",
-    "path_to_volume_id_test.pm",
-    "get_subdir_test.pm",
-    "filesystem_path_test.pm",
-    "prune_backups_test.pm",
-);
-
-exit -1 if !$res || $res->{failed} || $res->{parse_errors};
diff --git a/test/run_test_zfspoolplugin.pl b/test/run_test_zfspoolplugin.pl
deleted file mode 100755 (executable)
index 095ccb3..0000000
+++ /dev/null
@@ -1,2765 +0,0 @@
-#!/usr/bin/perl
-
-use lib '..';
-
-use strict;
-use warnings;
-
-use Data::Dumper qw(Dumper);
-use PVE::Storage;
-use PVE::Cluster;
-use PVE::Tools qw(run_command);
-use Cwd;
-$Data::Dumper::Sortkeys = 1;
-
-my $verbose = undef;
-
-my $storagename = "zfstank99";
-my $subvol = 'regressiontest';
-my $mountpoint = "${subvol}_mnt";
-
-#volsize in GB
-my $volsize = 1;
-my $vmdisk = "vm-102-disk-1";
-my $vmbase = "base-100-disk-1";
-my $vmlinked = "vm-101-disk-1";
-my $ctdisk = "subvol-202-disk-1";
-my $ctbase = "basevol-200-disk-1";
-my $ctlinked = "subvol-201-disk-1";
-
-my $basesnap = '@__base__';
-my $tests = {};
-
-#create zfs subvol for testing
-my $pool = undef;
-my $zpath = undef;
-my $cfg = undef;
-my $scfg = undef;
-my $count = 0;
-my $testnum = 19;
-my $end_test = $testnum;
-my $start_test = 1;
-
-if (@ARGV == 2) {
-    $end_test = $ARGV[1];
-    $start_test = $ARGV[0];
-} elsif (@ARGV == 1) {
-    $start_test = $ARGV[0];
-    $end_test = $ARGV[0];
-}
-
-my $test19 = sub {
-
-    print "\nrun test19 \"path\"\n";
-
-    my @res;
-    my $fail = 0;
-    eval {
-       @res = PVE::Storage::path($cfg, "$storagename:$vmdisk");
-       if ($res[0] ne "\/dev\/zvol\/regressiontest\/$vmdisk") {
-           $count++;
-           $fail = 1;
-           warn "Test 19 a: path is not correct: expected \'\/dev\/zvol\/regressiontest\/$vmdisk'\  get \'$res[0]\'";
-       }
-       if ($res[1] ne "102") {
-           if (!$fail) {
-               $count++;
-               $fail = 1;
-           }
-           warn "Test 19 a: owner is not correct: expected \'102\'  get \'$res[1]\'";
-       }
-       if ($res[2] ne "images") {
-           if (!$fail) {
-               $count++;
-               $fail = 1;
-           }
-           warn "Test 19 a: owner is not correct: expected \'images\'  get \'$res[2]\'";
-       }
-    };
-    if ( $@ ) {
-       $count++;
-       warn "Test 19 a: $@";
-    }
-
-    @res = undef;
-    $fail = 0;
-    eval {
-       @res = PVE::Storage::path($cfg, "$storagename:$vmbase");
-       if ($res[0] ne "\/dev\/zvol\/regressiontest\/$vmbase") {
-           $count++;
-           $fail = 1;
-           warn "Test 19 b: path is not correct: expected \'\/dev\/zvol\/regressiontest\/$vmbase'\  get \'$res[0]\'";
-       }
-       if ($res[1] ne "100") {
-           if (!$fail) {
-               $count++;
-               $fail = 1;
-           }
-           warn "Test 19 b: owner is not correct: expected \'100\'  get \'$res[1]\'";
-       }
-       if ($res[2] ne "images") {
-           if (!$fail) {
-               $count++;
-               $fail = 1;
-           }
-           warn "Test 19 b: owner is not correct: expected \'images\'  get \'$res[2]\'";
-       }
-    };
-    if ( $@ ) {
-       $count++;
-       warn "Test 19 b: $@";
-    }
-
-    @res = undef;
-    $fail = 0;
-    eval {
-       @res = PVE::Storage::path($cfg, "$storagename:$vmbase\/$vmlinked");
-       if ($res[0] ne "\/dev\/zvol\/regressiontest\/$vmlinked") {
-           $count++;
-           $fail = 1;
-           warn "Test 19 c: path is not correct: expected \'\/dev\/zvol\/regressiontest\/$vmlinked'\  get \'$res[0]\'";
-       }
-       if ($res[1] ne "101") {
-           if (!$fail) {
-               $count++;
-               $fail = 1;
-           }
-           warn "Test 19 c: owner is not correct: expected \'101\'  get \'$res[1]\'";
-       }
-       if ($res[2] ne "images") {
-           if (!$fail) {
-               $count++;
-               $fail = 1;
-           }
-           warn "Test 19 c: owner is not correct: expected \'images\'  get \'$res[2]\'";
-       }
-    };
-    if ( $@ ) {
-       $count++;
-       warn "Test 19 c: $@";
-    }
-
-    @res = undef;
-    $fail = 0;
-    eval {
-       @res = PVE::Storage::path($cfg, "$storagename:$ctdisk");
-       if ($res[0] ne "\/$mountpoint\/$ctdisk") {
-           $count++;
-           $fail = 1;
-           warn "Test 19 d: path is not correct: expected \'\/$mountpoint\/$ctdisk'\  get \'$res[0]\'";
-       }
-       if ($res[1] ne "202") {
-           if (!$fail) {
-               $count++;
-               $fail = 1;
-           }
-           warn "Test 19 d: owner is not correct: expected \'202\'  get \'$res[1]\'";
-       }
-       if ($res[2] ne "images") {
-           if (!$fail) {
-               $count++;
-               $fail = 1;
-           }
-           warn "Test 19 d: owner is not correct: expected \'images\'  get \'$res[2]\'";
-       }
-    };
-    if ( $@ ) {
-       $count++;
-       warn "Test 19 d: $@";
-    }
-
-    @res = undef;
-    $fail = 0;
-    eval {
-       @res = PVE::Storage::path($cfg, "$storagename:$ctbase");
-       if ($res[0] ne "\/$mountpoint\/$ctbase") {
-           $count++;
-           $fail = 1;
-           warn "Test 19 e: path is not correct: expected \'\/$mountpoint\/$ctbase'\  get \'$res[0]\'";
-       }
-       if ($res[1] ne "200") {
-           if (!$fail) {
-               $count++;
-               $fail = 1;
-           }
-           warn "Test 19 e: owner is not correct: expected \'200\'  get \'$res[1]\'";
-       }
-       if ($res[2] ne "images") {
-           if (!$fail) {
-               $count++;
-               $fail = 1;
-           }
-           warn "Test 19 e: owner is not correct: expected \'images\'  get \'$res[2]\'";
-       }
-    };
-    if ( $@ ) {
-       $count++;
-       warn "Test 19 e: $@";
-    }
-
-    @res = undef;
-    $fail = 0;
-    eval {
-       @res = PVE::Storage::path($cfg, "$storagename:$ctbase\/$ctlinked");
-       if ($res[0] ne "\/$mountpoint\/$ctlinked") {
-           $count++;
-           $fail = 1;
-           warn "Test 19 f: path is not correct: expected \'\/$mountpoint\/$ctlinked'\  get \'$res[0]\'";
-       }
-       if ($res[1] ne "201") {
-           if (!$fail) {
-               $count++;
-               $fail = 1;
-           }
-           warn "Test 19 f: owner is not correct: expected \'201\'  get \'$res[1]\'";
-       }
-       if ($res[2] ne "images") {
-           if (!$fail) {
-               $count++;
-               $fail = 1;
-           }
-           warn "Test 19 f: owner is not correct: expected \'images\'  get \'$res[2]\'";
-       }
-    };
-    if ( $@ ) {
-       $count++;
-       warn "Test 19 f: $@";
-    }
-};
-$tests->{19} = $test19;
-
-my $test18 = sub {
-
-    print "\nrun test18 \"scan_zfs\"\n";
-    my $res;
-
-    eval {
-       $res = PVE::Storage::scan_zfs($cfg, $storagename);
-
-       my $exists = 0;
-       foreach my $subvol (@$res){
-           if ($subvol->{pool} eq 'regressiontest') {
-               $exists++;
-           }
-       }
-       if (!$exists) {
-           $count++;
-           warn "Test 18 a: not pool";
-       }
-    };
-    if ( $@ ) {
-       $count++;
-       warn "Test 18 a: $@";
-    }
-    $res = undef;
-
-    eval {
-       $res = PVE::Storage::scan_zfs($cfg, $storagename);
-       
-       foreach my $subvol (@$res){
-           if ($subvol->{pool} eq 'zfspool/subvol') {
-               $count++;
-               warn "Test 18 b:";
-           }
-       }
-
-       foreach my $subvol (@$res){
-           if ($subvol->{pool} eq 'zfspool/basevol') {
-               $count++;
-               warn "Test 18 c";
-           }
-       }
-    };
-    if ( $@ ) {
-       $count++;
-       warn "Test 18 a: $@";
-    }
-};
-$tests->{18} = $test18;
-
-my $test17 = sub {
-
-    print "\nrun test17 \"deactivate_storage\"\n";
-
-    eval {
-       PVE::Storage::activate_storage($cfg, $storagename);
-       PVE::Storage::deactivate_storage($cfg, $storagename);
-    };
-    if ($@) {
-       $count++;
-       warn "Test 17 a: $@";
-    }
-};
-$tests->{17} = $test17;
-
-my $test16 = sub {
-
-    print "\nrun test16 \"activate_storage\"\n";
-
-    eval {
-       PVE::Storage::activate_storage($cfg, $storagename);
-    };
-    if ($@) {
-       $count++;
-       warn "Test 16 a: $@";
-    }
-};
-$tests->{16} = $test16;
-
-my $test15 = sub {
-
-    print "\nrun test15 \"template_list and vdisk_list\"\n";
-
-    my $hash = Dumper {};
-
-    my $res = Dumper PVE::Storage::template_list($cfg, $storagename, "vztmpl");
-    if ( $hash ne $res ) {
-       $count++;
-       warn "Test 15 a failed\n";
-    }
-    $res = undef;
-
-    $res = Dumper PVE::Storage::template_list($cfg, $storagename, "iso");
-    if ( $hash ne $res ) {
-       $count++;
-       warn "Test 15 b failed\n";
-    }
-    $res = undef;
-
-    $res = Dumper PVE::Storage::template_list($cfg, $storagename, "backup");
-    if ( $hash ne $res ) {
-       $count++;
-       warn "Test 15 c failed\n";
-    }
-    $res = undef;
-
-    $hash = Dumper {'zfstank99' => [
-                       {
-                           'parent' => undef,
-                           'volid' => 'zfstank99:base-100-disk-1',
-                           'name' => 'base-100-disk-1',
-                           'vmid' => '100',
-                           'size' => 1073741824,
-                           'format' => 'raw'
-                       }
-                       ]};
-
-    $res = Dumper PVE::Storage::vdisk_list($cfg, $storagename, 100, ["$storagename:$vmbase"]);
-
-    if ( $hash ne $res ) {
-       $count++;
-       warn "Test 15 d failed\n";
-    }
-    $res = undef;
-
-    $hash = Dumper {'zfstank99' => [
-                       {
-                           'parent' => undef,
-                           'volid' => 'zfstank99:vm-102-disk-1',
-                           'name' => 'vm-102-disk-1',
-                           'vmid' => '102',
-                           'size' => 1073741824,
-                           'format' => 'raw'
-                       }
-                       ]};
-
-    $res = Dumper PVE::Storage::vdisk_list($cfg, $storagename, 102, ["$storagename:$vmdisk"]);
-    if ( $hash ne $res ) {
-       $count++;
-       warn "Test 15 e failed\n";
-    }
-    $res = undef;
-
-    $hash = Dumper {'zfstank99' => [
-                       {
-                           'parent' => 'base-100-disk-1@__base__',
-                           'volid' => "$storagename:$vmbase\/$vmlinked",
-                           'name' => 'vm-101-disk-1',
-                           'vmid' => '101',
-                           'size' => 1073741824,
-                           'format' => 'raw'
-                       }
-                       ]};
-
-    $res =  Dumper PVE::Storage::vdisk_list($cfg, $storagename, 101, ["$storagename:$vmbase\/$vmlinked"]);
-    if ( $hash ne $res ) {
-       $count++;
-       warn "Test 15 f failed\n";
-    }
-    $res = undef;
-
-    $hash = Dumper {'zfstank99' => [
-                       {
-                           'parent' => undef,
-                           'volid' => 'zfstank99:basevol-200-disk-1',
-                           'name' => 'basevol-200-disk-1',
-                           'vmid' => '200',
-                           'size' => 1073741824,
-                           'format' => 'subvol'
-                       }
-                       ]};
-
-    $res =  Dumper PVE::Storage::vdisk_list($cfg, $storagename, 200, ["$storagename:$ctbase"]);
-    if ( $hash ne $res ) {
-       $count++;
-       warn "Test 15 g failed\n";
-    }
-    $res = undef;
-
-    $hash = Dumper {'zfstank99' => [
-                       {
-                           'parent' => undef,
-                           'volid' => 'zfstank99:subvol-202-disk-1',
-                           'name' => 'subvol-202-disk-1',
-                           'vmid' => '202',
-                           'size' => 1073741824,
-                           'format' => 'subvol'
-                       }
-                       ]};
-
-    $res = Dumper PVE::Storage::vdisk_list($cfg, $storagename, 202, ["$storagename:$ctdisk"]);
-    if ( $hash ne $res ) {
-       $count++;
-       warn "Test 15 h failed\n";
-    }
-    $res = undef;
-
-    $hash = Dumper {'zfstank99' => [
-                       {
-                           'parent' => 'basevol-200-disk-1@__base__',
-                           'volid' => "$storagename:$ctbase\/$ctlinked",
-                           'name' => 'subvol-201-disk-1',
-                           'vmid' => '201',
-                           'size' => 1073741824,
-                           'format' => 'subvol'
-                       }
-                       ]};
-    $res = Dumper PVE::Storage::vdisk_list($cfg, $storagename, 201, ["$storagename:$ctbase\/$ctlinked"]);
-    if ( $hash ne $res ) {
-       $count++;
-       warn "Test 15 i failed\n";
-    }
-};
-$tests->{15} = $test15;
-
-my $test14 = sub {
-
-    print "\nrun test14 \"vdisk_free\"\n";
-
-    eval {
-       PVE::Storage::vdisk_free($cfg, "$storagename:$vmdisk");
-
-       eval {
-           run_command("zfs list $zpath\/$vmdisk", outfunc => sub {}, errfunc => sub {});
-       };
-       if (!$@) {
-           $count++;
-           warn "Test14 a: vdisk still exists\n";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test14 a: $@";
-    }
-
-    eval {
-       PVE::Storage::vdisk_free($cfg, "$storagename:$vmbase");
-    };
-    if (!$@) {
-       $count++;
-       warn "Test14 b: free vdisk should not work\n";
-    }
-
-    eval {
-       PVE::Storage::vdisk_free($cfg, "$storagename:$vmbase\/$vmlinked");
-
-       eval {
-           run_command("zfs list $zpath\/$vmlinked", outfunc => sub {}, errfunc => sub {});
-       };
-       if (!$@) {
-           $count++;
-           warn "Test14 c: vdisk still exists\n";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test14 c: $@";
-    }
-
-    eval {
-       PVE::Storage::vdisk_free($cfg, "$storagename:$ctdisk");
-
-       eval {
-           run_command("zfs list $zpath\/$ctdisk", outfunc => sub {}, errfunc => sub {});
-       };
-       if (!$@) {
-           $count++;
-           warn "Test14 d: vdisk still exists\n";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test14 d: $@";
-    }
-
-    eval {
-       PVE::Storage::vdisk_free($cfg, "$storagename:$ctbase");
-    };
-    if (!$@) {
-       $count++;
-       warn "Test14 e: free vdisk should not work\n";
-    }
-
-    eval {
-       PVE::Storage::vdisk_free($cfg, "$storagename:$ctbase\/$ctlinked");
-
-       eval {
-           run_command("zfs list $zpath\/$ctlinked", outfunc => sub {}, errfunc => sub {});
-       };
-       if (!$@) {
-           $count++;
-           warn "Test14 f: vdisk still exists\n";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test14 f: $@";
-    }
-
-    eval {
-       PVE::Storage::vdisk_free($cfg, "$storagename:$vmbase");
-
-       eval {
-           run_command("zfs list $zpath\/$vmbase", outfunc => sub {}, errfunc => sub {});
-       };
-       if (!$@) {
-           $count++;
-           warn "Test14 g: vdisk still exists\n";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test14 g: $@";
-    }
-
-    eval {
-       PVE::Storage::vdisk_free($cfg, "$storagename:$ctbase");
-
-       eval {
-           run_command("zfs list $zpath\/$ctbase", outfunc => sub {}, errfunc => sub {});
-       };
-       if (!$@) {
-           $count++;
-           warn "Test14 h: vdisk still exists\n";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test14 h: $@";
-    }
-};
-$tests->{14} = $test14;
-
-my $test13 = sub {
-
-    print "\nrun test13 \"vdisk_alloc\"\n";
-
-    eval {
-       my $tmp_volid = PVE::Storage::vdisk_alloc($cfg, $storagename, "112", "raw", undef ,1024 * 1024);
-
-       if ($tmp_volid ne "$storagename:vm-112-disk-0") {
-           die "volname:$tmp_volid don't match\n";
-       }
-       eval {
-           run_command("zfs get -H volsize $zpath\/vm-112-disk-0", outfunc =>
-                       sub { my $tmp = shift;
-                             if ($tmp !~ m/^$zpath\/vm-112-disk-0.*volsize.*1G.*$/) {
-                                 die "size don't match\n";
-                             }
-                       });
-       };
-       if ($@) {
-           $count++;
-           warn "Test13 a: $@";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test13 a: $@";
-    }
-
-    eval {
-       my $tmp_volid = PVE::Storage::vdisk_alloc($cfg, $storagename, "112", "raw", undef ,2048 * 1024);
-
-       if ($tmp_volid ne "$storagename:vm-112-disk-1") {
-           die "volname:$tmp_volid don't match\n";
-       }
-       eval {
-           run_command("zfs get -H volsize $zpath\/vm-112-disk-1", outfunc =>
-                       sub { my $tmp = shift;
-                             if ($tmp !~ m/^$zpath\/vm-112-disk-1.*volsize.*2G.*$/) {
-                                 die "size don't match\n";
-                             }
-                       });
-       };
-       if ($@) {
-           $count++;
-           warn "Test13 b: $@";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test13 b: $@";
-    }
-
-    eval {
-       my $tmp_volid = PVE::Storage::vdisk_alloc($cfg, $storagename, "113", "subvol", undef ,1024 * 1024);
-
-       if ($tmp_volid ne "$storagename:subvol-113-disk-0") {
-           die "volname:$tmp_volid  don't match\n";
-       }
-       eval {
-           run_command("zfs get -H refquota $zpath\/subvol-113-disk-0", outfunc =>
-                       sub { my $tmp = shift;
-                             if ($tmp !~ m/^$zpath\/subvol-113-disk-0.*refquota.*1G.*$/) {
-                                 die "size don't match\n";
-                             }
-                       });
-       };
-       if ($@) {
-           $count++;
-           warn "Test13 c: $@";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test13 c: $@";
-    }
-
-    eval {
-       my $tmp_volid = PVE::Storage::vdisk_alloc($cfg, $storagename, "113", "subvol", undef ,2048 * 1024);
-
-       if ($tmp_volid ne "$storagename:subvol-113-disk-1") {
-           die "volname:$tmp_volid  don't match\n";
-       }
-       eval {
-           run_command("zfs get -H refquota $zpath\/subvol-113-disk-1", outfunc =>
-                       sub { my $tmp = shift;
-                             if ($tmp !~ m/^$zpath\/subvol-113-disk-1.*refquota.*G.*$/) {
-                                 die "size don't match\n";
-                             }
-                       });
-       };
-       if ($@) {
-           $count++;
-           warn "Test13 d: $@";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test13 d: $@";
-    }
-};
-$tests->{13} = $test13;
-
-my $test12 = sub {
-
-    print "\nrun test12 \"vdisk_create_base\"\n";
-
-    eval {
-       my $tmp_volid = PVE::Storage::vdisk_create_base($cfg, "$storagename:$vmdisk");
-
-       if ($tmp_volid ne "$storagename:base-102-disk-1") {
-           die;
-       }
-       eval {
-           run_command("zfs list $zpath\/base-102-disk-1", outfunc => sub {});
-       };
-       if ($@) {
-           $count++;
-           warn "Test12 a: $@";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test12 a: $@";
-    }
-
-    eval {
-       my $tmp_volid = PVE::Storage::vdisk_create_base($cfg, "$storagename:$vmlinked");
-
-       if ($tmp_volid ne "$storagename:base-101-disk-1") {
-           die;
-       }
-       eval {
-           run_command("zfs list $zpath\/base-101-disk-1", outfunc => sub {});
-       };
-       if ($@) {
-           $count++;
-           warn "Test12 b: $@";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test12 b: $@";
-    }
-
-    eval {
-       my $tmp_volid = PVE::Storage::vdisk_create_base($cfg, "$storagename:$ctdisk");
-
-       if ($tmp_volid ne "$storagename:basevol-202-disk-1") {
-           die ;
-       }
-       eval {
-           run_command("zfs list $zpath\/basevol-202-disk-1", outfunc => sub {});
-       };
-       if ($@) {
-           $count++;
-           warn "Test12 c: $@";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test12 c: $@";
-    }
-
-    eval {
-       my $tmp_volid = PVE::Storage::vdisk_create_base($cfg, "$storagename:$ctlinked");
-
-       if ($tmp_volid ne "$storagename:basevol-201-disk-1") {
-           die;
-       }
-       eval {
-           run_command("zfs list $zpath\/basevol-201-disk-1", outfunc => sub {});
-       };
-       if ($@) {
-           $count++;
-           warn "Test12 d: $@";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test12 d: $@";
-    }
-};
-$tests->{12} = $test12;
-
-my $test11 = sub {
-
-    print "\nrun test11 \"volume_is_base\"\n";
-
-    eval {
-       PVE::Storage::vdisk_clone($cfg, "$storagename:$vmdisk", 110);
-    };
-    if (!$@) {
-       $count++;
-       warn "Test11 a: clone_image only works on base images";
-    }
-
-    eval {
-       if ("$storagename:$vmbase\/vm-110-disk-0" ne
-           PVE::Storage::vdisk_clone($cfg, "$storagename:$vmbase", 110, '__base__')){
-           $count++;
-           warn  "Test11 b";
-       }
-       run_command("zfs list -H -o volsize $zpath\/vm-110-disk-0", outfunc => sub {
-           my $line = shift;
-
-           chomp($line);
-           warn "Test11 b not correct volsize" if $line !~ m/$volsize/; 
-                   });
-    };
-    if ($@) {
-       $count++;
-       warn "Test11 b: $@";
-    }
-
-    eval {
-       PVE::Storage::vdisk_clone($cfg, "$storagename:$vmbase\/$vmlinked", 111);
-    };
-    if (!$@) {
-       $count++;
-       warn "Test11 c: clone_image only works on base images";
-    }
-
-    eval {
-       PVE::Storage::vdisk_clone($cfg, "$storagename:$ctdisk", 110);
-    };
-    if (!$@) {
-       $count++;
-       warn "Test11 d: clone_image only works on base images";
-    }
-
-    eval {
-       if ( "$storagename:$ctbase\/subvol-210-disk-0" ne
-            PVE::Storage::vdisk_clone($cfg, "$storagename:$ctbase", 210, '__base__')){
-           $count++;
-           warn  "Test11 e";
-       }
-       run_command("zfs list -H -o refquota $zpath\/subvol-210-disk-0", outfunc => sub {
-           my $line = shift;
-
-           chomp($line);
-           warn "Test11 e not correct volsize" if $line !~ m/$volsize/; 
-                   });
-    };
-    if ($@) {
-       $count++;
-       warn "Test11 e: $@";
-    }
-
-    eval {
-       PVE::Storage::vdisk_clone($cfg, "$storagename:$ctbase\/$ctlinked", 211);
-    };
-    if (!$@) {
-       $count++;
-       warn "Test11 f: clone_image only works on base images";
-    }
-};
-$tests->{11} = $test11;
-
-my $test10 =sub {
-
-    print "\nrun test10 \"volume_is_base\"\n";
-
-    eval {
-       if (1 == volume_is_base($cfg, "$storagename:$vmdisk")) {
-           $count++;
-           warn "Test10 a: is no base";
-       }
-
-    };
-    if ($@) {
-       $count++;
-       warn "Test10 a: $@";
-    }
-
-    eval {
-       if (0 == volume_is_base($cfg, "$storagename:$vmbase")) {
-           $count++;
-           warn "Test10 b: is base";
-       }
-
-    };
-    if ($@) {
-       $count++;
-       warn "Test10 b: $@";
-    }
-
-    eval {
-       if (1 == volume_is_base($cfg, "$storagename:$vmbase\/$vmlinked")) {
-           $count++;
-           warn "Test10 c: is no base";
-       }
-
-    };
-    if ($@) {
-       $count++;
-       warn "Test10 c: $@";
-    }
-
-    eval {
-       if (1 == volume_is_base($cfg, "$storagename:$ctdisk")) {
-           $count++;
-           warn "Test10 d: is no base";
-       }
-
-    };
-    if ($@) {
-       $count++;
-       warn "Test10 d: $@";
-    }
-
-    eval {
-       if (0 == volume_is_base($cfg, "$storagename:$ctbase")) {
-           $count++;
-           warn "Test10 e: is base";
-       }
-
-    };
-    if ($@) {
-       $count++;
-       warn "Test10 e: $@";
-    }
-
-    eval {
-       if (1 == volume_is_base($cfg, "$storagename:$ctbase\/$ctlinked")) {
-           $count++;
-           warn "Test10 f: is no base";
-       }
-
-    };
-    if ($@) {
-       $count++;
-       warn "Test10 f: $@";
-    }
-};
-$tests->{10} = $test10;
-
-my $test9 =sub {
-
-    print "\nrun test9 \"parse_volume_id\"\n";
-
-    eval {
-       my ($store, $disk) = PVE::Storage::parse_volume_id("$storagename:$vmdisk");
-
-       if ($store ne $storagename || $disk ne $vmdisk) {
-           $count++;
-           warn "Test9 a: parsing wrong";
-       }
-
-    };
-    if ($@) {
-       $count++;
-       warn "Test9 a: $@";
-    }
-
-    eval {
-       my ($store, $disk) = PVE::Storage::parse_volume_id("$storagename:$vmbase");
-
-       if ($store ne $storagename || $disk ne $vmbase) {
-           $count++;
-           warn "Test9 b: parsing wrong";
-       }
-
-    };
-    if ($@) {
-       $count++;
-       warn "Test9 b: $@";
-    }
-
-    eval {
-       my ($store, $disk) = PVE::Storage::parse_volume_id("$storagename:$vmbase\/$vmlinked");
-
-       if ($store ne $storagename || $disk ne "$vmbase\/$vmlinked") {
-           $count++;
-           warn "Test9 c: parsing wrong";
-       }
-
-    };
-    if ($@) {
-       $count++;
-       warn "Test9 c: $@";
-    }
-
-    eval {
-       my ($store, $disk) = PVE::Storage::parse_volume_id("$storagename:$ctdisk");
-
-       if ($store ne $storagename || $disk ne $ctdisk) {
-           $count++;
-           warn "Test9 d: parsing wrong";
-       }
-
-    };
-    if ($@) {
-       $count++;
-       warn "Test9 d: $@";
-    }
-
-    eval {
-       my ($store, $disk) = PVE::Storage::parse_volume_id("$storagename:$ctbase");
-
-       if ($store ne $storagename || $disk ne $ctbase) {
-           $count++;
-           warn "Test9 e: parsing wrong";
-       }
-
-    };
-    if ($@) {
-       $count++;
-       warn "Test9 e: $@";
-    }
-
-    eval {
-       my ($store, $disk) = PVE::Storage::parse_volume_id("$storagename:$ctbase\/$ctlinked");
-
-       if ($store ne $storagename || $disk ne "$ctbase\/$ctlinked") {
-           $count++;
-           warn "Test9 f: parsing wrong";
-       }
-
-    };
-    if ($@) {
-       $count++;
-       warn "Test9 f: $@";
-    }
-};
-$tests->{9} = $test9;
-
-my $test8 = sub {
-
-    print "\nrun test8 \"parse_volname\"\n";
-
-    eval {
-       my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) = PVE::Storage::parse_volname($cfg, "$storagename:$vmdisk");
-
-       if ($vtype ne 'images' || $vmid ne '102' ||  $name ne $vmdisk ||
-           defined($basename) || defined($basevmid) || $isBase ||
-           $format ne 'raw') {
-           $count++;
-           warn "Test8 a: parsing wrong";
-       }
-
-    };
-    if ($@) {
-       $count++;
-       warn "Test8 a: $@";
-    }
-
-    eval {
-       my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) = PVE::Storage::parse_volname($cfg, "$storagename:$vmbase");
-
-       if ($vtype ne 'images' || $vmid ne '100' ||  $name ne $vmbase ||
-           defined($basename) || defined($basevmid) || !$isBase ||
-           $format ne 'raw') {
-           $count++;
-           warn "Test8 b: parsing wrong";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test8 b: $@";
-    }
-
-    eval {
-       my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) = PVE::Storage::parse_volname($cfg, "$storagename:$vmbase\/$vmlinked");
-
-       if ($vtype ne 'images' ||  $name ne $vmlinked || $vmid ne '101' ||
-           $basename ne $vmbase || $basevmid ne '100' || $isBase ||
-           $format ne 'raw') {
-           $count++;
-           warn "Test8 c: parsing wrong";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test8 c: $@";
-    }
-
-    eval {
-       my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) = PVE::Storage::parse_volname($cfg, "$storagename:$ctdisk");
-
-       if ($vtype ne 'images' || $vmid ne '202' ||  $name ne $ctdisk ||
-           defined($basename) || defined($basevmid) || $isBase ||
-           $format ne 'subvol') {
-           $count++;
-           warn "Test8 d: parsing wrong";
-       }
-
-    };
-    if ($@) {
-       $count++;
-       warn "Test8 d: $@";
-    }
-
-    eval {
-       my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) = PVE::Storage::parse_volname($cfg, "$storagename:$ctbase");
-       if ($vtype ne 'images' || $vmid ne '200' ||  $name ne $ctbase ||
-           defined($basename) || defined($basevmid) || !$isBase ||
-           $format ne 'subvol') {
-           $count++;
-           warn "Test8 e: parsing wrong";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test8 e: $@";
-    }
-
-    eval {
-       my ($vtype, $name, $vmid, $basename, $basevmid, $isBase, $format) = PVE::Storage::parse_volname($cfg, "$storagename:$ctbase\/$ctlinked");
-
-       if ($vtype ne 'images' ||  $name ne $ctlinked || $vmid ne '201' ||
-           $basename ne $ctbase || $basevmid ne '200' || $isBase ||
-           $format ne 'subvol') {
-           $count++;
-           warn "Test8 f: parsing wrong";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test8 f: $@";
-    }
-};
-$tests->{8} = $test8;
-
-my $test7 = sub {
-
-    print "\nrun test7 \"volume_rollback\"\n";
-
-    my $tmp_guid;
-    my $parse_guid = sub {
-       my ($line) = shift;
-
-       if ( $line =~ m/^Disk identifier \(GUID\)\: (.*)$/ ) {
-           $tmp_guid = $1;
-       }
-    };
-
-    eval {
-       PVE::Storage::activate_volumes($cfg, ["$storagename:$vmdisk"]);
-       run_command("sgdisk --randomize-guids \/dev\/zvol\/$zpath\/$vmdisk", outfunc => $parse_guid);
-       run_command("sgdisk -p \/dev\/zvol\/$zpath\/$vmdisk", outfunc => $parse_guid);
-
-       my $old_guid = $tmp_guid;
-       PVE::Storage::volume_snapshot($cfg, "$storagename:$vmdisk", 'snap1');
-
-       run_command("sgdisk --randomize-guids \/dev\/zvol\/$zpath\/$vmdisk", outfunc => $parse_guid);
-       eval {
-           PVE::Storage::volume_snapshot_rollback($cfg, "$storagename:$vmdisk", 'snap1');
-           PVE::Storage::activate_volumes($cfg, ["$storagename:$vmdisk"]);
-           $tmp_guid = undef;
-           run_command("sgdisk -p \/dev\/zvol\/$zpath\/$vmdisk", outfunc => $parse_guid);
-           if ($old_guid ne $tmp_guid) {
-               $count++;
-               warn "Test7 a: Zvol makes no rollback";
-           }
-       };
-       if ($@) {
-           $count++;
-           warn "Test7 a: $@";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test7 a: $@";
-    }
-    $tmp_guid = undef;
-
-    eval {
-       PVE::Storage::activate_volumes($cfg, ["$storagename:$vmbase"]);
-       run_command("sgdisk --randomize-guids \/dev\/zvol\/$zpath\/$vmbase", outfunc => $parse_guid);
-       run_command("sgdisk -p \/dev\/zvol\/$zpath\/$vmbase", outfunc => $parse_guid);
-
-       my $old_guid = $tmp_guid;
-       PVE::Storage::volume_snapshot($cfg, "$storagename:$vmbase", 'snap1');
-
-       run_command("sgdisk --randomize-guids \/dev\/zvol\/$zpath\/$vmbase", outfunc => $parse_guid);
-       eval {
-           PVE::Storage::volume_snapshot_rollback($cfg, "$storagename:$vmbase", 'snap1');
-           PVE::Storage::activate_volumes($cfg, ["$storagename:$vmbase"]);
-           $tmp_guid = undef;
-           run_command("sgdisk -p \/dev\/zvol\/$zpath\/$vmbase", outfunc => $parse_guid);
-           if ($old_guid ne $tmp_guid) {
-               $count++;
-               warn "Test7 b: Zvol makes no rollback";
-           }
-       };
-       if ($@) {
-           $count++;
-           warn "Test7 b: $@";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test7 b: $@";
-    }
-    $tmp_guid = undef;
-
-    eval {
-       PVE::Storage::activate_volumes($cfg, ["$storagename:$vmbase/$vmlinked"]);
-       run_command("sgdisk --randomize-guids \/dev\/zvol\/$zpath\/$vmlinked", outfunc => $parse_guid);
-       run_command("sgdisk -p \/dev\/zvol\/$zpath\/$vmlinked", outfunc => $parse_guid);
-
-       my $old_guid = $tmp_guid;
-       PVE::Storage::volume_snapshot($cfg, "$storagename:$vmbase\/$vmlinked", 'snap1');
-
-       run_command("sgdisk --randomize-guids \/dev\/zvol\/$zpath\/$vmlinked", outfunc => $parse_guid);
-       eval {
-           PVE::Storage::volume_snapshot_rollback($cfg, "$storagename:$vmbase\/$vmlinked", 'snap1');
-           PVE::Storage::activate_volumes($cfg, ["$storagename:$vmbase/$vmlinked"]);
-           $tmp_guid = undef;
-           run_command("sgdisk -p \/dev\/zvol\/$zpath\/$vmlinked", outfunc => $parse_guid);
-           if ($old_guid ne $tmp_guid) {
-               $count++;
-               warn "Test7 c: Zvol makes no rollback";
-           }
-       };
-       if ($@) {
-           $count++;
-           warn "Test7 c: $@";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test7 c: $@";
-    }
-    $tmp_guid = undef;
-
-    eval {
-       PVE::Storage::volume_snapshot($cfg, "$storagename:$ctdisk", 'snap1');
-
-       run_command("touch \/$mountpoint\/$ctdisk\/test.txt", outfunc => $parse_guid);
-       eval {
-           PVE::Storage::volume_snapshot_rollback($cfg, "$storagename:$ctdisk", 'snap1');
-           eval {
-               run_command("ls \/$mountpoint\/$ctdisk\/test.txt", errofunc => sub {});
-           };
-           if (!$@) {
-               $count++;
-               warn "Test7 d: $@";
-           }
-       };
-       if ($@) {
-           $count++;
-           warn "Test7 d: $@";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test7 d: $@";
-    }
-
-    eval {
-       PVE::Storage::volume_snapshot($cfg, "$storagename:$ctbase", 'snap1');
-
-       run_command("touch \/$mountpoint\/$ctbase\/test.txt", outfunc => $parse_guid);
-       eval {
-           PVE::Storage::volume_snapshot_rollback($cfg, "$storagename:$ctbase", 'snap1');
-           eval {
-               run_command("ls \/$mountpoint\/$ctbase\/test.txt", errofunc => sub {});
-           };
-           if (!$@) {
-               $count++;
-               warn "Test7 e: $@";
-           }
-       };
-       if ($@) {
-           $count++;
-           warn "Test7 e: $@";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test7 f: $@";
-    }
-
-    eval {
-       PVE::Storage::volume_snapshot($cfg, "$storagename:$ctbase/$ctlinked", 'snap1');
-
-       run_command("touch \/$mountpoint\/$ctlinked\/test.txt", outfunc => $parse_guid);
-       eval {
-           PVE::Storage::volume_snapshot_rollback($cfg, "$storagename:$ctbase/$ctlinked", 'snap1');
-           eval {
-               run_command("ls \/$zpath\/$ctlinked\/test.txt", errofunc => sub {});
-           };
-           if (!$@) {
-               $count++;
-               warn "Test7 g: $@";
-           }
-       };
-       if ($@) {
-           $count++;
-           warn "Test7 g: $@";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test7 g: $@";
-    }
-
-    eval {
-       PVE::Storage::volume_snapshot($cfg, "$storagename:$vmdisk", 'snap2');
-
-       eval {
-           PVE::Storage::volume_snapshot_rollback($cfg, "$storagename:$vmdisk", 'snap1');
-       };
-       if (!$@) {
-           $count++;
-           warn "Test7 h: Not allowed to rollback";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test7 h: $@";
-    }
-
-    eval {
-       PVE::Storage::volume_snapshot($cfg, "$storagename:$vmbase", 'snap2');
-
-       eval {
-           PVE::Storage::volume_snapshot_rollback($cfg, "$storagename:$vmbase", 'snap1');
-       };
-       if (!$@) {
-           $count++;
-           warn "Test7 i: Not allowed to rollback";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test7 i: $@";
-    }
-
-    eval {
-       PVE::Storage::volume_snapshot($cfg, "$storagename:$vmbase\/$vmlinked", 'snap2');
-
-       eval {
-           PVE::Storage::volume_snapshot_rollback($cfg, "$storagename:$vmbase\/$vmlinked", 'snap1');
-       };
-       if (!$@) {
-           $count++;
-           warn "Test7 j: Not allowed to rollback";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test7 j: $@";
-    }
-
-    eval {
-       PVE::Storage::volume_snapshot($cfg, "$storagename:$ctdisk", 'snap2');
-
-       eval {
-           PVE::Storage::volume_snapshot_rollback($cfg, "$storagename:$ctdisk", 'snap1');
-       };
-       if (!$@) {
-           $count++;
-           warn "Test7 k: Not allowed to rollback";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test7 k: $@";
-    }
-
-    eval {
-       PVE::Storage::volume_snapshot($cfg, "$storagename:$ctbase", 'snap2');
-
-       eval {
-           PVE::Storage::volume_snapshot_rollback($cfg, "$storagename:$ctbase", 'snap1');
-       };
-       if (!$@) {
-           $count++;
-           warn "Test7 l: Not allowed to rollback";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test7 l: $@";
-    }
-
-    eval {
-       PVE::Storage::volume_snapshot($cfg, "$storagename:$ctbase/$ctlinked", 'snap2');
-
-       eval {
-           PVE::Storage::volume_snapshot_rollback($cfg, "$storagename:$ctbase/$ctlinked", 'snap1');
-       };
-       if (!$@) {
-           $count++;
-           warn "Test7 m: Not allowed to rollback";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test7 m: $@";
-    }
-};
-$tests->{7} = $test7;
-
-my $test6 = sub {
-
-    print "\nrun test6 \"volume_rollback_is_possible\"\n";
-
-    eval {
-       PVE::Storage::volume_snapshot($cfg, "$storagename:$vmdisk", 'snap1');
-
-       my $blockers = [];
-       my $res = PVE::Storage::volume_rollback_is_possible(
-           $cfg,
-           "$storagename:$vmdisk",
-           'snap1',
-           $blockers,
-       );
-       if ($res != 1) {
-           $count++;
-           warn "Test6 a: Rollback should be possible";
-       }
-       if (scalar($blockers->@*) != 0) {
-           $count++;
-           warn "Test6 a: 'blockers' should be empty";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test6 a: $@";
-    }
-
-    eval {
-       PVE::Storage::volume_snapshot($cfg, "$storagename:$vmbase", 'snap1');
-       if ( 1 !=
-            PVE::Storage::volume_rollback_is_possible($cfg, "$storagename:$vmbase", 'snap1')) {
-           $count++;
-           warn "Test6 b: Rollback should be possible";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test6 b: $@";
-    }
-
-    eval {
-       PVE::Storage::volume_snapshot($cfg, "$storagename:$vmlinked", 'snap1');
-       if ( 1 !=
-            PVE::Storage::volume_rollback_is_possible($cfg, "$storagename:$vmbase\/$vmlinked", 'snap1')) {
-           $count++;
-           warn "Test6 c: Rollback should be possible";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test6 c: $@";
-    }
-
-    eval {
-       PVE::Storage::volume_snapshot($cfg, "$storagename:$ctdisk", 'snap1');
-       if ( 1 !=
-            PVE::Storage::volume_rollback_is_possible($cfg, "$storagename:$ctdisk", 'snap1')) {
-           $count++;
-           warn "Test6 d: Rollback should be possible";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test6 d: $@";
-    }
-
-    eval {
-       PVE::Storage::volume_snapshot($cfg, "$storagename:$ctbase", 'snap1');
-       if ( 1 !=
-            PVE::Storage::volume_rollback_is_possible($cfg, "$storagename:$ctbase", 'snap1')) {
-           $count++;
-           warn "Test6 e: Rollback should be possible";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test6 e: $@";
-    }
-
-    eval {
-       PVE::Storage::volume_snapshot($cfg, "$storagename:$ctlinked", 'snap1');
-       if ( 1 !=
-            PVE::Storage::volume_rollback_is_possible($cfg, "$storagename:$ctbase\/$ctlinked", 'snap1')) {
-           $count++;
-           warn "Test6 f: Rollback should be possible";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test6 f: $@";
-    }
-
-    my $blockers = [];
-    eval {
-       PVE::Storage::volume_snapshot($cfg, "$storagename:$vmdisk", 'snap2');
-       PVE::Storage::volume_rollback_is_possible($cfg, "$storagename:$vmdisk", 'snap1', $blockers);
-    };
-    if (!$@) {
-       $count++;
-       warn "Test6 g: Rollback should not be possible";
-    } elsif (scalar($blockers->@*) != 1 || $blockers->[0] ne 'snap2') {
-       $count++;
-       warn "Test6 g: 'blockers' should be ['snap2']";
-    }
-    undef $blockers;
-
-    $blockers = [];
-    eval {
-       PVE::Storage::volume_snapshot($cfg, "$storagename:$vmbase", 'snap2');
-       PVE::Storage::volume_snapshot($cfg, "$storagename:$vmbase", 'snap3');
-       PVE::Storage::volume_rollback_is_possible($cfg, "$storagename:$vmbase", 'snap1', $blockers);
-    };
-    if (!$@) {
-       $count++;
-       warn "Test6 h: Rollback should not be possible";
-    } else {
-       if (scalar($blockers->@*) != 2) {
-           $count++;
-           warn "Test6 g: 'blockers' should contain two elements";
-       }
-       my $blockers_hash = { map { $_ => 1 } $blockers->@* };
-       if (!$blockers_hash->{'snap2'}) {
-           $count++;
-           warn "Test6 g: 'blockers' should contain 'snap2'";
-       }
-       if (!$blockers_hash->{'snap3'}) {
-           $count++;
-           warn "Test6 g: 'blockers' should contain 'snap3'";
-       }
-    }
-    undef $blockers;
-
-    eval {
-       PVE::Storage::volume_snapshot($cfg, "$storagename:$vmlinked", 'snap2');
-       PVE::Storage::volume_rollback_is_possible($cfg, "$storagename:$vmbase\/$vmlinked", 'snap1');
-    };
-    if (!$@) {
-       $count++;
-       warn "Test6 j: Rollback should not be possible";
-    }
-
-    eval {
-       PVE::Storage::volume_snapshot($cfg, "$storagename:$ctdisk", 'snap2');
-       PVE::Storage::volume_rollback_is_possible($cfg, "$storagename:$ctdisk", 'snap1');
-    };
-    if (!$@) {
-       $count++;
-       warn "Test6 k: Rollback should not be possible";
-    }
-
-    eval {
-       PVE::Storage::volume_snapshot($cfg, "$storagename:$ctbase", 'snap2');
-        PVE::Storage::volume_rollback_is_possible($cfg, "$storagename:$ctbase", 'snap1');
-    };
-    if (!$@) {
-       $count++;
-       warn "Test6 l: Rollback should not be possible";
-    }
-
-    eval {
-       PVE::Storage::volume_snapshot($cfg, "$storagename:$ctlinked", 'snap2');
-       PVE::Storage::volume_rollback_is_possible($cfg, "$storagename:$ctbase\/$ctlinked", 'snap1');
-    };
-    if (!$@) {
-       $count++;
-       warn "Test6 m: Rollback should not be possible";
-    }
-};
-$tests->{6} = $test6;
-
-my $test5 = sub {
-
-    print "\nrun test5 \"volume_snapshot_delete\"\n";
-    my $out = sub{my $tmp = shift;};
-
-    eval {
-       run_command("zfs snapshot $zpath\/$vmdisk\@snap");
-       eval{
-           PVE::Storage::volume_snapshot_delete($cfg, "$storagename:$vmdisk", 'snap');
-           eval{
-               run_command("zfs list $zpath\/$vmdisk\@snap", errfunc => $out, outfunc => $out);
-           };
-           if (!$@) {
-               $count++;
-               warn "Test5 a: snapshot still exists";
-           }
-       };
-       if ($@) {
-           $count++;
-           warn "Test5 PVE a: $@";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test5 a: $@";
-    }
-
-    eval {
-       run_command("zfs snapshot $zpath\/$vmbase\@snap");
-       eval{
-           PVE::Storage::volume_snapshot_delete($cfg, "$storagename:$vmbase", 'snap');
-           eval{
-               run_command("zfs list $zpath\/$vmbase\@snap", errmsg => $out, outfunc => $out);
-           };
-           if (!$@) {
-               $count++;
-               warn "Test5 b: snapshot still exists";
-           }
-       };
-       if ($@) {
-           $count++;
-           warn "Test5 PVE b: $@";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test5 b: $@";
-    }
-
-    eval {
-       run_command("zfs snapshot $zpath\/$vmlinked\@snap");
-       eval{
-           PVE::Storage::volume_snapshot_delete($cfg, "$storagename:$vmbase\/$vmlinked", 'snap');
-           eval{
-               run_command("zfs list $zpath\/$vmlinked\@snap", errmsg => $out, outfunc => $out);
-           };
-           if (!$@) {
-               $count++;
-               warn "Test5 c: snapshot still exists";
-           }
-       };
-       if ($@) {
-           $count++;
-           warn "Test5 PVE c: $@";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test5 c: $@";
-    }
-
-    eval {
-       run_command("zfs snapshot $zpath\/$ctdisk\@snap");
-       eval{
-           PVE::Storage::volume_snapshot_delete($cfg, "$storagename:$ctdisk", 'snap');
-           eval{
-               run_command("zfs list $zpath\/$ctdisk\@snap", errmsg => $out, outfunc => $out);
-           };
-           if (!$@) {
-               $count++;
-               warn "Test5 d: snapshot still exists";
-           }
-       };
-       if ($@) {
-           $count++;
-           warn "Test5 PVE d: $@";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test5 d: $@";
-    }
-
-    eval {
-       run_command("zfs snapshot $zpath\/$ctbase\@snap");
-       eval{
-           PVE::Storage::volume_snapshot_delete($cfg, "$storagename:$ctbase", 'snap');
-           eval{
-               run_command("zfs list $zpath\/$ctbase\@snap", errmsg => $out, outfunc => $out);
-           };
-           if (!$@) {
-               $count++;
-               warn "Test5 e: snapshot still exists";
-           }
-       };
-       if ($@) {
-           $count++;
-           warn "Test5 PVE e: $@";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test5 e: $@";
-    }
-
-    eval {
-       run_command("zfs snapshot $zpath\/$ctlinked\@snap");
-       eval{
-           PVE::Storage::volume_snapshot_delete($cfg, "$storagename:$ctbase\/$ctlinked", 'snap');
-           eval{
-               run_command("zfs list $zpath\/$ctlinked\@snap", errmsg => $out, outfunc => $out);
-           };
-           if (!$@) {
-               $count++;
-               warn "Test5 f: snapshot still exists";
-           }
-       };
-       if ($@) {
-           $count++;
-           warn "Test5 PVE f: $@";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test5 f: $@";
-    }
-    print "######Ignore Output if no Test5 g: is included######\n";
-    eval{
-       PVE::Storage::volume_snapshot_delete($cfg, "$storagename:$vmbase", '__base__');
-       eval{
-           run_command("zfs list $zpath\/$vmbase\@__base__", outfunc => $out);
-       };
-       if ($@) {
-           $count++;
-           warn "Test5 g: $@";
-       }
-    };
-    if (!$@) {
-       $count++;
-       warn "Test5 PVE g: snapshot __base__ can be erased";
-    }
-    print "######End Ignore#######\n";
-};
-$tests->{5} = $test5;
-
-my $test4 = sub {
-
-    print "\nrun test4 \"volume_snapshot\"\n";
-    my $out = sub{};
-
-    eval {
-       PVE::Storage::volume_snapshot($cfg, "$storagename:$vmdisk", 'snap');
-       eval{
-           run_command("zfs list $zpath\/$vmdisk\@snap", errmsg => $out, outfunc => $out);
-       };
-       if ($@) {
-           $count++;
-           warn "Test4 a: $@";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test4 a: $@";
-    }
-
-    eval {
-       PVE::Storage::volume_snapshot($cfg, "$storagename:$vmbase", 'snap');
-       eval{
-           run_command("zfs list $zpath\/$vmbase\@snap", errmsg => $out, outfunc => $out);
-       };
-       if ($@) {
-           $count++;
-           warn "Test4 b: $@";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test4 c: $@";
-    }
-
-    eval {
-       PVE::Storage::volume_snapshot($cfg, "$storagename:$vmbase\/$vmlinked", 'snap');
-       eval{
-           run_command("zfs list $zpath\/$vmdisk\@snap", errmsg => $out, outfunc => $out);
-       };
-       if ($@) {
-           $count++;
-           warn "Test4 c: $@";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test4 c: $@";
-    }
-
-    eval {
-       PVE::Storage::volume_snapshot($cfg, "$storagename:$ctdisk", 'snap');
-       eval{
-           run_command("zfs list $zpath\/$ctdisk\@snap", errmsg => $out, outfunc => $out);
-       };
-       if ($@) {
-           $count++;
-           warn "Test4 d: $@";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test4 d: $@";
-    }
-
-    eval {
-       PVE::Storage::volume_snapshot($cfg, "$storagename:$ctbase", 'snap');
-       eval{
-           run_command("zfs list $zpath\/$ctbase\@snap", errmsg => $out, outfunc => $out);
-       };
-       if ($@) {
-           $count++;
-           warn "Test4 e: $@";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test4 e: $@";
-    }
-
-    eval {
-       PVE::Storage::volume_snapshot($cfg, "$storagename:$ctbase\/$ctlinked", 'snap');
-       eval{
-           run_command("zfs list $zpath\/$ctdisk\@snap", errmsg => $out, outfunc => $out);
-       };
-       if ($@) {
-           $count++;
-           warn "Test4 f: $@";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test4 f: $@";
-    }
-};
-$tests->{4} = $test4;
-
-my $test3 = sub {
-
-    print "\nrun test3 \"volume_has_feature\"\n";
-
-    eval {
-       if (!PVE::Storage::volume_has_feature($cfg, 'snapshot', "$storagename:$vmdisk", undef, 0)) {
-           $count++;
-           warn "Test3 a failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 a: $@";
-    }
-
-    eval {
-       if (PVE::Storage::volume_has_feature($cfg, 'snapshot', "$storagename:$vmbase", undef, 0)) {
-           $count++;
-           warn "Test3 b failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 b: $@";
-    }
-
-    eval {
-       if (!PVE::Storage::volume_has_feature($cfg, 'snapshot', "$storagename:$vmbase\/$vmlinked", undef, 0)) {
-           $count++;
-           warn "Test3 c failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 c: $@";
-    }
-
-    eval {
-       if (!PVE::Storage::volume_has_feature($cfg, 'snapshot', "$storagename:$ctdisk", undef, 0)) {
-           $count++;
-           warn "Test3 d failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 d: $@";
-    }
-
-    eval {
-       if (PVE::Storage::volume_has_feature($cfg, 'snapshot', "$storagename:$ctbase", undef, 0)) {
-           $count++;
-           warn "Test3 e failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 e: $@";
-    }
-
-    eval {
-       if (!PVE::Storage::volume_has_feature($cfg, 'snapshot', "$storagename:$ctbase\/$ctlinked", undef, 0)) {
-           $count++;
-           warn "Test3 f failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 f: $@";
-    }
-
-    eval {
-       if (PVE::Storage::volume_has_feature($cfg, 'clone', "$storagename:$vmdisk", undef, 0)) {
-           $count++;
-           warn "Test3 g failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 g: $@";
-    }
-
-    eval {
-       if (!PVE::Storage::volume_has_feature($cfg, 'clone', "$storagename:$vmbase", undef, 0)) {
-           $count++;
-           warn "Test3 h failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 h: $@";
-    }
-
-    eval {
-       if (PVE::Storage::volume_has_feature($cfg, 'clone', "$storagename:$vmbase\/$vmlinked", undef, 0)) {
-           $count++;
-           warn "Test3 h failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 h: $@";
-    }
-
-    eval {
-       if (PVE::Storage::volume_has_feature($cfg, 'clone', "$storagename:$ctdisk", undef, 0)) {
-           $count++;
-           warn "Test3 i failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 i: $@";
-    }
-
-    eval {
-       if (!PVE::Storage::volume_has_feature($cfg, 'clone', "$storagename:$ctbase", undef, 0)) {
-           $count++;
-           warn "Test3 j failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 j: $@";
-    }
-
-    eval {
-       if (PVE::Storage::volume_has_feature($cfg, 'clone', "$storagename:$ctbase\/$ctlinked", undef, 0)) {
-           $count++;
-           warn "Test3 k failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 k: $@";
-    }
-
-    eval {
-       if (!PVE::Storage::volume_has_feature($cfg, 'template', "$storagename:$vmdisk", undef, 0)) {
-           $count++;
-           warn "Test3 l failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 l: $@";
-    }
-
-    eval {
-       if (PVE::Storage::volume_has_feature($cfg, 'template', "$storagename:$vmbase", undef, 0)) {
-           $count++;
-           warn "Test3 m failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 m: $@";
-    }
-
-    eval {
-       if (!PVE::Storage::volume_has_feature($cfg, 'template', "$storagename:$vmbase\/$vmlinked", undef, 0)) {
-           $count++;
-           warn "Test3 n failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 n: $@";
-    }
-
-    eval {
-       if (!PVE::Storage::volume_has_feature($cfg, 'template', "$storagename:$ctdisk", undef, 0)) {
-           $count++;
-           warn "Test3 o failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 o: $@";
-    }
-
-    eval {
-       if (PVE::Storage::volume_has_feature($cfg, 'template', "$storagename:$ctbase", undef, 0)) {
-           $count++;
-           warn "Test3 p failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 p: $@";
-    }
-
-    eval {
-       if (!PVE::Storage::volume_has_feature($cfg, 'template', "$storagename:$ctbase\/$ctlinked", undef, 0)) {
-           $count++;
-           warn "Test3 q failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 q: $@";
-    }
-
-    eval {
-       if (!PVE::Storage::volume_has_feature($cfg, 'copy', "$storagename:$vmdisk", undef, 0)) {
-           $count++;
-           warn "Test3 r failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 r: $@";
-    }
-
-    eval {
-       if (!PVE::Storage::volume_has_feature($cfg, 'copy', "$storagename:$vmbase", undef, 0)) {
-           $count++;
-           warn "Test3 s failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 s: $@";
-    }
-
-    eval {
-       if (!PVE::Storage::volume_has_feature($cfg, 'copy', "$storagename:$vmbase\/$vmlinked", undef, 0)) {
-           $count++;
-           warn "Test3 t failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 t: $@";
-    }
-
-    eval {
-       if (!PVE::Storage::volume_has_feature($cfg, 'copy', "$storagename:$ctdisk", undef, 0)) {
-           $count++;
-           warn "Test3 u failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 u: $@";
-    }
-
-    eval {
-       if (!PVE::Storage::volume_has_feature($cfg, 'copy', "$storagename:$ctbase", undef, 0)) {
-           $count++;
-           warn "Test3 v failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 v: $@";
-    }
-
-    eval {
-       if (!PVE::Storage::volume_has_feature($cfg, 'copy', "$storagename:$ctbase\/$ctlinked", undef, 0)) {
-           $count++;
-           warn "Test3 w failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 w: $@";
-    }
-
-    eval {
-       if (!PVE::Storage::volume_has_feature($cfg, 'sparseinit', "$storagename:$vmdisk", undef, 0)) {
-           $count++;
-           warn "Test3 x failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 x: $@";
-    }
-
-    eval {
-       if (!PVE::Storage::volume_has_feature($cfg, 'sparseinit', "$storagename:$vmbase", undef, 0)) {
-           $count++;
-           warn "Test3 y failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 y: $@";
-    }
-
-    eval {
-       if (!PVE::Storage::volume_has_feature($cfg, 'sparseinit', "$storagename:$vmbase\/$vmlinked", undef, 0)) {
-           $count++;
-           warn "Test3 z failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 z: $@";
-    }
-
-    eval {
-       if (!PVE::Storage::volume_has_feature($cfg, 'sparseinit', "$storagename:$ctdisk", undef, 0)) {
-           $count++;
-           warn "Test3 A failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 A: $@";
-    }
-
-    eval {
-       if (!PVE::Storage::volume_has_feature($cfg, 'sparseinit', "$storagename:$ctbase", undef, 0)) {
-           $count++;
-           warn "Test3 B failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 B: $@";
-    }
-
-    eval {
-       if (!PVE::Storage::volume_has_feature($cfg, 'sparseinit', "$storagename:$ctbase\/$ctlinked", undef, 0)) {
-           $count++;
-           warn "Test3 C failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 C: $@";
-    }
-
-    eval {
-       if (!PVE::Storage::volume_has_feature($cfg, 'snapshot', "$storagename:$vmdisk", 'test', 0)) {
-           $count++;
-           warn "Test3 a1 failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 a1: $@";
-    }
-
-    eval {
-       if (!PVE::Storage::volume_has_feature($cfg, 'snapshot', "$storagename:$vmbase", 'test', 0)) {
-           $count++;
-           warn "Test3 b1 failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 b1: $@";
-    }
-
-    eval {
-       if (!PVE::Storage::volume_has_feature($cfg, 'snapshot', "$storagename:$vmbase\/$vmlinked", 'test', 0)) {
-           $count++;
-           warn "Test3 c1 failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 c1: $@";
-    }
-
-    eval {
-       if (!PVE::Storage::volume_has_feature($cfg, 'snapshot', "$storagename:$ctdisk", 'test', 0)) {
-           $count++;
-           warn "Test3 d1 failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 d1: $@";
-    }
-
-    eval {
-       if (!PVE::Storage::volume_has_feature($cfg, 'snapshot', "$storagename:$ctbase", 'test', 0)) {
-           $count++;
-           warn "Test3 e1 failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 e1: $@";
-    }
-
-    eval {
-       if (!PVE::Storage::volume_has_feature($cfg, 'snapshot', "$storagename:$ctbase\/$ctlinked", 'test', 0)) {
-           $count++;
-           warn "Test3 f1 failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 f1: $@";
-    }
-
-    eval {
-       if (PVE::Storage::volume_has_feature($cfg, 'clone', "$storagename:$vmdisk", 'test', 0)) {
-           $count++;
-           warn "Test3 g1 failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 g1: $@";
-    }
-
-    eval {
-       if (PVE::Storage::volume_has_feature($cfg, 'clone', "$storagename:$vmbase", 'test', 0)) {
-           $count++;
-           warn "Test3 h1 failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 h1: $@";
-    }
-
-    eval {
-       if (PVE::Storage::volume_has_feature($cfg, 'clone', "$storagename:$vmbase\/$vmlinked", 'test', 0)) {
-           $count++;
-           warn "Test3 h1 failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 h1: $@";
-    }
-
-    eval {
-       if (PVE::Storage::volume_has_feature($cfg, 'clone', "$storagename:$ctdisk", 'test', 0)) {
-           $count++;
-           warn "Test3 i1 failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 i1: $@";
-    }
-
-    eval {
-       if (PVE::Storage::volume_has_feature($cfg, 'clone', "$storagename:$ctbase", 'test', 0)) {
-           $count++;
-           warn "Test3 j1 failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 j1: $@";
-    }
-
-    eval {
-       if (PVE::Storage::volume_has_feature($cfg, 'clone', "$storagename:$ctbase\/$ctlinked", 'test', 0)) {
-           $count++;
-           warn "Test3 k1 failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 k1: $@";
-    }
-
-    eval {
-       if (PVE::Storage::volume_has_feature($cfg, 'template', "$storagename:$vmdisk", 'test', 0)) {
-           $count++;
-           warn "Test3 l1 failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 l1: $@";
-    }
-
-    eval {
-       if (PVE::Storage::volume_has_feature($cfg, 'template', "$storagename:$vmbase", 'test', 0)) {
-           $count++;
-           warn "Test3 m1 failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 m1: $@";
-    }
-
-    eval {
-       if (PVE::Storage::volume_has_feature($cfg, 'template', "$storagename:$vmbase\/$vmlinked", 'test', 0)) {
-           $count++;
-           warn "Test3 n1 failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 n1: $@";
-    }
-
-    eval {
-       if (PVE::Storage::volume_has_feature($cfg, 'template', "$storagename:$ctdisk", 'test', 0)) {
-           $count++;
-           warn "Test3 o1 failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 o1: $@";
-    }
-
-    eval {
-       if (PVE::Storage::volume_has_feature($cfg, 'template', "$storagename:$ctbase", 'test', 0)) {
-           $count++;
-           warn "Test3 p1 failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 p1: $@";
-    }
-
-    eval {
-       if (PVE::Storage::volume_has_feature($cfg, 'template', "$storagename:$ctbase\/$ctlinked", 'test', 0)) {
-           $count++;
-           warn "Test3 q1 failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 q1: $@";
-    }
-
-    eval {
-       if (PVE::Storage::volume_has_feature($cfg, 'copy', "$storagename:$vmdisk", 'test', 0)) {
-           $count++;
-           warn "Test3 r1 failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 r1: $@";
-    }
-
-    eval {
-       if (PVE::Storage::volume_has_feature($cfg, 'copy', "$storagename:$vmbase", 'test', 0)) {
-           $count++;
-           warn "Test3 s1 failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 s1: $@";
-    }
-
-    eval {
-       if (PVE::Storage::volume_has_feature($cfg, 'copy', "$storagename:$vmbase\/$vmlinked", 'test', 0)) {
-           $count++;
-           warn "Test3 t1 failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 t1: $@";
-    }
-
-    eval {
-       if (PVE::Storage::volume_has_feature($cfg, 'copy', "$storagename:$ctdisk", 'test', 0)) {
-           $count++;
-           warn "Test3 u1 failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 u1: $@";
-    }
-
-    eval {
-       if (PVE::Storage::volume_has_feature($cfg, 'copy', "$storagename:$ctbase", 'test', 0)) {
-           $count++;
-           warn "Test3 v1 failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 v1: $@";
-    }
-
-    eval {
-       if (PVE::Storage::volume_has_feature($cfg, 'copy', "$storagename:$ctbase\/$ctlinked", 'test', 0)) {
-           $count++;
-           warn "Test3 w1 failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 w1: $@";
-    }
-
-    eval {
-       if (PVE::Storage::volume_has_feature($cfg, 'sparseinit', "$storagename:$vmdisk", 'test', 0)) {
-           $count++;
-           warn "Test3 x1 failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 x1: $@";
-    }
-
-    eval {
-       if (PVE::Storage::volume_has_feature($cfg, 'sparseinit', "$storagename:$vmbase", 'test', 0)) {
-           $count++;
-           warn "Test3 y1 failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 y1: $@";
-    }
-
-    eval {
-       if (PVE::Storage::volume_has_feature($cfg, 'sparseinit', "$storagename:$vmbase\/$vmlinked", 'test', 0)) {
-           $count++;
-           warn "Test3 z1 failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 z1: $@";
-    }
-
-    eval {
-       if (PVE::Storage::volume_has_feature($cfg, 'sparseinit', "$storagename:$ctdisk", 'test', 0)) {
-           $count++;
-           warn "Test3 A1 failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 A1: $@";
-    }
-
-    eval {
-       if (PVE::Storage::volume_has_feature($cfg, 'sparseinit', "$storagename:$ctbase", 'test', 0)) {
-           $count++;
-           warn "Test3 B1 failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 B1: $@";
-    }
-
-    eval {
-       if (PVE::Storage::volume_has_feature($cfg, 'sparseinit', "$storagename:$ctbase\/$ctlinked", 'test', 0)) {
-           $count++;
-           warn "Test3 C1 failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test3 C1: $@";
-    }
-};
-$tests->{3} = $test3;
-
-my $test2 = sub {
-
-    print "\nrun test2 \"volume_resize\"\n";
-    my $newsize = ($volsize + 1) * 1024 * 1024 * 1024;
-
-    eval {
-       if (($newsize/1024) !=
-           PVE::Storage::volume_resize($cfg, "$storagename:$vmdisk", $newsize, 0)) {
-           $count++;
-           warn "Test2 a failed";
-       }
-       if ($newsize  !=
-           PVE::Storage::volume_size_info($cfg, "$storagename:$vmdisk")) {
-           $count++;
-           warn "Test2 a failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test2 a: $@";
-    }
-
-    eval {
-       warn "Test2 b failed" if ($newsize/1024) != PVE::Storage::volume_resize($cfg, "$storagename:$vmbase", $newsize, 0);
-       warn "Test2 b failed" if $newsize  !=
-           PVE::Storage::volume_size_info($cfg, "$storagename:$vmbase");
-    };
-    if ($@) {
-       $count++;
-       warn "Test2 b: $@";
-    }
-
-    eval {
-       if (($newsize/1024) != PVE::Storage::volume_resize($cfg, "$storagename:$vmbase\/$vmlinked", $newsize, 0)) {
-           $count++;
-           warn "Test2 c failed";
-       }
-       if ($newsize  !=
-           PVE::Storage::volume_size_info($cfg, "$storagename:$vmbase\/$vmlinked")) {
-           $count++;
-           warn "Test2 c failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test2 c: $@";
-    }
-
-    eval {
-       if (($newsize/1024) != PVE::Storage::volume_resize($cfg, "$storagename:$ctdisk", $newsize, 0)) {
-           $count++;
-           warn "Test2 d failed";
-       }
-       if ($newsize  !=
-           PVE::Storage::volume_size_info($cfg, "$storagename:$ctdisk")) {
-           $count++;
-           warn "Test2 d failed"
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test2 d: $@";
-    }
-
-    eval {
-       if (($newsize/1024) !=
-           PVE::Storage::volume_resize($cfg, "$storagename:$ctbase", $newsize, 0)) {
-           $count++;
-           warn "Test2 e failed";
-       }
-       if ($newsize  !=
-           PVE::Storage::volume_size_info($cfg, "$storagename:$ctbase")) {
-           $count++;
-           warn "Test2 e failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test2 e: $@";
-    }
-
-    eval {
-       if (($newsize/1024) !=
-           PVE::Storage::volume_resize($cfg, "$storagename:$ctbase\/$ctlinked", $newsize, 0)) {
-           $count++;
-           warn "Test2 f failed";
-       }
-       if ($newsize  !=
-           PVE::Storage::volume_size_info($cfg, "$storagename:$ctbase\/$ctlinked")) {
-           $count++;
-           warn "Test2 f failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test2 f: $@";
-    }
-};
-$tests->{2} = $test2;
-
-my $test1 = sub {
-
-    print "\nrun test1 \"volume_size_info\"\n";
-    my $size = ($volsize * 1024 * 1024 * 1024);
-
-    eval {
-       if ($size != PVE::Storage::volume_size_info($cfg, "$storagename:$vmdisk")) {
-           $count++;
-           warn "Test1 a failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test1 a : $@";
-    }
-
-    eval {
-       if ($size != PVE::Storage::volume_size_info($cfg, "$storagename:$vmbase")) {
-           $count++;
-           warn "Test1 b failed";
-       }
-
-    };
-    if ($@) {
-       $count++;
-       warn "Test1 b : $@";
-    }
-
-    eval {
-       if ($size !=
-           PVE::Storage::volume_size_info($cfg, "$storagename:$vmbase\/$vmlinked")) {
-           $count++;
-           warn "Test1 c failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test1 c : $@";
-    }
-
-    eval {
-       if ($size !=
-           PVE::Storage::volume_size_info($cfg, "$storagename:$ctdisk")) {
-           $count++;
-           warn "Test1 d failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test1 d : $@";
-    }
-
-    eval {
-       if ($size !=
-           PVE::Storage::volume_size_info($cfg, "$storagename:$ctbase")) {
-           $count++;
-           warn "Test1 e failed";
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test1 e : $@";
-    }
-
-    eval {
-       if ($size !=
-           PVE::Storage::volume_size_info($cfg, "$storagename:$vmbase\/$vmlinked")) {
-           $count++;
-           warn "Test1 f failed"
-       }
-    };
-    if ($@) {
-       $count++;
-       warn "Test1 f : $@";
-    }
-
-};
-$tests->{1} = $test1;
-
-sub setup_zfs {
-    #create VM zvol
-    print "create zvol $vmdisk\n" if $verbose;
-    run_command("zfs create -V${volsize}G $zpath\/$vmdisk");
-
-    print "create zvol $vmbase\n" if $verbose;
-    run_command("zfs create -V${volsize}G $zpath\/$vmbase");
-    run_command("zfs snapshot $zpath\/$vmbase$basesnap");
-
-    print "create linked clone $vmlinked\n" if $verbose;
-    run_command("zfs clone $zpath\/$vmbase$basesnap $zpath\/$vmlinked");
-
-    #create CT subvol
-    print "create subvol $ctdisk\n" if $verbose;
-    run_command("zfs create -o refquota=${volsize}G $zpath\/$ctdisk");
-
-    print "create subvol $ctbase\n" if $verbose;
-    run_command("zfs create -o refquota=${volsize}G $zpath\/$ctbase");
-    run_command("zfs snapshot $zpath\/$ctbase$basesnap");
-
-    print "create linked clone $ctlinked\n" if $verbose;
-    run_command("zfs clone $zpath\/$ctbase$basesnap $zpath\/$ctlinked -o refquota=${volsize}G");
-
-    my $vollist = [
-       "$storagename:$vmdisk",
-       "$storagename:$vmbase",
-       "$storagename:$vmbase/$vmlinked",
-       "$storagename:$ctdisk",
-       "$storagename:$ctbase",
-       "$storagename:$ctbase/$ctlinked",
-    ];
-
-    PVE::Storage::activate_volumes($cfg, $vollist);
-}
-
-sub cleanup_zfs {
-
-    print "destroy $pool\/$subvol\n" if $verbose;
-    eval { run_command("zfs destroy $zpath -r"); };
-    if ($@) {
-       print "cleanup failed: $@\nretrying once\n" if $verbose;
-       eval { run_command("zfs destroy $zpath -r"); };
-       if ($@) {
-           clean_up_zpool();
-           setup_zpool();
-       }
-    }
-}
-
-sub setup_zpool {
-
-    unlink 'zpool.img';
-    eval {
-       run_command("truncate -s 8G zpool.img");
-    };
-    if ($@) {
-       clean_up_zpool();
-    }
-    my $pwd = cwd();
-    eval {
-       run_command("zpool create -m \/$mountpoint $subvol $pwd\/zpool.img");
-    };
-    if ($@) {
-       clean_up_zpool();
-    }
-}
-
-sub clean_up_zpool {
-
-    eval {
-       run_command("zpool destroy -f $subvol");
-    };
-    if ($@) {
-       warn $@;}
-    unlink 'zpool.img';
-}
-
-sub volume_is_base {
-    my ($cfg, $volid) = @_;
-
-    my (undef, undef, undef, undef, undef, $isBase, undef) = PVE::Storage::parse_volname($cfg, $volid);
-
-    return $isBase;
-}
-
-if ($> != 0) { #EUID
-    warn "not root, skipping zfs tests\n";
-    exit 0;
-}
-
-eval { run_command("zpool status"); };
-if ($@) {
-    warn "zpool status failed, not running tests: $@\n";
-    exit 0;
-}
-
-setup_zpool();
-
-my $time = time;
-print "Start tests for ZFSPoolPlugin\n";
-
-$cfg = {'ids' => {
-    $storagename => {
-       'content' => {
-           'images' => 1,
-           'rootdir' => 1
-       },
-               'pool' => $subvol,
-               'mountpoint' => "\/$mountpoint",
-               'type' => 'zfspool'
-    },
-       },
-               'order' => {'zfstank99' => 1,}
-};
-
-$zpath = $subvol;
-
-for (my $i = $start_test; $i <= $end_test; $i++) {
-    setup_zfs();
-
-    eval {
-       $tests->{$i}();
-    };
-    if (my $err = $@) {
-       warn $err;
-       $count++;
-    }
-    cleanup_zfs();
-}
-
-clean_up_zpool();
-
-$time = time - $time;
-
-print "Stop tests for ZFSPoolPlugin\n";
-print "$count tests failed\n";
-print "Time: ${time}s\n";
-
-exit -1 if $count > 0;
diff --git a/udev-rbd/50-rbd-pve.rules b/udev-rbd/50-rbd-pve.rules
deleted file mode 100644 (file)
index 79432df..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-KERNEL=="rbd[0-9]*", ENV{DEVTYPE}=="disk", PROGRAM="/usr/libexec/ceph-rbdnamer-pve %k", SYMLINK+="rbd-pve/%c"
-KERNEL=="rbd[0-9]*", ENV{DEVTYPE}=="partition", PROGRAM="/usr/libexec/ceph-rbdnamer-pve %k", SYMLINK+="rbd-pve/%c-part%n"
diff --git a/udev-rbd/Makefile b/udev-rbd/Makefile
deleted file mode 100644 (file)
index 065933b..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-PACKAGE=libpve-storage-perl
-
-DESTDIR=
-PREFIX=/usr
-LIBEXECDIR=${PREFIX}/libexec
-LIBDIR=${PREFIX}/lib
-
-all:
-
-.PHONY: install
-install: 50-rbd-pve.rules ceph-rbdnamer-pve
-       install -d ${DESTDIR}${LIBEXECDIR}
-       install -m 0755 ceph-rbdnamer-pve ${DESTDIR}${LIBEXECDIR}
-       install -d ${DESTDIR}${LIBDIR}/udev/rules.d
-       install -m 0644 50-rbd-pve.rules ${DESTDIR}${LIBDIR}/udev/rules.d
-
-.PHONY: clean
-clean:
-
-.PHONY: distclean
-distclean: clean
diff --git a/udev-rbd/ceph-rbdnamer-pve b/udev-rbd/ceph-rbdnamer-pve
deleted file mode 100755 (executable)
index 23dd626..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-#!/bin/sh
-
-DEV=$1
-NUM=`echo $DEV | sed 's#p.*##g; s#[a-z]##g'`
-POOL=`cat /sys/devices/rbd/$NUM/pool`
-CLUSTER_FSID=`cat /sys/devices/rbd/$NUM/cluster_fsid`
-
-if [ -f /sys/devices/rbd/$NUM/pool_ns ]; then
-    NAMESPACE=`cat /sys/devices/rbd/$NUM/pool_ns`
-else
-    NAMESPACE=""
-fi
-IMAGE=`cat /sys/devices/rbd/$NUM/name`
-SNAP=`cat /sys/devices/rbd/$NUM/current_snap`
-
-echo -n "/$CLUSTER_FSID/$POOL"
-
-if [ -n "$NAMESPACE" ]; then
-    echo -n "/$NAMESPACE"
-fi
-echo -n "/$IMAGE"
-if [ "$SNAP" != "-" ]; then
-    echo -n "@$SNAP"
-fi