From b6cf0a6659405908cc4dc543fa157354df013245 Mon Sep 17 00:00:00 2001 From: Dietmar Maurer Date: Tue, 23 Aug 2011 07:43:03 +0200 Subject: [PATCH] imported from svn 'pve-storage/pve2' --- ChangeLog | 293 +++++ Makefile | 71 ++ PVE/API2/Makefile | 5 + PVE/API2/Storage/Config.pm | 329 +++++ PVE/API2/Storage/Content.pm | 257 ++++ PVE/API2/Storage/Makefile | 6 + PVE/API2/Storage/Scan.pm | 190 +++ PVE/API2/Storage/Status.pm | 228 ++++ PVE/Makefile | 6 + PVE/Storage.pm | 2360 +++++++++++++++++++++++++++++++++++ README | 74 ++ changelog.Debian | 85 ++ control.in | 9 + copyright | 16 + pvesm | 278 +++++ 15 files changed, 4207 insertions(+) create mode 100644 ChangeLog create mode 100644 Makefile create mode 100644 PVE/API2/Makefile create mode 100755 PVE/API2/Storage/Config.pm create mode 100644 PVE/API2/Storage/Content.pm create mode 100644 PVE/API2/Storage/Makefile create mode 100644 PVE/API2/Storage/Scan.pm create mode 100644 PVE/API2/Storage/Status.pm create mode 100644 PVE/Makefile create mode 100755 PVE/Storage.pm create mode 100644 README create mode 100644 changelog.Debian create mode 100644 control.in create mode 100644 copyright create mode 100755 pvesm diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..9ca0794 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,293 @@ +2011-08-18 Proxmox Support Team + + * PVE/Storage.pm (iscsi_login): login to target, instead of + portal- to make it work when one portal is offline. + +2011-08-15 Proxmox Support Team + + * PVE/Storage.pm (parse_config): fix parser for files without + newline at eof + +2011-08-12 Proxmox Support Team + + * PVE/Storage.pm (scan_usb): imp. + +2011-08-05 Proxmox Support Team + + * changelog.Debian: increase release number to 2.0-4 + + * PVE/Storage.pm (iscsi_device_list): return numeric values for + channel/ID/LUN + +2011-08-01 Proxmox Support Team + + * PVE/Storage.pm (iscsi_test_portal): factor out code to test if + portal in online (use 2 seconds timeout). + (iscsi_discovery): test if portal is online using + iscsi_test_portal(). This avoids that we run int a timeout (iscsi + default timeout is 15 seconds, we now use 2 seconds) + (cluster_lock_storage): fix cfs_lock_file() arguments, + (lock_storage_config): use default timeout (10) + + * PVE/API2/Storage/Config.pm: s/resolv_portal_dns/resolv_portal/ + (delete) do not call deactivate_storage(), because we likely run + into timeouts. + + * PVE/Storage.pm (resolv_portal_dns): remove duplicate (use resolv_portal instead) + (resolv_portal): use resolv_server() + + * PVE/API2/Storage/Scan.pm: remove unneccessary call to resolv_portal_dns() + + * PVE/Storage.pm (iscsi_login): use Net::Ping to check portal + availability (avoid long iscsi login timeouts) + (resolv_portal_dns): use resolv_server() + +2011-07-29 Proxmox Support Team + + * changelog.Debian: update version to 2.0-3 + + * PVE/API2/Storage/Config.pm: activate base storage before we try + to create the VG. Make 'nodes' optional. + +2011-07-28 Proxmox Support Team + + * PVE/Storage.pm (storage_check_node): check if storage is + available on a specific node. + (storage_check_enabled): check if storage is + available on the local node. + + * PVE/API2/Storage/Config.pm (create): add 'nodes' options, do not + activate storage automatically. + + * PVE/API2/Storage/Config.pm (update): add 'nodes' options, do not + activate storage automatically. + + * pvesm (lock): removed - we do not use the central lock manager + anymore. + + * PVE/Storage.pm (vdisk_alloc): use run_command() in order to get + better error messages. + +2011-07-27 Proxmox Support Team + + * PVE/API2/Storage/Config.pm (create): add option 'base' + +2011-07-26 Proxmox Support Team + + * PVE/Storage.pm (verify_portal_dns): new type + 'pve-storage-portal-dns', which allows to use a DNS name. + (resolv_portal_dns): helper to convert portal with DNS name to IP address. + + * PVE/API2/Storage/Config.pm: 'target' can be arbitrary string (we + do not check format for now) + + * PVE/API2/Storage/Scan.pm (iscsiscan): rename 'server' to 'portal' + +2010-11-08 Proxmox Support Team + + * Storage.pm (iscsi_login): multipath fixes: try to log in to all + portals (backport from stable) + +2010-10-28 Proxmox Support Team + + * Storage.pm (iscsi_session_list): allow several sessions per + target (multipath)(backport from stable). + (iscsi_session_rescan): rescan all sessions (backport from stable) + +2010-09-13 Proxmox Support Team + + * Storage.pm (storage_info): cache VGs, mountdata and iSCSI + session list (backport from stable) + +2010-05-06 Proxmox Support Team + + * Storage.pm (storage_migrate): use --sparse and --whole-file, + this alsocreates sparse files (backport from stable) + +2011-07-22 Proxmox Support Team + + * PVE/API2/Storage/Scan.pm: split scan into three different + methods with divverent return values + +2011-07-21 Proxmox Support Team + + * PVE/Storage.pm (storage_info): do not list disabled storages + +2011-05-06 Proxmox Support Team + + * PVE/API2/Storage/Status.pm: impl. content filter + + * PVE/Storage.pm (storage_info): include content type + +2011-04-04 Proxmox Support Team + + * PVE/Storage.pm (load_stable_scsi_paths): only load + /dev/disk/by-id once (avoid delays when we have many disks) + +2011-03-09 Proxmox Support Team + + * pvesm (status): report sizes like 'df' + + * PVE/Storage.pm (file_size_info): allow to pass timeout + (important when NFS server is down) + (__activate_storage_full): avoid call to mkpath if not necessary + - avoid hang when NFS server is offline + (storage_info): return sizes in bytes + (storage_info): use PVE::Tools::df with timeout + (lvm_vgs): use '--units b' (report size in bytes) + (lvm_lvs): use '--units b' (report size in bytes) + (file_size_info): report size in bytes + + * control.in (Depends): remove libfilesys-df-perl + +2011-03-08 Proxmox Support Team + + * PVE/Storage.pm (__activate_storage_full): avoid to create empty + content config + +2011-02-11 Proxmox Support Team + + * PVE/API2/*: cleanup API Object hierarchiy + + * PVE/API2/Storage.pm: removed (no longer needed) + +2011-01-25 Proxmox Support Team + + * PVE/Storage.pm: use new cfs_read_file/cfs_write_file everywhere + (cluster filesystem support) + +2010-11-08 Proxmox Support Team + + * PVE/Storage.pm: moved hostname read/write to INotify.pm + +2010-09-14 Proxmox Support Team + + * pvesm: add/use 'verifyapi' command + + * Storage.pm (storage_info): better caching - avoid timeout bug + with large number of VGs. + +2010-09-07 Proxmox Support Team + + * Storage.pm (parse_options): renamed from parse_options_new + +2010-08-26 Proxmox Support Team + + * Storage.pm (vdisk_list): return full volid instead of volume name. + (template_list): return full volid instead of volume name. + (foreach_volid): re-add, slightly modified + +2010-08-25 Proxmox Support Team + + * pvesm: use new PVE::CLIHandler + + * PVE/API2/Storage.pm: create extra upload method, because this + have different 'proxy' requirements that normal 'create' + +2010-08-24 Proxmox Support Team + + * pvesm: use new PVE::RPCEnvironment + + * PVE/API2/*.pm: remove $conn parameter everywhere + +2010-08-19 Proxmox Support Team + + * pvesm: more cleanups - use new API calls + +2010-08-17 Proxmox Support Team + + * API2::Storage.pm: moved from pve-manager + + * split API::Storage into different files + +2010-08-16 Proxmox Support Team + + * Storage.pm (file_read_firstline): import from PVE::Tools + + * Storage.pm: use new INotify class + + * Storage.pm (lock_config): renamed to lock_storage_config, use + lock_file from PVE::Utils + + * control.in (Depends): add libpve-common-perl + +2010-07-16 Proxmox Support Team + + * Storage.pm (parse_options): added ability to verify a + HASH (needed by REST API) + +2010-01-25 Proxmox Support Team + + * Storage.pm (parse_lvm_name, parse_storage_id, parse_volume_id): + fix regex (allow 2 character names) + +2010-01-18 Proxmox Support Team + + * Storage.pm (iscsi_device_list): fix for kernel 2.6.32 + +2009-10-29 Proxmox Support Team + + * Storage.pm (parse_volume_id): ignore case. + +2009-10-27 Proxmox Support Team + + * Storage.pm (parse_volume_id): correctly parse storage id. + +2009-10-19 Proxmox Support Team + + * Storage.pm (storage_migrate): flush output. + +2009-10-08 Proxmox Support Team + + * Storage.pm (path): use parse_volume_id() + (template_list): list backup files too + +2009-10-07 Proxmox Support Team + + * Storage.pm (cluster_lock_storage): dont use ssh for local + request (master = localhost) + +2009-09-18 Proxmox Support Team + + * Storage.pm (storage_remove): do not remove storage which is used + as base for other storage. + +2009-09-04 Proxmox Support Team + + * Storage.pm (lvm_create_volume_group): don't set clustered flag + (vdisk_alloc): a better way to create unique disk names + +2009-08-21 Proxmox Support Team + + * Storage.pm (activate_storage_list): only call udevsettle when + there are events. openvz container start/stop sometimes increases + event counter, but deliver no events. So udevsettle simply + hangs. Above optimization eliminate that bug in 99%. + +2009-08-20 Proxmox Support Team + + * Storage.pm (cluster_lock_storage): implemented simply central + cluster lock manager. + +2009-08-18 Proxmox Support Team + + * Storage.pm (iscsi_session_rescan): do not rescan uscsi too often + (wait at leaset 10 seconds). + (parse_storage_id): allow captial letters. + +2009-08-13 Proxmox Support Team + + * Storage.pm: use arrays instead of hash to return lists (SOAP + compatibility) + (foreach_volid): new helper method + (storage_migrate): first try + +2009-07-20 Proxmox Support Team + + * Storage.pm (target_is_used): new function + +2009-07-03 Proxmox Support Team + + * Storage.pm (activate_storage_list): only call udev settle when + necessary (else it hangs sometimes) + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..484eb1c --- /dev/null +++ b/Makefile @@ -0,0 +1,71 @@ +RELEASE=2.0 + +VERSION=2.0 +PACKAGE=libpve-storage-perl +PKGREL=4 + +DESTDIR= +PREFIX=/usr +BINDIR=${PREFIX}/bin +SBINDIR=${PREFIX}/sbin +MANDIR=${PREFIX}/share/man +DOCDIR=${PREFIX}/share/doc +MAN1DIR=${MANDIR}/man1/ +export PERLDIR=${PREFIX}/share/perl5 + +#ARCH:=$(shell dpkg-architecture -qDEB_BUILD_ARCH) +ARCH=all +DEB=${PACKAGE}_${VERSION}-${PKGREL}_${ARCH}.deb + + +all: ${DEB} + +.PHONY: dinstall +dinstall: deb + dpkg -i ${DEB} + +.PHONY: install +install: + install -d ${DESTDIR}${SBINDIR} + install -m 0755 pvesm ${DESTDIR}${SBINDIR} + make -C PVE install + install -d ${DESTDIR}/usr/share/man/man1 + pod2man -n pvesm -s 1 -r "proxmox 1.0" -c "Proxmox Documentation" ${DESTDIR}/usr/share/man/man1/pvesm.1.gz + +.PHONY: deb ${DEB} +deb ${DEB}: + rm -rf debian + mkdir debian + make DESTDIR=${CURDIR}/debian install + perl -I. ./pvesm verifyapi + install -d -m 0755 debian/DEBIAN + sed -e s/@@VERSION@@/${VERSION}/ -e s/@@PKGRELEASE@@/${PKGREL}/ -e s/@@ARCH@@/${ARCH}/ debian/DEBIAN/control + install -D -m 0644 copyright debian/${DOCDIR}/${PACKAGE}/copyright + install -m 0644 changelog.Debian debian/${DOCDIR}/${PACKAGE}/ + gzip -9 debian/${DOCDIR}/${PACKAGE}/changelog.Debian + install -m 0644 ChangeLog debian/${DOCDIR}/${PACKAGE}/changelog + gzip -9 debian/${DOCDIR}/${PACKAGE}/changelog + dpkg-deb --build debian + mv debian.deb ${DEB} + rm -rf debian + lintian ${DEB} + +.PHONY: clean +clean: + rm -rf debian *.deb ${PACKAGE}-*.tar.gz dist + find . -name '*~' -exec rm {} ';' + +.PHONY: distclean +distclean: clean + + +.PHONY: upload +upload: ${DEB} + umount /pve/${RELEASE}; mount /pve/${RELEASE} -o rw + mkdir -p /pve/${RELEASE}/extra + rm -f /pve/${RELEASE}/extra/${PACKAGE}_*.deb + rm -f /pve/${RELEASE}/extra/Packages* + cp ${DEB} /pve/${RELEASE}/extra + cd /pve/${RELEASE}/extra; dpkg-scanpackages . /dev/null > Packages; gzip -9c Packages > Packages.gz + umount /pve/${RELEASE}; mount /pve/${RELEASE} -o ro + diff --git a/PVE/API2/Makefile b/PVE/API2/Makefile new file mode 100644 index 0000000..e3ccdc1 --- /dev/null +++ b/PVE/API2/Makefile @@ -0,0 +1,5 @@ + + +.PHONY: install +install: + make -C Storage install \ No newline at end of file diff --git a/PVE/API2/Storage/Config.pm b/PVE/API2/Storage/Config.pm new file mode 100755 index 0000000..3a6c6bc --- /dev/null +++ b/PVE/API2/Storage/Config.pm @@ -0,0 +1,329 @@ +package PVE::API2::Storage::Config; + +use strict; +use warnings; + +use PVE::SafeSyslog; +use PVE::Cluster qw(cfs_read_file cfs_write_file); +use PVE::Storage; +use HTTP::Status qw(:constants); +use Storable qw(dclone); +use PVE::JSONSchema qw(get_standard_option); + +use Data::Dumper; # fixme: remove + +use PVE::RESTHandler; + +use base qw(PVE::RESTHandler); + +my @ctypes = qw(images vztmpl iso backup); + +my $storage_type_enum = ['dir', 'nfs', 'lvm', 'iscsi']; + +my $api_storage_config = sub { + my ($cfg, $storeid) = @_; + + my $scfg = dclone(PVE::Storage::storage_config ($cfg, $storeid)); + $scfg->{storage} = $storeid; + delete $scfg->{priority}; + $scfg->{digest} = $cfg->{digest}; + $scfg->{content} = PVE::Storage::content_hash_to_string($scfg->{content}); + + if ($scfg->{nodes}) { + $scfg->{nodes} = join(',', keys(%{$scfg->{nodes}})); + } + + return $scfg; +}; + +__PACKAGE__->register_method ({ + name => 'index', + path => '', + method => 'GET', + description => "Storage index.", + 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 $cfg = cfs_read_file("storage.cfg"); + + my @sids = PVE::Storage::storage_ids($cfg); + + my $res = []; + foreach my $storeid (@sids) { + 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.", + parameters => { + additionalProperties => 0, + properties => { + storage => get_standard_option('pve-storage-id'), + }, + }, + returns => {}, + code => sub { + my ($param) = @_; + + my $cfg = cfs_read_file("storage.cfg"); + + return &$api_storage_config($cfg, $param->{storage}); + }}); + +__PACKAGE__->register_method ({ + name => 'create', + protected => 1, + path => '', + method => 'POST', + description => "Create a new storage.", + parameters => { + additionalProperties => 0, + properties => { + storage => get_standard_option('pve-storage-id'), + nodes => get_standard_option('pve-node-list', { optional => 1 }), + type => { + type => 'string', + enum => $storage_type_enum, + }, + path => { + type => 'string', format => 'pve-storage-path', + optional => 1, + }, + export => { + type => 'string', format => 'pve-storage-path', + optional => 1, + }, + server => { + type => 'string', format => 'pve-storage-server', + optional => 1, + }, + options => { + type => 'string', format => 'pve-storage-options', + optional => 1, + }, + target => { + type => 'string', + optional => 1, + }, + vgname => { + type => 'string', format => 'pve-storage-vgname', + optional => 1, + }, + base => { + type => 'string', format => 'pve-volume-id', + optional => 1, + }, + portal => { + type => 'string', format => 'pve-storage-portal-dns', + optional => 1, + }, + content => { + type => 'string', format => 'pve-storage-content-list', + optional => 1, + }, + disable => { + type => 'boolean', + optional => 1, + }, + shared => { + type => 'boolean', + optional => 1, + }, + 'format' => { + type => 'string', format => 'pve-storage-format', + optional => 1, + }, + }, + }, + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + my $type = $param->{type}; + delete $param->{type}; + + my $storeid = $param->{storage}; + delete $param->{storage}; + + if ($param->{portal}) { + $param->{portal} = PVE::Storage::resolv_portal($param->{portal}); + } + + my $opts = PVE::Storage::parse_options($storeid, $type, $param, 1); + + PVE::Storage::lock_storage_config( + sub { + + my $cfg = cfs_read_file('storage.cfg'); + + if (my $scfg = PVE::Storage::storage_config ($cfg, $storeid, 1)) { + die "storage ID '$storeid' already defined\n"; + } + + $cfg->{ids}->{$storeid} = $opts; + + if ($type eq 'lvm' && $opts->{base}) { + + my ($baseid, $volname) = PVE::Storage::parse_volume_id ($opts->{base}); + + 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 + if (!($basecfg->{type} eq 'iscsi')) { + die "unsupported base type '$basecfg->{type}'"; + } + + my $path = PVE::Storage::path ($cfg, $opts->{base}); + + PVE::Storage::activate_storage($cfg, $baseid); + + PVE::Storage::lvm_create_volume_group ($path, $opts->{vgname}, $opts->{shared}); + } + + # 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); + } + + cfs_write_file('storage.cfg', $cfg); + + }, "create storage failed"); + + }}); + +__PACKAGE__->register_method ({ + name => 'update', + protected => 1, + path => '{storage}', + method => 'PUT', + description => "Update storage configuration.", + parameters => { + additionalProperties => 0, + properties => { + storage => get_standard_option('pve-storage-id'), + nodes => get_standard_option('pve-node-list', { optional => 1 }), + content => { + type => 'string', format => 'pve-storage-content-list', + optional => 1, + }, + 'format' => { + type => 'string', format => 'pve-storage-format', + optional => 1, + }, + disable => { + type => 'boolean', + optional => 1, + }, + shared => { + type => 'boolean', + optional => 1, + }, + options => { + type => 'string', format => 'pve-storage-options', + optional => 1, + }, + digest => { + type => 'string', + optional => 1, + } + }, + }, + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + my $storeid = $param->{storage}; + delete($param->{storage}); + + my $digest = $param->{digest}; + delete($param->{digest}); + + PVE::Storage::lock_storage_config( + sub { + + my $cfg = cfs_read_file('storage.cfg'); + + PVE::Storage::assert_if_modified ($cfg, $digest); + + my $scfg = PVE::Storage::storage_config ($cfg, $storeid); + + my $opts = PVE::Storage::parse_options($storeid, $scfg->{type}, $param); + + foreach my $k (%$opts) { + $scfg->{$k} = $opts->{$k}; + } + + cfs_write_file('storage.cfg', $cfg); + + }, "update storage failed"); + + return undef; + }}); + +__PACKAGE__->register_method ({ + name => 'delete', + protected => 1, + path => '{storage}', # /storage/config/{storage} + method => 'DELETE', + description => "Delete storage configuration.", + parameters => { + additionalProperties => 0, + properties => { + storage => get_standard_option('pve-storage-id'), + }, + }, + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + my $storeid = $param->{storage}; + delete($param->{storage}); + + PVE::Storage::lock_storage_config( + sub { + + my $cfg = cfs_read_file('storage.cfg'); + + die "can't remove storage - storage is used as base of another storage\n" + if PVE::Storage::storage_is_used ($cfg, $storeid); + + delete ($cfg->{ids}->{$storeid}); + + cfs_write_file('storage.cfg', $cfg); + + }, "delete storage failed"); + + return undef; + }}); + +1; diff --git a/PVE/API2/Storage/Content.pm b/PVE/API2/Storage/Content.pm new file mode 100644 index 0000000..9f34819 --- /dev/null +++ b/PVE/API2/Storage/Content.pm @@ -0,0 +1,257 @@ +package PVE::API2::Storage::Content; + +use strict; +use warnings; + +use PVE::SafeSyslog; +use PVE::Cluster qw(cfs_read_file); +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 base qw(PVE::RESTHandler); + +my @ctypes = qw(images vztmpl iso backup); + +__PACKAGE__->register_method ({ + name => 'index', + path => '', + method => 'GET', + description => "List storage content.", + protected => 1, + proxyto => 'node', + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + storage => get_standard_option('pve-storage-id'), + content => { + description => "Only list content of this type.", + type => 'string', format => 'pve-storage-content', + optional => 1, + }, + vmid => get_standard_option + ('pve-vmid', { + description => "Only list images for this VM", + optional => 1, + }), + }, + }, + returns => { + type => 'array', + items => { + type => "object", + properties => { + volid => { + type => 'string' + } + }, + }, + links => [ { rel => 'child', href => "{volid}" } ], + }, + code => sub { + my ($param) = @_; + + my $cts = $param->{content} ? [ $param->{content} ] : [ @ctypes ]; + + my $storeid = $param->{storage}; + + my $cfg = cfs_read_file("storage.cfg"); + + my $scfg = PVE::Storage::storage_config ($cfg, $storeid); + + my $res = []; + foreach my $ct (@$cts) { + my $data; + if ($ct eq 'images') { + $data = PVE::Storage::vdisk_list ($cfg, $storeid, $param->{vmid}); + } elsif ($ct eq 'iso') { + $data = PVE::Storage::template_list ($cfg, $storeid, 'iso') + if !$param->{vmid}; + } elsif ($ct eq 'vztmpl') { + $data = PVE::Storage::template_list ($cfg, $storeid, 'vztmpl') + if !$param->{vmid}; + } elsif ($ct eq 'backup') { + $data = PVE::Storage::template_list ($cfg, $storeid, 'backup') + if !$param->{vmid}; + } + + next if !$data || !$data->{$storeid}; + + foreach my $item (@{$data->{$storeid}}) { + push @$res, $item; + } + } + + return $res; + }}); + +__PACKAGE__->register_method ({ + name => 'create', + path => '', + method => 'POST', + description => "Allocate disk images.", + protected => 1, + proxyto => 'node', + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + storage => get_standard_option('pve-storage-id'), + filename => { + description => "The name of the file to create/upload.", + type => 'string', + }, + vmid => get_standard_option('pve-vmid', { description => "Specify owner VM" } ), + 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'], + 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)$/) { + 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 = cfs_read_file('storage.cfg'); + + 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); + raise_param_exc({ storage => "storage ID missmatch" }) + if $storeid && $sid ne $storeid; + $volid = $volume; + }; + raise_param_exc({ volume => $@}) if $@; + + } else { + raise_param_exc({ volume => "no storage speficied - incomplete volume ID" }) + if !$storeid; + + $volid = "$storeid:$volume"; + } + + return $volid; +}; + +__PACKAGE__->register_method ({ + name => 'info', + path => '{volume}', + method => 'GET', + description => "Get volume attributes", + 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' }, + code => sub { + my ($param) = @_; + + my $volid = &$real_volume_id($param->{storage}, $param->{volume}); + + my $cfg = cfs_read_file('storage.cfg'); + + my $path = PVE::Storage::path($cfg, $volid); + my ($size, $format, $used) = PVE::Storage::file_size_info ($path); + + # fixme: return more attributes? + return { + path => $path, + size => $size, + used => $used, + }; + }}); + +__PACKAGE__->register_method ({ + name => 'delete', + path => '{volume}', + method => 'DELETE', + description => "Delete volume", + 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 => 'null' }, + code => sub { + my ($param) = @_; + + my $volid = &$real_volume_id($param->{storage}, $param->{volume}); + + my $cfg = cfs_read_file('storage.cfg'); + + PVE::Storage::vdisk_free ($cfg, $volid); + + return undef; + }}); + +1; diff --git a/PVE/API2/Storage/Makefile b/PVE/API2/Storage/Makefile new file mode 100644 index 0000000..b23c17c --- /dev/null +++ b/PVE/API2/Storage/Makefile @@ -0,0 +1,6 @@ + +SOURCES= Content.pm Status.pm Config.pm Scan.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/Scan.pm b/PVE/API2/Storage/Scan.pm new file mode 100644 index 0000000..3aaa066 --- /dev/null +++ b/PVE/API2/Storage/Scan.pm @@ -0,0 +1,190 @@ +package PVE::API2::Storage::Scan; + +use strict; +use warnings; + +use PVE::SafeSyslog; +use PVE::Storage; +use HTTP::Status qw(:constants); +use PVE::JSONSchema qw(get_standard_option); + +use PVE::RESTHandler; + +use base qw(PVE::RESTHandler); + +__PACKAGE__->register_method ({ + name => 'index', + path => '', + method => 'GET', + description => "Index of available scan methods", + 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 => 'lvm' }, + { method => 'iscsi' }, + { method => 'nfs' }, + { method => 'usb' }, + ]; + + return $res; + }}); + +__PACKAGE__->register_method ({ + name => 'nfsscan', + path => 'nfs', + method => 'GET', + description => "Scan remote NFS server.", + protected => 1, + proxyto => "node", + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + server => { type => 'string', format => 'pve-storage-server' }, + }, + }, + returns => { + type => 'array', + items => { + type => "object", + properties => { + path => { type => 'string'}, + options => { type => 'string'}, + }, + }, + }, + code => sub { + my ($param) = @_; + + my $server = $param->{server}; + my $res = PVE::Storage::scan_nfs($server); + + my $data = []; + foreach my $k (keys %$res) { + push @$data, { path => $k, options => $res->{$k} }; + } + return $data; + }}); + +__PACKAGE__->register_method ({ + name => 'iscsiscan', + path => 'iscsi', + method => 'GET', + description => "Scan remote iSCSI server.", + protected => 1, + proxyto => "node", + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + portal => { type => 'string', format => 'pve-storage-portal-dns' }, + }, + }, + returns => { + type => 'array', + items => { + type => "object", + properties => { + target => { type => 'string'}, + portal => { type => 'string'}, + }, + }, + }, + code => sub { + my ($param) = @_; + + my $res = PVE::Storage::scan_iscsi($param->{portal}); + + my $data = []; + foreach my $k (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", + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + }, + }, + returns => { + type => 'array', + items => { + type => "object", + properties => { + vg => { type => 'string'}, + }, + }, + }, + code => sub { + my ($param) = @_; + + my $res = PVE::Storage::lvm_vgs(); + return PVE::RESTHandler::hash_to_array($res, 'vg'); + }}); + +__PACKAGE__->register_method ({ + name => 'usbscan', + path => 'usb', + method => 'GET', + description => "List local USB devices.", + protected => 1, + proxyto => "node", + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + }, + }, + returns => { + type => 'array', + items => { + type => "object", + properties => { + busnum => { type => 'integer'}, + devnum => { type => 'integer'}, + port => { type => 'integer'}, + usbpath => { type => 'string', optional => 1}, + level => { type => 'integer'}, + class => { type => 'integer'}, + vendid => { type => 'string'}, + prodid => { type => 'string'}, + speed => { type => 'string'}, + + product => { type => 'string', optional => 1 }, + serial => { type => 'string', optional => 1 }, + manufacturer => { type => 'string', optional => 1 }, + }, + }, + }, + code => sub { + my ($param) = @_; + + return PVE::Storage::scan_usb(); + }}); + +1; diff --git a/PVE/API2/Storage/Status.pm b/PVE/API2/Storage/Status.pm new file mode 100644 index 0000000..509253d --- /dev/null +++ b/PVE/API2/Storage/Status.pm @@ -0,0 +1,228 @@ +package PVE::API2::Storage::Status; + +use strict; +use warnings; + +use PVE::Cluster qw(cfs_read_file); +use PVE::Storage; +use PVE::API2::Storage::Content; +use PVE::RESTHandler; +use PVE::RPCEnvironment; +use PVE::JSONSchema qw(get_standard_option); +use PVE::Exception qw(raise_param_exc); + +use base qw(PVE::RESTHandler); + + +__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 ({ + name => 'index', + path => '', + method => 'GET', + description => "Get status for all datastores.", + 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, + }), + content => { + description => "Only list stores which support this content type.", + type => 'string', format => 'pve-storage-content', + optional => 1, + }, + }, + }, + returns => { + type => 'array', + items => { + type => "object", + properties => { storage => { type => 'string' } }, + }, + links => [ { rel => 'child', href => "{storage}" } ], + }, + code => sub { + my ($param) = @_; + + my $cfg = cfs_read_file("storage.cfg"); + + my $info = PVE::Storage::storage_info($cfg, $param->{content}); + + if ($param->{storage}) { + my $data = $info->{$param->{storage}}; + + raise_param_exc({ storage => "No such storage." }) + if !defined($data); + + $data->{storage} = $param->{storage}; + + return [ $data ]; + } + return PVE::RESTHandler::hash_to_array($info, 'storage'); + }}); + +__PACKAGE__->register_method ({ + name => 'diridx', + path => '{storage}', + method => 'GET', + description => "", + 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 => 'status' }, + { subdir => 'content' }, + { subdir => 'rrd' }, + { subdir => 'rrddata' }, + ]; + + return $res; + }}); + +__PACKAGE__->register_method ({ + name => 'read_status', + path => '{storage}/status', + method => 'GET', + description => "Read storage status.", + 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 = cfs_read_file("storage.cfg"); + + 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).", + 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::Cluster::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.", + 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::Cluster::create_rrd_data( + "pve2-storage/$param->{node}/$param->{storage}", + $param->{timeframe}, $param->{cf}); + }}); + +1; diff --git a/PVE/Makefile b/PVE/Makefile new file mode 100644 index 0000000..ae63b2c --- /dev/null +++ b/PVE/Makefile @@ -0,0 +1,6 @@ + + +.PHONY: install +install: + install -D -m 0644 Storage.pm ${DESTDIR}${PERLDIR}/PVE/Storage.pm + make -C API2 install \ No newline at end of file diff --git a/PVE/Storage.pm b/PVE/Storage.pm new file mode 100755 index 0000000..5fe6c95 --- /dev/null +++ b/PVE/Storage.pm @@ -0,0 +1,2360 @@ +package PVE::Storage; + +use strict; +use POSIX; +use IO::Select; +use IO::Dir; +use IO::File; +use Fcntl ':flock'; +use File::stat; +use File::Basename; +use File::Path; +use IPC::Open2; +use Cwd 'abs_path'; +use Getopt::Long qw(GetOptionsFromArray); +use Socket; +use Digest::SHA1; +use Net::Ping; + +use PVE::Tools qw(run_command file_read_firstline trim); +use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_write_file cfs_lock_file); +use PVE::Exception qw(raise_param_exc); +use PVE::JSONSchema; +use PVE::INotify; + +my $ISCSIADM = '/usr/bin/iscsiadm'; +my $UDEVADM = '/sbin/udevadm'; + +$ISCSIADM = undef if ! -X $ISCSIADM; + +# fixme: always_call_parser => 1 ?? +cfs_register_file ('storage.cfg', + \&parse_config, + \&write_config); + +# generic utility function + +sub config { + return cfs_read_file("storage.cfg"); +} + +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 load_stable_scsi_paths { + + my $stable_paths = {}; + + my $stabledir = "/dev/disk/by-id"; + + if (my $dh = IO::Dir->new($stabledir)) { + while (defined(my $tmp = $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-/) { + my $path = "$stabledir/$tmp"; + my $bdevdest = readlink($path); + if ($bdevdest && $bdevdest =~ m|^../../([^/]+)|) { + $stable_paths->{$1}=$tmp; + } + } + } + $dh->close; + } + return $stable_paths; +} + +sub dir_glob_regex { + my ($dir, $regex) = @_; + + my $dh = IO::Dir->new ($dir); + return wantarray ? () : undef if !$dh; + + while (defined(my $tmp = $dh->read)) { + if (my @res = $tmp =~ m/^($regex)$/) { + $dh->close; + return wantarray ? @res : $tmp; + } + } + $dh->close; + + return wantarray ? () : undef; +} + +sub dir_glob_foreach { + my ($dir, $regex, $func) = @_; + + my $dh = IO::Dir->new ($dir); + if (defined $dh) { + while (defined(my $tmp = $dh->read)) { + if (my @res = $tmp =~ m/^($regex)$/) { + &$func (@res); + } + } + } +} + +sub read_proc_mounts { + + local $/; # enable slurp mode + + my $data = ""; + if (my $fd = IO::File->new ("/proc/mounts", "r")) { + $data = <$fd>; + close ($fd); + } + + return $data; +} + +# PVE::Storage utility functions + +sub lock_storage_config { + my ($code, $errmsg) = @_; + + cfs_lock_file("storage.cfg", undef, $code); + my $err = $@; + if ($err) { + $errmsg ? die "$errmsg: $err" : die $err; + } +} + +my $confvars = { + path => 'path', + shared => 'bool', + disable => 'bool', + format => 'format', + content => 'content', + server => 'server', + export => 'path', + vgname => 'vgname', + base => 'volume', + portal => 'portal', + target => 'target', + nodes => 'nodes', + options => 'options', +}; + +my $required_config = { + dir => ['path'], + nfs => ['path', 'server', 'export'], + lvm => ['vgname'], + iscsi => ['portal', 'target'], +}; + +my $fixed_config = { + dir => ['path'], + nfs => ['path', 'server', 'export'], + lvm => ['vgname', 'base'], + iscsi => ['portal', 'target'], +}; + +my $default_config = { + dir => { + path => 1, + nodes => 0, + shared => 0, + disable => 0, + content => [ { images => 1, rootdir => 1, vztmpl => 1, iso => 1, backup => 1, none => 1 }, + { images => 1, rootdir => 1 }], + format => [ { raw => 1, qcow2 => 1, vmdk => 1 } , 'raw' ], + }, + + nfs => { + path => 1, + nodes => 0, + disable => 0, + server => 1, + export => 1, + options => 0, + content => [ { images => 1, iso => 1, backup => 1}, + { images => 1 }], + format => [ { raw => 1, qcow2 => 1, vmdk => 1 } , 'raw' ], + }, + + lvm => { + vgname => 1, + nodes => 0, + shared => 0, + disable => 0, + content => [ {images => 1}, { images => 1 }], + base => 1, + }, + + iscsi => { + portal => 1, + target => 1, + nodes => 0, + disable => 0, + content => [ {images => 1, none => 1}, { images => 1 }], + }, +}; + +sub valid_content_types { + my ($stype) = @_; + + my $def = $default_config->{$stype}; + + return {} if !$def; + + return $def->{content}->[0]; +} + +sub content_hash_to_string { + my $hash = shift; + + my @cta; + foreach my $ct (keys %$hash) { + push @cta, $ct if $hash->{$ct}; + } + + return join(',', @cta); +} + +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) = @_; + + # fixme: use better regex ? + # IP or DNS name + if ($server !~ m/^[[:alnum:]\-\.]+$/) { + 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-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 ($portal !~ m/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|[[:alnum:]\-\.]+)(:\d+)?$/) { + 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)/) { + 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; +} + +sub check_type { + my ($stype, $ct, $key, $value, $storeid, $noerr) = @_; + + my $def = $default_config->{$stype}; + + if (!$def) { # should not happen + return undef if $noerr; + die "unknown storage type '$stype'\n"; + } + + if (!defined($def->{$key})) { + return undef if $noerr; + die "unexpected property\n"; + } + + if (!defined ($value)) { + return undef if $noerr; + die "got undefined value\n"; + } + + if ($value =~ m/[\n\r]/) { + return undef if $noerr; + die "property contains a line feed\n"; + } + + if ($ct eq 'bool') { + return 1 if ($value eq '1') || ($value =~ m/^(on|yes|true)$/i); + return 0 if ($value eq '0') || ($value =~ m/^(off|no|false)$/i); + return undef if $noerr; + die "type check ('boolean') failed - got '$value'\n"; + } elsif ($ct eq 'options') { + return verify_options($value, $noerr); + } elsif ($ct eq 'path') { + return verify_path($value, $noerr); + } elsif ($ct eq 'server') { + return verify_server($value, $noerr); + } elsif ($ct eq 'vgname') { + return parse_lvm_name ($value, $noerr); + } elsif ($ct eq 'portal') { + return verify_portal($value, $noerr); + } elsif ($ct eq 'nodes') { + my $res = {}; + + foreach my $node (PVE::Tools::split_list($value)) { + if (PVE::JSONSchema::pve_verify_node_name($node, $noerr)) { + $res->{$node} = 1; + } + } + + # no node restrictions for local storage + if ($storeid && $storeid eq 'local' && scalar(keys(%$res))) { + return undef if $noerr; + die "storage '$storeid' does not allow node restrictions\n"; + } + + return $res; + } elsif ($ct eq 'target') { + return $value; + } elsif ($ct eq 'string') { + return $value; + } elsif ($ct eq 'format') { + my $valid_formats = $def->{format}->[0]; + + if (!$valid_formats->{$value}) { + return undef if $noerr; + die "storage does not support format '$value'\n"; + } + + return $value; + + } elsif ($ct eq 'content') { + my $valid_content = $def->{content}->[0]; + + my $res = {}; + + foreach my $c (PVE::Tools::split_list($value)) { + if (!$valid_content->{$c}) { + return undef if $noerr; + die "storage does not support content type '$c'\n"; + } + $res->{$c} = 1; + } + + # only local storage may have several content types + if ($res->{none} || !($storeid && $storeid eq 'local')) { + if (scalar (keys %$res) > 1) { + return undef if $noerr; + die "storage does not support multiple content types\n"; + } + } + + # no backup to local storage + if ($storeid && $storeid eq 'local' && $res->{backup}) { + return undef if $noerr; + die "storage 'local' does not support backups\n"; + } + + return $res; + } elsif ($ct eq 'volume') { + return $value if parse_volume_id ($value, $noerr); + } + + return undef if $noerr; + die "type check not implemented - internal error\n"; +} + +sub parse_config { + my ($filename, $raw) = @_; + + my $ids = {}; + + my $sha1 = Digest::SHA1->new; + + my $pri = 0; + + while ($raw && $raw =~ s/^(.*?)(\n|$)//) { + my $line = $1; + + $sha1->add ($line); # compute digest + + next if $line =~ m/^\#/; + next if $line =~ m/^\s*$/; + + if ($line =~ m/^(\S+):\s*(\S+)\s*$/) { + my $storeid = $2; + my $type = $1; + my $ignore = 0; + + if (!parse_storage_id ($storeid, 1)) { + $ignore = 1; + warn "ignoring storage '$storeid' - (illegal characters)\n"; + } elsif (!$default_config->{$type}) { + $ignore = 1; + warn "ignoring storage '$storeid' (unsupported type '$type')\n"; + } else { + $ids->{$storeid}->{type} = $type; + $ids->{$storeid}->{priority} = $pri++; + } + + while ($raw && $raw =~ s/^(.*?)(\n|$)//) { + $line = $1; + + next if $line =~ m/^\#/; + last if $line =~ m/^\s*$/; + + next if $ignore; # skip + + if ($line =~ m/^\s+(\S+)(\s+(.*\S))?\s*$/) { + my ($k, $v) = ($1, $3); + if (my $ct = $confvars->{$k}) { + $v = 1 if $ct eq 'bool' && !defined($v); + eval { + $ids->{$storeid}->{$k} = check_type ($type, $ct, $k, $v, $storeid); + }; + warn "storage '$storeid' - unable to parse value of '$k': $@" if $@; + } else { + warn "storage '$storeid' - unable to parse value of '$k'\n"; + } + + } else { + warn "storage '$storeid' - ignore config line: $line\n"; + } + } + } else { + warn "ignore config line: $line\n"; + } + } + + # make sure we have a reasonable 'local:' storage + # openvz expects things to be there + if (!$ids->{local} || $ids->{local}->{type} ne 'dir' || + $ids->{local}->{path} ne '/var/lib/vz') { + $ids->{local} = { + type => 'dir', + priority => $pri++, + path => '/var/lib/vz', + content => { images => 1, rootdir => 1, vztmpl => 1, iso => 1}, + }; + } + + # we always need this for OpenVZ + $ids->{local}->{content}->{rootdir} = 1; + $ids->{local}->{content}->{vztmpl} = 1; + delete ($ids->{local}->{disable}); + + # remove node restrictions for local storage + delete($ids->{local}->{nodes}); + + foreach my $storeid (keys %$ids) { + my $d = $ids->{$storeid}; + + my $req_keys = $required_config->{$d->{type}}; + foreach my $k (@$req_keys) { + if (!defined ($d->{$k})) { + warn "ignoring storage '$storeid' - missing value " . + "for required option '$k'\n"; + delete $ids->{$storeid}; + next; + } + } + + my $def = $default_config->{$d->{type}}; + + if ($def->{content}) { + $d->{content} = $def->{content}->[1] if !$d->{content}; + } + + if ($d->{type} eq 'iscsi' || $d->{type} eq 'nfs') { + $d->{shared} = 1; + } + } + + my $digest = $sha1->hexdigest; + + my $cfg = { ids => $ids, digest => $digest}; + + return $cfg; +} + +sub parse_options { + my ($storeid, $stype, $param, $create) = @_; + + my $settings = { type => $stype }; + + die "unknown storage type '$stype'\n" + if !$default_config->{$stype}; + + foreach my $opt (keys %$param) { + my $value = $param->{$opt}; + + my $ct = $confvars->{$opt}; + if (defined($value)) { + eval { + $settings->{$opt} = check_type ($stype, $ct, $opt, $value, $storeid); + }; + raise_param_exc({ $opt => $@ }) if $@; + } else { + raise_param_exc({ $opt => "got undefined value" }); + } + } + + if ($create) { + my $req_keys = $required_config->{$stype}; + foreach my $k (@$req_keys) { + + if ($stype eq 'nfs' && !$settings->{path}) { + $settings->{path} = "/mnt/pve/$storeid"; + } + + # check if we have a value for all required options + if (!defined ($settings->{$k})) { + raise_param_exc({ $k => "property is missing and it is not optional" }); + } + } + } else { + my $fixed_keys = $fixed_config->{$stype}; + foreach my $k (@$fixed_keys) { + + # only allow to change non-fixed values + + if (defined ($settings->{$k})) { + raise_param_exc({$k => "can't change value (fixed parameter)"}); + } + } + } + + return $settings; +} + +sub cluster_lock_storage { + my ($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 storage_config { + my ($cfg, $storeid, $noerr) = @_; + + die "no storage id specified\n" if !$storeid; + + my $scfg = $cfg->{ids}->{$storeid}; + + die "storage '$storeid' does not exists\n" if (!$noerr && !$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'" 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); +} + +sub storage_ids { + my ($cfg) = @_; + + my $ids = $cfg->{ids}; + + my @sa = sort {$ids->{$a}->{priority} <=> $ids->{$b}->{priority}} keys %$ids; + + return @sa; +} + +sub assert_if_modified { + my ($cfg, $digest) = @_; + + if ($digest && ($cfg->{digest} ne $digest)) { + die "detected modified storage configuration - try again\n"; + } +} + +sub sprint_config_line { + my ($k, $v) = @_; + + my $ct = $confvars->{$k}; + + if ($ct eq 'bool') { + return $v ? "\t$k\n" : ''; + } elsif ($ct eq 'nodes') { + my $nlist = join(',', keys(%$v)); + return $nlist ? "\tnodes $nlist\n" : ''; + } elsif ($ct eq 'content') { + my $clist = content_hash_to_string($v); + if ($clist) { + return "\t$k $clist\n"; + } else { + return "\t$k none\n"; + } + } else { + return "\t$k $v\n"; + } +} + +sub write_config { + my ($filename, $cfg) = @_; + + my $out = ''; + + my $ids = $cfg->{ids}; + + my $maxpri = 0; + foreach my $storeid (keys %$ids) { + my $pri = $ids->{$storeid}->{priority}; + $maxpri = $pri if $pri && $pri > $maxpri; + } + foreach my $storeid (keys %$ids) { + if (!defined ($ids->{$storeid}->{priority})) { + $ids->{$storeid}->{priority} = ++$maxpri; + } + } + + foreach my $storeid (sort {$ids->{$a}->{priority} <=> $ids->{$b}->{priority}} keys %$ids) { + my $scfg = $ids->{$storeid}; + my $type = $scfg->{type}; + my $def = $default_config->{$type}; + + die "unknown storage type '$type'\n" if !$def; + + my $data = "$type: $storeid\n"; + + $data .= "\tdisable\n" if $scfg->{disable}; + + my $done_hash = { disable => 1}; + foreach my $k (@{$required_config->{$type}}) { + $done_hash->{$k} = 1; + my $v = $ids->{$storeid}->{$k}; + die "storage '$storeid' - missing value for required option '$k'\n" + if !defined ($v); + $data .= sprint_config_line ($k, $v); + } + + foreach my $k (keys %$def) { + next if defined ($done_hash->{$k}); + if (defined (my $v = $ids->{$storeid}->{$k})) { + $data .= sprint_config_line ($k, $v); + } + } + + $out .= "$data\n"; + } + + return $out; +} + +sub get_image_dir { + my ($cfg, $storeid, $vmid) = @_; + + my $path = $cfg->{ids}->{$storeid}->{path}; + return $vmid ? "$path/images/$vmid" : "$path/images"; +} + +sub get_iso_dir { + my ($cfg, $storeid) = @_; + + my $isodir = $cfg->{ids}->{$storeid}->{path}; + $isodir .= '/template/iso' if $storeid eq 'local'; + + return $isodir; +} + +sub get_vztmpl_dir { + my ($cfg, $storeid) = @_; + + my $tmpldir = $cfg->{ids}->{$storeid}->{path}; + $tmpldir .= '/template/cache' if $storeid eq 'local'; + + return $tmpldir; +} + +# iscsi utility functions + +sub iscsi_session_list { + + check_iscsi_support (); + + my $cmd = [$ISCSIADM, '--mode', 'session']; + + my $res = {}; + + run_command ($cmd, outfunc => sub { + my $line = shift; + + if ($line =~ m/^tcp:\s+\[(\S+)\]\s+\S+\s+(\S+)\s*$/) { + my ($session, $target) = ($1, $2); + # there can be several sessions per target (multipath) + push @{$res->{$target}}, $session; + + } + }); + + return $res; +} + +sub iscsi_test_portal { + my ($portal) = @_; + + my ($server, $port) = split(':', $portal); + my $p = Net::Ping->new("tcp", 2); + $p->port_number($port || 3260); + return $p->ping($server); +} + +sub iscsi_discovery { + my ($portal) = @_; + + check_iscsi_support (); + + my $cmd = [$ISCSIADM, '--mode', 'discovery', '--type', 'sendtargets', + '--portal', $portal]; + + my $res = {}; + + return $res if !iscsi_test_portal($portal); # fixme: raise exception here? + + run_command ($cmd, outfunc => sub { + my $line = shift; + + if ($line =~ m/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\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 $@; + + my $cmd = [$ISCSIADM, '--mode', 'node', '--targetname', $target, '--login']; + run_command ($cmd); +} + +sub iscsi_logout { + my ($target, $portal) = @_; + + check_iscsi_support (); + + my $cmd = [$ISCSIADM, '--mode', 'node', '--targetname', $target, '--logout']; + run_command ($cmd); +} + +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', '-r', $session, '-R']; + eval { run_command ($cmd, outfunc => sub {}); }; + warn $@ if $@; + } +} + +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 / 2), + '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; +} + +# library implementation + + +PVE::JSONSchema::register_format('pve-storage-id', \&parse_storage_id); +sub parse_storage_id { + my ($storeid, $noerr) = @_; + + if ($storeid !~ m/^[a-z][a-z0-9\-\_\.]*[a-z0-9]$/i) { + return undef if $noerr; + die "storage ID '$storeid' contains illegal characters\n"; + } + return $storeid; +} + +PVE::JSONSchema::register_standard_option('pve-storage-id', { + description => "The storage identifier.", + type => 'string', format => 'pve-storage-id', +}); + +PVE::JSONSchema::register_format('pve-storage-vgname', \&parse_lvm_name); +sub parse_lvm_name { + my ($name, $noerr) = @_; + + if ($name !~ m/^[a-z][a-z0-9\-\_\.]*[a-z0-9]$/i) { + return undef if $noerr; + die "lvm name '$name' contains illegal characters\n"; + } + + return $name; +} + +sub parse_vmid { + my $vmid = shift; + + die "VMID '$vmid' contains illegal characters\n" if $vmid !~ m/^\d+$/; + + return int($vmid); +} + +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"; +} + +sub parse_name_dir { + my $name = shift; + + if ($name =~ m!^([^/\s]+\.(raw|qcow2|vmdk))$!) { + return ($1, $2); + } + + die "unable to parse volume filename '$name'\n"; +} + +sub parse_volname_dir { + my $volname = shift; + + if ($volname =~ m!^(\d+)/(\S+)$!) { + my ($vmid, $name) = ($1, $2); + parse_name_dir ($name); + return ('image', $name, $vmid); + } elsif ($volname =~ m!^iso/([^/]+\.[Ii][Ss][Oo])$!) { + return ('iso', $1); + } elsif ($volname =~ m!^vztmpl/([^/]+\.tar\.gz)$!) { + return ('vztmpl', $1); + } + die "unable to parse directory volume name '$volname'\n"; +} + +sub parse_volname_lvm { + my $volname = shift; + + parse_lvm_name ($volname); + + if ($volname =~ m/^(vm-(\d+)-\S+)$/) { + return ($1, $2); + } + + die "unable to parse lvm volume name '$volname'\n"; +} + +sub parse_volname_iscsi { + my $volname = shift; + + if ($volname =~ m!^\d+\.\d+\.\d+\.(\S+)$!) { + my $byid = $1; + return $byid; + } + + die "unable to parse iscsi volume name '$volname'\n"; +} + +# 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 ($ids->{$sid} && (my $type = $ids->{$sid}->{type})) { + if ($type eq 'dir' || $type eq 'nfs') { + my ($vtype, $name, $vmid) = parse_volname_dir ($volname); + return ($vtype, $path); + } + } + return (''); + } + + $path = abs_path ($path); + + foreach my $sid (keys %$ids) { + my $type = $ids->{$sid}->{type}; + next if !($type eq 'dir' || $type eq 'nfs'); + + my $imagedir = $ids->{$sid}->{path} . "/images"; + my $isodir = get_iso_dir ($cfg, $sid); + my $tmpldir = get_vztmpl_dir ($cfg, $sid); + + if ($path =~ m!^$imagedir/(\d+)/([^/\s]+)$!) { + my $vmid = $1; + my $name = $2; + return ('image', "$sid:$vmid/$name"); + } elsif ($path =~ m!^$isodir/([^/]+\.[Ii][Ss][Oo])$!) { + my $name = $1; + return ('iso', "$sid:iso/$name"); + } elsif ($path =~ m!^$tmpldir/([^/]+\.tar\.gz)$!) { + my $name = $1; + return ('vztmpl', "$sid:vztmpl/$name"); + } + } + + # can't map path to volume id + return (''); +} + +sub path { + my ($cfg, $volid) = @_; + + my ($storeid, $volname) = parse_volume_id ($volid); + + my $scfg = storage_config ($cfg, $storeid); + + my $path; + my $owner; + + if ($scfg->{type} eq 'dir' || $scfg->{type} eq 'nfs') { + my ($vtype, $name, $vmid) = parse_volname_dir ($volname); + $owner = $vmid; + + my $imagedir = get_image_dir ($cfg, $storeid, $vmid); + my $isodir = get_iso_dir ($cfg, $storeid); + my $tmpldir = get_vztmpl_dir ($cfg, $storeid); + + if ($vtype eq 'image') { + $path = "$imagedir/$name"; + } elsif ($vtype eq 'iso') { + $path = "$isodir/$name"; + } elsif ($vtype eq 'vztmpl') { + $path = "$tmpldir/$name"; + } else { + die "should not be reached"; + } + + } elsif ($scfg->{type} eq 'lvm') { + + my $vg = $scfg->{vgname}; + + my ($name, $vmid) = parse_volname_lvm ($volname); + $owner = $vmid; + + $path = "/dev/$vg/$name"; + + } elsif ($scfg->{type} eq 'iscsi') { + my $byid = parse_volname_iscsi ($volname); + $path = "/dev/disk/by-id/$byid"; + } else { + die "unknown storage type '$scfg->{type}'"; + } + + return wantarray ? ($path, $owner) : $path; +} + +sub storage_migrate { + my ($cfg, $volid, $target_host, $target_storeid, $target_volname) = @_; + + my ($storeid, $volname) = parse_volume_id ($volid); + $target_volname = $volname if !$target_volname; + + my $scfg = storage_config ($cfg, $storeid); + + # no need to migrate shared content + return if $storeid eq $target_storeid && $scfg->{shared}; + + my $tcfg = storage_config ($cfg, $target_storeid); + + my $target_volid = "${target_storeid}:${target_volname}"; + + my $errstr = "unable to migrate '$volid' to '${target_volid}' on host '$target_host'"; + + # blowfish is a fast block cipher, much faster then 3des + my $sshoptions = "-c blowfish -o 'BatchMode=yes'"; + my $ssh = "/usr/bin/ssh $sshoptions"; + + local $ENV{RSYNC_RSH} = $ssh; + + if ($scfg->{type} eq 'dir' || $scfg->{type} eq 'nfs') { + if ($tcfg->{type} eq 'dir' || $tcfg->{type} eq 'nfs') { + + my $src = path ($cfg, $volid); + my $dst = path ($cfg, $target_volid); + + my $dirname = dirname ($dst); + + if ($tcfg->{shared}) { # we can do a local copy + + run_command (['/bin/mkdir', '-p', $dirname]); + + run_command (['/bin/cp', $src, $dst]); + + } else { + + run_command (['/usr/bin/ssh', "root\@${target_host}", + '/bin/mkdir', '-p', $dirname]); + + # we use rsync with --sparse, so we can't use --inplace, + # so we remove file on the target if it already exists to + # save space + my ($size, $format) = file_size_info($src); + if ($format && ($format eq 'raw') && $size) { + run_command (['/usr/bin/ssh', "root\@${target_host}", + 'rm', '-f', $dst], + outfunc => sub {}); + } + + my $cmd = ['/usr/bin/rsync', '--progress', '--sparse', '--whole-file', + $src, "root\@${target_host}:$dst"]; + + my $percent = -1; + + run_command ($cmd, outfunc => sub { + my $line = shift; + + if ($line =~ m/^\s*(\d+\s+(\d+)%\s.*)$/) { + if ($2 > $percent) { + $percent = $2; + print "rsync status: $1\n"; + *STDOUT->flush(); + } + } else { + print "$line\n"; + *STDOUT->flush(); + } + }); + } + + + } else { + + die "$errstr - target type '$tcfg->{type}' not implemented\n"; + } + + } else { + die "$errstr - source type '$scfg->{type}' not implemented\n"; + } +} + +sub vdisk_alloc { + my ($cfg, $storeid, $vmid, $fmt, $name, $size) = @_; + + die "no storage id specified\n" if !$storeid; + + parse_storage_id ($storeid); + + my $scfg = storage_config ($cfg, $storeid); + + die "no VMID specified\n" if !$vmid; + + $vmid = parse_vmid ($vmid); + + my $defformat = storage_default_format ($cfg, $storeid); + + $fmt = $defformat if !$fmt; + + activate_storage ($cfg, $storeid); + + # lock shared storage + return cluster_lock_storage($storeid, $scfg->{shared}, undef, sub { + + if ($scfg->{type} eq 'dir' || $scfg->{type} eq 'nfs') { + + my $imagedir = get_image_dir ($cfg, $storeid, $vmid); + + mkpath $imagedir; + + if (!$name) { + + for (my $i = 1; $i < 100; $i++) { + my @gr = <$imagedir/vm-$vmid-disk-$i.*>; + if (!scalar(@gr)) { + $name = "vm-$vmid-disk-$i.$fmt"; + last; + } + } + } + + die "unable to allocate an image name for VM $vmid in storage '$storeid'\n" + 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 -f $path; + + run_command("/usr/bin/qemu-img create -f $fmt '$path' ${size}K", + errmsg => "unable to create image"); + + return "$storeid:$vmid/$name"; + + } elsif ($scfg->{type} eq 'lvm') { + + die "unsupported format '$fmt'" if $fmt ne 'raw'; + + die "illegal name '$name' - sould be 'vm-$vmid-*'\n" + if $name && $name !~ m/^vm-$vmid-/; + + my $vgs = lvm_vgs (); + + my $vg = $scfg->{vgname}; + + die "no such volume gruoup '$vg'\n" if !defined ($vgs->{$vg}); + + my $free = int ($vgs->{$vg}->{free}); + + die "not enough free space ($free < $size)\n" if $free < $size; + + if (!$name) { + my $lvs = lvm_lvs ($vg); + + for (my $i = 1; $i < 100; $i++) { + my $tn = "vm-$vmid-disk-$i"; + if (!defined ($lvs->{$vg}->{$tn})) { + $name = $tn; + last; + } + } + } + + die "unable to allocate an image name for VM $vmid in storage '$storeid'\n" + if !$name; + + my $cmd = ['/sbin/lvcreate', '--addtag', "pve-vm-$vmid", '--size', "${size}k", '--name', $name, $vg]; + + run_command ($cmd); + + return "$storeid:$name"; + + } elsif ($scfg->{type} eq 'iscsi') { + die "can't allocate space in iscsi storage\n"; + } else { + die "unknown storage type '$scfg->{type}'"; + } + }); +} + +sub vdisk_free { + my ($cfg, $volid) = @_; + + my ($storeid, $volname) = parse_volume_id ($volid); + + my $scfg = storage_config ($cfg, $storeid); + + activate_storage ($cfg, $storeid); + + # lock shared storage + cluster_lock_storage($storeid, $scfg->{shared}, undef, sub { + + if ($scfg->{type} eq 'dir' || $scfg->{type} eq 'nfs') { + my $path = path ($cfg, $volid); + + if (! -f $path) { + warn "disk image '$path' does not exists\n"; + } else { + unlink $path; + } + } elsif ($scfg->{type} eq 'lvm') { + + my $vg = $scfg->{vgname}; + + my $cmd = ['/sbin/lvremove', '-f', "$vg/$volname"]; + + run_command ($cmd); + } elsif ($scfg->{type} eq 'iscsi') { + die "can't free space in iscsi storage\n"; + } else { + die "unknown storage type '$scfg->{type}'"; + } + }); +} + +# lvm utility functions + +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 => $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); + + $cmd = ['/sbin/vgcreate', $vgname, $device]; + # push @$cmd, '-c', 'y' if $shared; # we do not use this yet + + run_command ($cmd); +} + +sub lvm_vgs { + + my $cmd = ['/sbin/vgs', '--separator', ':', '--noheadings', '--units', 'b', + '--unbuffered', '--nosuffix', '--options', + 'vg_name,vg_size,vg_free']; + + my $vgs = {}; + run_command ($cmd, outfunc => sub { + my $line = shift; + + $line = trim($line); + + my ($name, $size, $free) = split (':', $line); + + $vgs->{$name} = { size => int ($size), free => int ($free) }; + }); + + return $vgs; +} + +sub lvm_lvs { + my ($vgname) = @_; + + my $cmd = ['/sbin/lvs', '--separator', ':', '--noheadings', '--units', 'b', + '--unbuffered', '--nosuffix', '--options', + 'vg_name,lv_name,lv_size,uuid,tags']; + + push @$cmd, $vgname if $vgname; + + my $lvs = {}; + run_command ($cmd, outfunc => sub { + my $line = shift; + + $line = trim($line); + + my ($vg, $name, $size, $uuid, $tags) = split (':', $line); + + return if $name !~ m/^vm-(\d+)-/; + my $nid = $1; + + my $owner; + foreach my $tag (split (/,/, $tags)) { + if ($tag =~ m/^pve-vm-(\d+)$/) { + $owner = $1; + last; + } + } + + if ($owner) { + if ($owner ne $nid) { + warn "owner mismatch name = $name, owner = $owner\n"; + } + + $lvs->{$vg}->{$name} = { format => 'raw', size => $size, + uuid => $uuid, tags => $tags, + vmid => $owner }; + } + }); + + return $lvs; +} + +#install iso or openvz template ($tt = ) +# we simply overwrite when file already exists +sub install_template { + my ($cfg, $storeid, $tt, $srcfile, $destfile) = @_; + + my $scfg = storage_config ($cfg, $storeid); + + my $type = $scfg->{type}; + + die "invalid storage type '$type'" if !($type eq 'dir' || $type eq 'nfs'); + + my $path; + + if ($tt eq 'iso') { + die "file '$destfile' has no '.iso' extension\n" + if $destfile !~ m![^/]+\.[Ii][Ss][Oo]$!; + die "storage '$storeid' does not support 'iso' content\n" + if !$scfg->{content}->{iso}; + $path = get_iso_dir ($cfg, $storeid); + } elsif ($tt eq 'vztmpl') { + die "file '$destfile' has no '.tar.gz' extension\n" + if $destfile !~ m![^/]+\.tar\.gz$!; + die "storage '$storeid' does not support 'vztmpl' content\n" + if !$scfg->{content}->{vztmpl}; + $path = get_vztmpl_dir ($cfg, $storeid); + } else { + die "unknown template type '$tt'"; + } + + activate_storage ($cfg, $storeid); + + my $dest = "$path/$destfile"; + + my $cmd = ['cp', $srcfile, $dest]; + + eval { run_command ($cmd); }; + my $err = $@; + + if ($err) { + unlink $dest; + die $err; + } +} + +#list iso or openvz template ($tt = ) +sub template_list { + my ($cfg, $storeid, $tt) = @_; + + die "unknown template type '$tt'\n" if !($tt eq 'iso' || $tt eq 'vztmpl' || $tt eq 'backup'); + + 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 !storage_check_enabled($cfg, $sid, undef, 1); + + next if $tt eq 'iso' && !$scfg->{content}->{iso}; + next if $tt eq 'vztmpl' && !$scfg->{content}->{vztmpl}; + next if $tt eq 'backup' && !$scfg->{content}->{backup}; + + activate_storage ($cfg, $sid); + + if ($type eq 'dir' || $type eq 'nfs') { + + my $path; + if ($tt eq 'iso') { + $path = get_iso_dir ($cfg, $sid); + } elsif ($tt eq 'vztmpl') { + $path = get_vztmpl_dir ($cfg, $sid); + } elsif ($tt eq 'backup') { + $path = $scfg->{path}; + } else { + die "unknown template type '$tt'\n"; + } + + foreach my $fn (<$path/*>) { + + my $info; + + if ($tt eq 'iso') { + next if $fn !~ m!/([^/]+\.[Ii][Ss][Oo])$!; + + $info = { volid => "$sid:iso/$1", format => 'iso' }; + + } elsif ($tt eq 'vztmpl') { + next if $fn !~ m!/([^/]+\.tar\.gz)$!; + + $info = { volid => "$sid:vztmpl/$1", format => 'tgz' }; + + } elsif ($tt eq 'backup') { + next if $fn !~ m!/([^/]+\.(tar|tgz))$!; + + $info = { volid => "$sid:backup/$1", format => $2 }; + } + + $info->{size} = -s $fn; + + push @{$res->{$sid}}, $info; + } + + } + + @{$res->{$sid}} = sort {lc($a->{volid}) cmp lc ($b->{volid}) } @{$res->{$sid}} if $res->{$sid}; + } + + return $res; +} + +sub file_size_info { + my ($filename, $timeout) = @_; + + my $cmd = ['/usr/bin/qemu-img', 'info', $filename]; + + my $format; + my $size = 0; + my $used = 0; + + eval { + run_command ($cmd, timeout => $timeout, outfunc => sub { + my $line = shift; + + if ($line =~ m/^file format:\s+(\S+)\s*$/) { + $format = $1; + } elsif ($line =~ m/^virtual size:\s\S+\s+\((\d+)\s+bytes\)$/) { + $size = int($1); + } elsif ($line =~ m/^disk size:\s+(\d+(.\d+)?)([KMGT])\s*$/) { + $used = $1; + my $u = $3; + + $used *= 1024 if $u eq 'K'; + $used *= (1024*1024) if $u eq 'M'; + $used *= (1024*1024*1024) if $u eq 'G'; + $used *= (1024*1024*1024*1024) if $u eq 'T'; + + $used = int($used); + } + }); + }; + + return wantarray ? ($size, $format, $used) : $size; +} + +sub vdisk_list { + my ($cfg, $storeid, $vmid, $vollist) = @_; + + my $ids = $cfg->{ids}; + + storage_check_enabled($cfg, $storeid) if ($storeid); + + my $res = {}; + + # prepare/activate/refresh all storages + + my $stypes = {}; + + 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; + $stypes->{$ids->{$sid}->{type}} = 1; + } + } else { + foreach my $sid (keys %$ids) { + next if $storeid && $storeid ne $sid; + next if !storage_check_enabled($cfg, $sid, undef, 1); + push @$storage_list, $sid; + $stypes->{$ids->{$sid}->{type}} = 1; + } + } + + activate_storage_list ($cfg, $storage_list); + + my $lvs = $stypes->{lvm} ? lvm_lvs () : {}; + + my $iscsi_devices = iscsi_device_list() if $stypes->{iscsi}; + + # query the storage + + foreach my $sid (keys %$ids) { + if ($storeid) { + next if $storeid ne $sid; + next if !storage_check_enabled($cfg, $sid, undef, 1); + } + my $scfg = $ids->{$sid}; + my $type = $scfg->{type}; + + if ($type eq 'dir' || $type eq 'nfs') { + + my $path = $scfg->{path}; + + my $fmts = join ('|', keys %{$default_config->{$type}->{format}->[0]}); + + foreach my $fn (<$path/images/[0-9][0-9]*/*>) { + + next if $fn !~ m!^(/.+/images/(\d+)/([^/]+\.($fmts)))$!; + $fn = $1; # untaint + + my $owner = $2; + my $name = $3; + my $volid = "$sid:$owner/$name"; + + if ($vollist) { + my $found = grep { $_ eq $volid } @$vollist; + next if !$found; + } else { + next if defined ($vmid) && ($owner ne $vmid); + } + + my ($size, $format, $used) = file_size_info ($fn); + + if ($format && $size) { + push @{$res->{$sid}}, { + volid => $volid, format => $format, + size => $size, vmid => $owner, used => $used }; + } + + } + + } elsif ($type eq 'lvm') { + + my $vgname = $scfg->{vgname}; + + if (my $dat = $lvs->{$vgname}) { + + foreach my $volname (keys %$dat) { + + my $owner = $dat->{$volname}->{vmid}; + + my $volid = "$sid:$volname"; + + if ($vollist) { + my $found = grep { $_ eq $volid } @$vollist; + next if !$found; + } else { + next if defined ($vmid) && ($owner ne $vmid); + } + + my $info = $dat->{$volname}; + $info->{volid} = $volid; + + push @{$res->{$sid}}, $info; + } + } + + } elsif ($type eq 'iscsi') { + + # we have no owner for iscsi devices + + my $target = $scfg->{target}; + + if (my $dat = $iscsi_devices->{$target}) { + + foreach my $volname (keys %$dat) { + + my $volid = "$sid:$volname"; + + if ($vollist) { + my $found = grep { $_ eq $volid } @$vollist; + next if !$found; + } else { + next if !($storeid && ($storeid eq $sid)); + } + + my $info = $dat->{$volname}; + $info->{volid} = $volid; + + push @{$res->{$sid}}, $info; + } + } + + } else { + die "implement me"; + } + + @{$res->{$sid}} = sort {lc($a->{volid}) cmp lc ($b->{volid}) } @{$res->{$sid}} if $res->{$sid}; + } + + return $res; +} + +sub nfs_is_mounted { + my ($server, $export, $mountpoint, $mountdata) = @_; + + my $source = "$server:$export"; + + $mountdata = read_proc_mounts() if !$mountdata; + + if ($mountdata =~ m/^$source\s$mountpoint\snfs/m) { + return $mountpoint; + } + + return undef; +} + +sub nfs_mount { + my ($server, $export, $mountpoint, $options) = @_; + + my $source = "$server:$export"; + + my $cmd = ['/bin/mount', '-t', 'nfs', $source, $mountpoint]; + if ($options) { + push @$cmd, '-o', $options; + } + + run_command ($cmd); +} + +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_full { + my ($cfg, $storeid, $session) = @_; + + my $scfg = storage_check_enabled($cfg, $storeid); + + return if $session->{activated}->{$storeid}; + + if (!$session->{mountdata}) { + $session->{mountdata} = read_proc_mounts(); + } + + if (!$session->{uevent_seqnum}) { + $session->{uevent_seqnum} = uevent_seqnum (); + } + + my $mountdata = $session->{mountdata}; + + my $type = $scfg->{type}; + + if ($type eq 'dir' || $type eq 'nfs') { + + my $path = $scfg->{path}; + + if ($type eq 'nfs') { + my $server = $scfg->{server}; + my $export = $scfg->{export}; + + if (!nfs_is_mounted ($server, $export, $path, $mountdata)) { + + # NOTE: only call mkpath when not mounted (avoid hang + # when NFS server is offline + + mkpath $path; + + die "unable to activate storage '$storeid' - " . + "directory '$path' does not exist\n" if ! -d $path; + + nfs_mount ($server, $export, $path, $scfg->{options}); + } + + } else { + + mkpath $path; + + die "unable to activate storage '$storeid' - " . + "directory '$path' does not exist\n" if ! -d $path; + } + + my $imagedir = get_image_dir ($cfg, $storeid); + my $isodir = get_iso_dir ($cfg, $storeid); + my $tmpldir = get_vztmpl_dir ($cfg, $storeid); + + if (defined($scfg->{content})) { + mkpath $imagedir if $scfg->{content}->{images} && + $imagedir ne $path; + mkpath $isodir if $scfg->{content}->{iso} && + $isodir ne $path; + mkpath $tmpldir if $scfg->{content}->{vztmpl} && + $tmpldir ne $path; + } + + } elsif ($type eq 'lvm') { + + if ($scfg->{base}) { + my ($baseid, undef) = parse_volume_id ($scfg->{base}); + __activate_storage_full ($cfg, $baseid, $session); + } + + if (!$session->{vgs}) { + $session->{vgs} = lvm_vgs(); + } + + # In LVM2, vgscans take place automatically; + # this is just to be sure + if ($session->{vgs} && !$session->{vgscaned} && + !$session->{vgs}->{$scfg->{vgname}}) { + $session->{vgscaned} = 1; + my $cmd = ['/sbin/vgscan', '--ignorelockingfailure', '--mknodes']; + eval { run_command ($cmd, outfunc => sub {}); }; + warn $@ if $@; + } + + my $cmd = ['/sbin/vgchange', '-aly', $scfg->{vgname}]; + run_command ($cmd, outfunc => sub {}); + + } elsif ($type eq 'iscsi') { + + return if !check_iscsi_support(1); + + $session->{iscsi_sessions} = iscsi_session_list() + if !$session->{iscsi_sessions}; + + my $iscsi_sess = $session->{iscsi_sessions}->{$scfg->{target}}; + if (!defined ($iscsi_sess)) { + eval { iscsi_login ($scfg->{target}, $scfg->{portal}); }; + warn $@ if $@; + } else { + # make sure we get all devices + iscsi_session_rescan ($iscsi_sess); + } + + } else { + die "implement me"; + } + + my $newseq = uevent_seqnum (); + + # only call udevsettle if there are events + if ($newseq > $session->{uevent_seqnum}) { + my $timeout = 30; + system ("$UDEVADM settle --timeout=$timeout"); # ignore errors + $session->{uevent_seqnum} = $newseq; + } + + $session->{activated}->{$storeid} = 1; +} + +sub activate_storage_list { + my ($cfg, $storeid_list, $session) = @_; + + $session = {} if !$session; + + foreach my $storeid (@$storeid_list) { + __activate_storage_full ($cfg, $storeid, $session); + } +} + +sub activate_storage { + my ($cfg, $storeid) = @_; + + my $session = {}; + + __activate_storage_full ($cfg, $storeid, $session); +} + +sub activate_volumes { + my ($cfg, $vollist) = @_; + + my $storagehash = {}; + foreach my $volid (@$vollist) { + my ($storeid, undef) = parse_volume_id ($volid); + $storagehash->{$storeid} = 1; + } + + activate_storage_list ($cfg, [keys %$storagehash]); + + foreach my $volid (@$vollist) { + my ($storeid, $volname) = parse_volume_id ($volid); + + my $scfg = storage_config ($cfg, $storeid); + + my $path = path ($cfg, $volid); + + if ($scfg->{type} eq 'lvm') { + my $cmd = ['/sbin/lvchange', '-aly', $path]; + eval { run_command ($cmd); }; + warn $@ if $@; + } + + # check is volume exists + if ($scfg->{type} eq 'dir' || $scfg->{type} eq 'nfs') { + die "volume '$volid' does not exist\n" if ! -f $path; + } else { + die "volume '$volid' does not exist\n" if ! -b $path; + } + } +} + +sub deactivate_volumes { + my ($cfg, $vollist) = @_; + + my $lvs = lvm_lvs (); + + foreach my $volid (@$vollist) { + my ($storeid, $volname) = parse_volume_id ($volid); + + my $scfg = storage_config ($cfg, $storeid); + + if ($scfg->{type} eq 'lvm') { + my ($name) = parse_volname_lvm ($volname); + + if ($lvs->{$scfg->{vgname}}->{$name}) { + my $path = path ($cfg, $volid); + my $cmd = ['/sbin/lvchange', '-aln', $path]; + eval { run_command ($cmd); }; + warn $@ if $@; + } + } + } +} + +sub deactivate_storage { + my ($cfg, $storeid) = @_; + + my $iscsi_sessions; + + my $scfg = storage_config ($cfg, $storeid); + + my $type = $scfg->{type}; + + if ($type eq 'dir') { + # nothing to do + } elsif ($type eq 'nfs') { + my $mountdata = read_proc_mounts(); + my $server = $scfg->{server}; + my $export = $scfg->{export}; + my $path = $scfg->{path}; + + my $cmd = ['/bin/umount', $path]; + + run_command ($cmd) if nfs_is_mounted ($server, $export, $path, $mountdata); + } elsif ($type eq 'lvm') { + my $cmd = ['/sbin/vgchange', '-aln', $scfg->{vgname}]; + run_command ($cmd); + } elsif ($type eq 'iscsi') { + my $portal = $scfg->{portal}; + my $target = $scfg->{target}; + + my $iscsi_sessions = iscsi_session_list(); + iscsi_logout ($target, $portal) + if defined ($iscsi_sessions->{$target}); + + } else { + die "implement me"; + } +} + +sub storage_info { + my ($cfg, $content) = @_; + + my $ids = $cfg->{ids}; + + my $info = {}; + my $stypes = {}; + + my $slist = []; + foreach my $storeid (keys %$ids) { + + next if $content && !$ids->{$storeid}->{content}->{$content}; + + next if !storage_check_enabled($cfg, $storeid, undef, 1); + + my $type = $ids->{$storeid}->{type}; + + $info->{$storeid} = { + type => $type, + total => 0, + avail => 0, + used => 0, + content => content_hash_to_string($ids->{$storeid}->{content}), + active => 0, + }; + + $stypes->{$type} = 1; + + push @$slist, $storeid; + } + + my $session = {}; + my $mountdata = ''; + my $iscsi_sessions = {}; + my $vgs = {}; + + if ($stypes->{lvm}) { + $session->{vgs} = lvm_vgs(); + $vgs = $session->{vgs}; + } + if ($stypes->{nfs}) { + $mountdata = read_proc_mounts(); + $session->{mountdata} = $mountdata; + } + if ($stypes->{iscsi}) { + $iscsi_sessions = iscsi_session_list(); + $session->{iscsi_sessions} = $iscsi_sessions; + } + + eval { activate_storage_list ($cfg, $slist, $session); }; + + foreach my $storeid (keys %$ids) { + my $scfg = $ids->{$storeid}; + + next if !$info->{$storeid}; + + my $type = $scfg->{type}; + + if ($type eq 'dir' || $type eq 'nfs') { + + my $path = $scfg->{path}; + + if ($type eq 'nfs') { + my $server = $scfg->{server}; + my $export = $scfg->{export}; + + next if !nfs_is_mounted ($server, $export, $path, $mountdata); + } + + my $timeout = 2; + my $res = PVE::Tools::df($path, $timeout); + + next if !$res || !$res->{total}; + + $info->{$storeid}->{total} = $res->{total}; + $info->{$storeid}->{avail} = $res->{avail}; + $info->{$storeid}->{used} = $res->{used}; + $info->{$storeid}->{active} = 1; + + } elsif ($type eq 'lvm') { + + my $vgname = $scfg->{vgname}; + + my $total = 0; + my $free = 0; + + if (defined ($vgs->{$vgname})) { + $total = $vgs->{$vgname}->{size}; + $free = $vgs->{$vgname}->{free}; + + $info->{$storeid}->{total} = $total; + $info->{$storeid}->{avail} = $free; + $info->{$storeid}->{used} = $total - $free; + $info->{$storeid}->{active} = 1; + } + + } elsif ($type eq 'iscsi') { + + $info->{$storeid}->{total} = 0; + $info->{$storeid}->{avail} = 0; + $info->{$storeid}->{used} = 0; + $info->{$storeid}->{active} = + defined ($iscsi_sessions->{$scfg->{target}}); + + } else { + die "implement me"; + } + } + + return $info; +} + +sub resolv_server { + my ($server) = @_; + + my $packed_ip = gethostbyname($server); + if (defined $packed_ip) { + return inet_ntoa($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 resolv_portal { + my ($portal, $noerr) = @_; + + if ($portal =~ m/^([^:]+)(:(\d+))?$/) { + my $server = $1; + my $port = $3; + + if (my $ip = resolv_server($server)) { + $server = $ip; + return $port ? "$server:$port" : $server; + } + } + return undef if $noerr; + + raise_param_exc({ portal => "unable to resolve portal address '$portal'" }); +} + +# idea is from usbutils package (/usr/bin/usb-devices) script +sub __scan_usb_device { + my ($res, $devpath, $parent, $level) = @_; + + return if ! -d $devpath; + return if $level && $devpath !~ m/^.*[-.](\d+)$/; + my $port = $level ? int($1 - 1) : 0; + + my $busnum = int(file_read_firstline("$devpath/busnum")); + my $devnum = int(file_read_firstline("$devpath/devnum")); + + my $d = { + port => $port, + level => $level, + busnum => $busnum, + devnum => $devnum, + speed => file_read_firstline("$devpath/speed"), + class => hex(file_read_firstline("$devpath/bDeviceClass")), + vendid => file_read_firstline("$devpath/idVendor"), + prodid => file_read_firstline("$devpath/idProduct"), + }; + + if ($level) { + my $usbpath = $devpath; + $usbpath =~ s|^.*/\d+\-||; + $d->{usbpath} = $usbpath; + } + + my $product = file_read_firstline("$devpath/product"); + $d->{product} = $product if $product; + + my $manu = file_read_firstline("$devpath/manufacturer"); + $d->{manufacturer} = $manu if $manu; + + my $serial => file_read_firstline("$devpath/serial"); + $d->{serial} = $serial if $serial; + + push @$res, $d; + + foreach my $subdev (<$devpath/$busnum-*>) { + next if $subdev !~ m|/$busnum-[0-9]+(\.[0-9]+)*$|; + __scan_usb_device($res, $subdev, $devnum, $level + 1); + } + +}; + +sub scan_usb { + + my $devlist = []; + + foreach my $device () { + __scan_usb_device($devlist, $device, 0, 0); + } + + return $devlist; +} + +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 iscsi_discovery($portal); +} + +sub storage_default_format { + my ($cfg, $storeid) = @_; + + my $scfg = storage_config ($cfg, $storeid); + + my $def = $default_config->{$scfg->{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; +} + +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"; + } + } + } +} + +1; diff --git a/README b/README new file mode 100644 index 0000000..89a365e --- /dev/null +++ b/README @@ -0,0 +1,74 @@ +STORAGE Design: +=============== + +pool: ability to create more than one volume + + - directory (NFS server, local dir) + + - LVM group + + - physical disk (partitions) ?? + + - ISCSI volume pools ?? + + - qemu base image ?? + + a pool can support several formats (raw, qcow2, vmdk, ...) + +volume: can be used for VM storage + + - block device + + - file (raw, qcow2, ...) + + - ISCSI LUN + +A pool is either shared of local. The resulting volume +inherits that property. + + +lvs --separator , --noheadings --units b --unbuffered --nosuffix --options "lv_name,uuid,devices,seg_size,vg_extent_size" + +pvs --noheadings -o pv_name,vg_name + +vgs --separator : --noheadings --units b --unbuffered --nosuffix --options "vg_size,vg_free" VGNAME + + +What about ISO/template storage? + +Storage Configuration: +====================== + +/etc/pve/storage.shared +/etc/pve/storage.local + +oder + +/etc/pve/storage.config + +mit node attribute for jeden pool. + +jedes volume kann einen owner haben (VMID)?? + + +Aus einem pool werden volumes generiert. Jedes volume is einer VMID zugeordnet, entweder +üder den Pfad im filesystem: + +$PATH/images/$VMID/xyz.qcow2 + +oder über lvm tags: + +pve-vm-$vmid + +Namen müssen pro storage 'unique' sein, daher werden folgende namen verwendet: + +vm-$VMID-disk-XXX.$EXT + +Nur mit einzigartigen namem kann man kurze storage-id generieren. + +store1:vm-100-disk-5 + +Configuration format: + +pool: + type \ No newline at end of file diff --git a/changelog.Debian b/changelog.Debian new file mode 100644 index 0000000..50d6ff0 --- /dev/null +++ b/changelog.Debian @@ -0,0 +1,85 @@ +libpve-storage-perl (2.0-4) unstable; urgency=low + + * return numeric values for channel/ID/LUN + + -- Proxmox Support Team Fri, 05 Aug 2011 08:46:58 +0200 + +libpve-storage-perl (2.0-3) unstable; urgency=low + + * implemented node restrictions (storage can be restricted to specific + nodes - i.e. DRBD) + + -- Proxmox Support Team Fri, 29 Jul 2011 08:55:11 +0200 + +libpve-storage-perl (2.0-2) unstable; urgency=low + + * backport fixes (multipath, cache) from stable + + -- Proxmox Support Team Mon, 25 Jul 2011 07:02:06 +0200 + +libpve-storage-perl (2.0-1) unstable; urgency=low + + * change copyright to AGPL + + -- Proxmox Support Team Thu, 19 Aug 2010 10:15:46 +0200 + +libpve-storage-perl (1.0-10) unstable; urgency=low + + * fix used space compute + + -- Proxmox Support Team Thu, 11 Feb 2010 10:48:58 +0100 + +libpve-storage-perl (1.0-9) unstable; urgency=low + + * also query used space as suggested by Slavio + + -- Proxmox Support Team Thu, 04 Feb 2010 08:57:02 +0100 + +libpve-storage-perl (1.0-8) unstable; urgency=low + + * also list vmdk files + + -- Proxmox Support Team Mon, 25 Jan 2010 11:52:43 +0100 + +libpve-storage-perl (1.0-7) unstable; urgency=low + + * fix iscsi device detection on kernel 2.6.32 + + -- Proxmox Support Team Mon, 18 Jan 2010 13:37:24 +0100 + +libpve-storage-perl (1.0-6) unstable; urgency=low + + * fix bug in parse_volume_id (ignore case) + + -- Proxmox Support Team Thu, 29 Oct 2009 09:22:37 +0100 + +libpve-storage-perl (1.0-5) unstable; urgency=low + + * fix bug in parse_volume_id + + -- Proxmox Support Team Tue, 27 Oct 2009 10:45:49 +0100 + +libpve-storage-perl (1.0-4) unstable; urgency=low + + * new functions to list backup files + + -- Proxmox Support Team Thu, 08 Oct 2009 13:34:45 +0200 + +libpve-storage-perl (1.0-3) unstable; urgency=low + + * new install/delete template functions + + -- Proxmox Support Team Wed, 07 Oct 2009 08:29:55 +0200 + +libpve-storage-perl (1.0-2) unstable; urgency=low + + * do not remove storage which is used as base for other storage. + + -- Proxmox Support Team Fri, 18 Sep 2009 08:05:32 +0200 + +libpve-storage-perl (1.0-1) unstable; urgency=low + + * initial package + + -- Proxmox Support Team Fri, 20 Mar 2009 11:13:19 +0100 + diff --git a/control.in b/control.in new file mode 100644 index 0000000..40b08df --- /dev/null +++ b/control.in @@ -0,0 +1,9 @@ +Package: libpve-storage-perl +Version: @@VERSION@@-@@PKGRELEASE@@ +Section: perl +Priority: optional +Architecture: @@ARCH@@ +Depends: perl (>= 5.6.0-16), nfs-common, udev, libpve-common-perl +Maintainer: Proxmox Support Team +Description: Proxmox VE storage management library + This package contains the storage management library used by Proxmox VE. diff --git a/copyright b/copyright new file mode 100644 index 0000000..f96f3fb --- /dev/null +++ b/copyright @@ -0,0 +1,16 @@ +Copyright (C) 2010 Proxmox Server Solutions GmbH + +This software is written by Proxmox Server Solutions GmbH + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . diff --git a/pvesm b/pvesm new file mode 100755 index 0000000..dbee738 --- /dev/null +++ b/pvesm @@ -0,0 +1,278 @@ +#!/usr/bin/perl -w + +use strict; +use Getopt::Long; +use Fcntl ':flock'; +use File::Path; + +use PVE::SafeSyslog; +use PVE::Cluster; +use PVE::INotify; +use PVE::RPCEnvironment; +use PVE::Storage; +use PVE::API2::Storage::Config; +use PVE::API2::Storage::Content; +use PVE::API2::Storage::Status; +use PVE::API2::Storage::Scan; +use PVE::JSONSchema qw(get_standard_option); + +use PVE::CLIHandler; + +use base qw(PVE::CLIHandler); + +$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin'; + +initlog ('pvesm'); + +die "please run as root\n" if $> != 0; + +PVE::INotify::inotify_init(); + +my $rpcenv = PVE::RPCEnvironment->init('cli'); + +$rpcenv->init_request(); +$rpcenv->set_language($ENV{LANG}); +$rpcenv->set_user('root@pam'); + +__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', + }, + }, + }, + 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; + + }}); + +my $print_content = sub { + my ($list) = @_; + + my $maxlenname = 0; + foreach my $info (@$list) { + + my $volid = $info->{volid}; + my $sidlen = length ($volid); + $maxlenname = $sidlen if $sidlen > $maxlenname; + } + + foreach my $info (@$list) { + next if !$info->{vmid}; + my $volid = $info->{volid}; + + printf "%-${maxlenname}s %5s %10d %d\n", $volid, + $info->{format}, $info->{size}, $info->{vmid}; + } + + foreach my $info (sort { $a->{format} cmp $b->{format} } @$list) { + next if $info->{vmid}; + my $volid = $info->{volid}; + + printf "%-${maxlenname}s %5s %10d\n", $volid, + $info->{format}, $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; + + foreach my $res (sort { $a->{storage} cmp $b->{storage} } @$res) { + my $storeid = $res->{storage}; + + my $sum = $res->{used} + $res->{avail}; + my $per = $sum ? (0.5 + ($res->{used}*100)/$sum) : 100; + + printf "%-${maxlen}s %5s %1d %15d %15d %15d %.2f%%\n", $storeid, + $res->{type}, $res->{active}, + $res->{total}/1024, $res->{used}/1024, $res->{avail}/1024, $per; + } +}; + +my $nodename = PVE::INotify::nodename(); + +my $cmddef = { + add => [ "PVE::API2::Storage::Config", 'create', ['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 "sucessfuly created '$volid'\n"; + }], + free => [ "PVE::API2::Storage::Content", 'delete', ['volume'], + { node => $nodename } ], + nfsscan => [ "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}; + } + }], + iscsiscan => [ "PVE::API2::Storage::Scan", 'iscsiscan', ['server'], + { 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}; + } + }], + lvmscan => [ "PVE::API2::Storage::Scan", 'lvmscan', [], + { node => $nodename }, sub { + my $res = shift; + foreach my $rec (@$res) { + printf "$rec->{vg}\n"; + } + }], + path => [ __PACKAGE__, 'path', ['volume']], +}; + +my $cmd = shift; + +if ($cmd && $cmd eq 'verifyapi') { + PVE::RESTHandler::validate_method_schemas(); + exit 0; +} + +PVE::CLIHandler::handle_cmd($cmddef, "pvesm", $cmd, \@ARGV); + +exit 0; + +__END__ + +=head1 NAME + +pvesm - PVE Storage Manager + +=head1 SYNOPSIS + + pvesm [OPTIONS] + + # scan iscsi host for available targets + pvesm scan iscsi + + # scan nfs server for available exports + pvesm scan nfs + + # add storage pools + pvesm add + pvesm add dir --path + pvesm add nfs --path --server --export + pvesm add lvm --vgname + pvesm add iscsi --portal --target + + # disable storage pools + pvesm set --disable 1 + + # enable storage pools + pvesm set --disable 0 + + # change/set storage options + pvesm set + pvesm set --shared 1 + pvesm set local --format qcow2 + pvesm set --content iso + + # remove storage pools - does not delete any data + pvesm remove + + # add single devices?? + + # alloc volumes + pvesm alloc [--format ] + + # alloc 4G volume in local storage - use auto generated name + pvesm alloc local '' 4G + + # free volumes (warning: destroy/deletes all volume data) + pvesm free + + # list storage status + pvesm status + + # list storage contents + pvesm list [--vmid ] + + # list volumes allocated by VMID + pvesm list --vmid + + # list iso images + pvesm list --iso + + # list openvz templates + pvesm list --vztmpl + + # show filesystem path for a volume + pvesm path + + # import disks ?? + + +=head1 DESCRIPTION + +=head2 Storage pools + +Each storage pool is uniquely identified by its . + +=head3 Storage content + +A storage can support several content types, for example virtual disk +images, cdrom iso images, openvz templates or openvz root directories +(C, C, C, C). + +=head2 Volumes + +A volume is identified by the , followed by a storage type +dependent volume name, separated by colon. A valid looks like: + + local:230/example-image.raw + + local:iso/debian-501-amd64-netinst.iso + + local:vztmpl/debian-5.0-joomla_1.5.9-1_i386.tar.gz + + iscsi-storage:0.0.2.scsi-14f504e46494c4500494b5042546d2d646744372d31616d61 + +To get the filesystem path for a use: + + pvesm path + -- 2.39.5