package PVE::Storage::CIFSPlugin; use strict; use warnings; use Net::IP; use PVE::Tools qw(run_command); use PVE::ProcFSTools; use File::Path; use PVE::Storage::Plugin; use PVE::JSONSchema qw(get_standard_option); use base qw(PVE::Storage::Plugin); # CIFS helper functions sub cifs_is_mounted { my ($server, $share, $mountpoint, $mountdata) = @_; $server = "[$server]" if Net::IP::ip_is_ipv6($server); my $source = "//${server}/$share"; $mountdata = PVE::ProcFSTools::parse_proc_mounts() if !$mountdata; return $mountpoint if grep { $_->[2] =~ /^cifs/ && $_->[0] =~ m|^\Q$source\E/?$| && $_->[1] eq $mountpoint } @$mountdata; return undef; } sub cifs_cred_file_name { my ($storeid) = @_; return "/etc/pve/priv/storage/${storeid}.pw"; } sub cifs_delete_credentials { my ($storeid) = @_; if (my $cred_file = get_cred_file($storeid)) { unlink($cred_file) or warn "removing cifs credientials '$cred_file' failed: $!\n"; } } sub cifs_set_credentials { my ($password, $storeid) = @_; my $cred_file = cifs_cred_file_name($storeid); mkdir "/etc/pve/priv/storage"; PVE::Tools::file_set_contents($cred_file, "password=$password\n"); return $cred_file; } sub get_cred_file { my ($storeid) = @_; my $cred_file = cifs_cred_file_name($storeid); if (-e $cred_file) { return $cred_file; } return undef; } sub cifs_mount { my ($server, $share, $mountpoint, $storeid, $smbver, $user, $domain) = @_; $server = "[$server]" if Net::IP::ip_is_ipv6($server); my $source = "//${server}/$share"; my $cmd = ['/bin/mount', '-t', 'cifs', $source, $mountpoint, '-o', 'soft', '-o']; if (my $cred_file = get_cred_file($storeid)) { push @$cmd, "username=$user", '-o', "credentials=$cred_file"; push @$cmd, '-o', "domain=$domain" if defined($domain); } else { push @$cmd, 'guest,username=guest'; } push @$cmd, '-o', defined($smbver) ? "vers=$smbver" : "vers=default"; run_command($cmd, errmsg => "mount error"); } # Configuration sub type { return 'cifs'; } sub plugindata { return { content => [ { images => 1, rootdir => 1, vztmpl => 1, iso => 1, backup => 1, snippets => 1}, { images => 1 }], format => [ { raw => 1, qcow2 => 1, vmdk => 1 } , 'raw' ], }; } sub properties { return { share => { description => "CIFS share.", type => 'string', }, password => { description => "Password for accessing the share/datastore.", type => 'string', maxLength => 256, }, domain => { description => "CIFS domain.", type => 'string', optional => 1, maxLength => 256, }, smbversion => { description => "SMB protocol version. 'default' if not set, negotiates the highest SMB2+" ." version supported by both the client and server.", type => 'string', default => 'default', enum => ['default', '2.0', '2.1', '3', '3.0', '3.11'], optional => 1, }, }; } sub options { return { path => { fixed => 1 }, server => { fixed => 1 }, share => { fixed => 1 }, nodes => { optional => 1 }, disable => { optional => 1 }, maxfiles => { optional => 1 }, 'prune-backups' => { optional => 1 }, 'max-protected-backups' => { optional => 1 }, content => { optional => 1 }, format => { optional => 1 }, username => { optional => 1 }, password => { optional => 1}, domain => { optional => 1}, smbversion => { optional => 1}, mkdir => { optional => 1 }, bwlimit => { optional => 1 }, preallocation => { optional => 1 }, }; } sub check_config { my ($class, $sectionId, $config, $create, $skipSchemaCheck) = @_; $config->{path} = "/mnt/pve/$sectionId" if $create && !$config->{path}; return $class->SUPER::check_config($sectionId, $config, $create, $skipSchemaCheck); } # Storage implementation sub on_add_hook { my ($class, $storeid, $scfg, %sensitive) = @_; if (defined($sensitive{password})) { cifs_set_credentials($sensitive{password}, $storeid); if (!exists($scfg->{username})) { warn "storage $storeid: ignoring password parameter, no user set\n"; } } else { cifs_delete_credentials($storeid); } return; } sub on_update_hook { my ($class, $storeid, $scfg, %sensitive) = @_; return if !exists($sensitive{password}); if (defined($sensitive{password})) { cifs_set_credentials($sensitive{password}, $storeid); if (!exists($scfg->{username})) { warn "storage $storeid: ignoring password parameter, no user set\n"; } } else { cifs_delete_credentials($storeid); } return; } sub on_delete_hook { my ($class, $storeid, $scfg) = @_; cifs_delete_credentials($storeid); return; } sub status { my ($class, $storeid, $scfg, $cache) = @_; $cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts() if !$cache->{mountdata}; my $path = $scfg->{path}; my $server = $scfg->{server}; my $share = $scfg->{share}; return undef if !cifs_is_mounted($server, $share, $path, $cache->{mountdata}); return $class->SUPER::status($storeid, $scfg, $cache); } sub activate_storage { my ($class, $storeid, $scfg, $cache) = @_; $cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts() if !$cache->{mountdata}; my $path = $scfg->{path}; my $server = $scfg->{server}; my $share = $scfg->{share}; if (!cifs_is_mounted($server, $share, $path, $cache->{mountdata})) { mkpath $path if !(defined($scfg->{mkdir}) && !$scfg->{mkdir}); die "unable to activate storage '$storeid' - " . "directory '$path' does not exist\n" if ! -d $path; cifs_mount($server, $share, $path, $storeid, $scfg->{smbversion}, $scfg->{username}, $scfg->{domain}); } $class->SUPER::activate_storage($storeid, $scfg, $cache); } sub deactivate_storage { my ($class, $storeid, $scfg, $cache) = @_; $cache->{mountdata} = PVE::ProcFSTools::parse_proc_mounts() if !$cache->{mountdata}; my $path = $scfg->{path}; my $server = $scfg->{server}; my $share = $scfg->{share}; if (cifs_is_mounted($server, $share, $path, $cache->{mountdata})) { my $cmd = ['/bin/umount', $path]; run_command($cmd, errmsg => 'umount error'); } } sub check_connection { my ($class, $storeid, $scfg) = @_; my $servicename = '//'.$scfg->{server}.'/'.$scfg->{share}; my $cmd = ['/usr/bin/smbclient', $servicename, '-d', '0']; if (defined($scfg->{smbversion}) && $scfg->{smbversion} ne 'default') { # max-protocol version, so basically only relevant for smb2 vs smb3 push @$cmd, '-m', "smb" . int($scfg->{smbversion}); } if (my $cred_file = get_cred_file($storeid)) { push @$cmd, '-U', $scfg->{username}, '-A', $cred_file; push @$cmd, '-W', $scfg->{domain} if defined($scfg->{domain}); } else { push @$cmd, '-U', 'Guest','-N'; } push @$cmd, '-c', 'echo 1 0'; my $out_str; my $out = sub { $out_str .= shift }; eval { run_command($cmd, timeout => 10, outfunc => $out, errfunc => sub {}) }; if (my $err = $@) { die "$out_str\n" if defined($out_str) && ($out_str =~ m/NT_STATUS_(ACCESS_DENIED|LOGON_FAILURE)/); return 0; } return 1; } # FIXME remove on the next APIAGE reset. # Deprecated, use get_volume_attribute instead. sub get_volume_notes { my $class = shift; PVE::Storage::DirPlugin::get_volume_notes($class, @_); } # FIXME remove on the next APIAGE reset. # Deprecated, use update_volume_attribute instead. sub update_volume_notes { my $class = shift; PVE::Storage::DirPlugin::update_volume_notes($class, @_); } sub get_volume_attribute { return PVE::Storage::DirPlugin::get_volume_attribute(@_); } sub update_volume_attribute { return PVE::Storage::DirPlugin::update_volume_attribute(@_); } 1;