public override async Task <ControllerUnpublishVolumeResponse> ControllerUnpublishVolume(ControllerUnpublishVolumeRequest request, ServerCallContext context) { var volumeName = request.VolumeId; //tmp fix to remove bad pvc name if (volumeName.StartsWith("C:\\")) { volumeName = HypervUtils.GetFileNameWithoutExtension(volumeName); } if (!Guid.TryParse(request.NodeId, out var vmId)) { throw new RpcException(new Status(StatusCode.InvalidArgument, "node id invalid")); } _logger.LogInformation("unpublish volume {VolumeName} from node {VMId}", volumeName, vmId); var foundVolume = await _service.GetVolumesAsync(new HypervVolumeFilter { Name = volumeName }) .FirstOrDefaultAsync(context.CancellationToken); if (foundVolume is null) { throw new RpcException(new Status(StatusCode.NotFound, "volume not found")); } var vm = await _service.GetVirtualMachinesAsync(new HypervVirtualMachineFilter { Id = vmId }) .FirstOrDefaultAsync(context.CancellationToken); if (vm is null) { throw new RpcException(new Status(StatusCode.NotFound, "node not found")); } //todo maybe vm is deleted, spec: SHOULD return OK await _service.DetachVolumeAsync(new HypervDetachVolumeRequest { VMId = vm.Id, VolumePath = foundVolume.Path, Host = vm.Host }, context.CancellationToken); var rsp = new ControllerUnpublishVolumeResponse { }; return(rsp); }
public async Task read_kvp_entries(string filePath) { var dic = new Dictionary <string, string>(); await foreach (var entry in HypervUtils.ReadKvpPoolAsync(filePath)) { dic[entry.Name] = entry.Value; } Assert.Contains("VirtualMachineId", (IDictionary <string, string>)dic); Assert.Contains("VirtualMachineName", (IDictionary <string, string>)dic); Assert.Contains("PhysicalHostNameFullyQualified", (IDictionary <string, string>)dic); }
public override async Task <NodeGetInfoResponse> NodeGetInfo(NodeGetInfoRequest request, ServerCallContext context) { //KVP daemon for KVP transfers to and from the host //centos: sudo yum install -y hyperv-daemons //read hostname from kvp values //strings /var/lib/hyperv/.kvp_pool_3|sed -n '/PhysicalHostNameFullyQualified/ {n;p}' //KVP byte array of fix sized fields key:512,value:2048 /0 filled //The byte array contains a UTF-8 encoded string, //which is padded out to the max size with null characters. //However, null string termination is not guaranteed (see kvp_send_key). //https://stackoverflow.com/questions/17530460/code-sample-for-reading-values-from-hyper-v-kvp-component-on-linux-aka-kvp-data var vmId = string.Empty; await foreach (var entry in HypervUtils.ReadKvpPoolAsync().WithCancellation(context.CancellationToken)) { switch (entry.Name) { case "VirtualMachineId": vmId = entry.Value; break; //case "VirtualMachineName": //case "PhysicalHostNameFullyQualified": default: break; } } if (string.IsNullOrEmpty(vmId)) { throw new RpcException(new Status(StatusCode.InvalidArgument, string.Empty), "hyperv kvp could not be read"); } var rsp = new NodeGetInfoResponse { NodeId = vmId, //todo MaxVolumesPerNode = 4*64 -1 //todo query by lsscsi //maybe AccessibleTopology from FailoverCluster query }; return(rsp); }
public override async Task <DeleteVolumeResponse> DeleteVolume(DeleteVolumeRequest request, ServerCallContext context) { var volumeName = request.VolumeId; //tmp fix to remove bad pvc name if (volumeName.StartsWith("C:\\")) { volumeName = HypervUtils.GetFileNameWithoutExtension(volumeName); } _logger.LogInformation("delete volume {VolumeName}", volumeName); var foundVolumes = await _service.GetVolumesAsync(new HypervVolumeFilter { Name = volumeName }) .ToListAsync(context.CancellationToken); if (foundVolumes.Count > 1) { throw new RpcException(new Status(StatusCode.AlreadyExists, "volume id ambiguous")); } if (foundVolumes.Count == 1) { var volume = await _service.GetVolumeAsync(foundVolumes[0].Path, context.CancellationToken); if (volume.Attached) { throw new RpcException(new Status(StatusCode.FailedPrecondition, "volume attached")); } //todo snapshot/parent check await _service.DeleteVolumeAsync(new HypervDeleteVolumeRequest { Id = volume.Id, Path = volume.Path }, context.CancellationToken); } return(new DeleteVolumeResponse()); }
public async Task GetDeviceByVhdIdAsync(string hostName, Guid vhdId) { var power = Fixture.GetPower(hostName); Command cmd; var commands = new List <Command>(2); var diskFilter = HypervUtils.GetDiskFilter(vhdId); cmd = new Command("Get-ChildItem"); cmd.Parameters.Add("Path", diskFilter); commands.Add(cmd); cmd = new Command("Select-Object"); cmd.Parameters.Add("Property", new [] { "Directory", "Target" }); commands.Add(cmd); //hack until Join-Path in pipe dynamic obj = await power.InvokeAsync(commands).ThrowOnError() .FirstOrDefaultAsync(); var blockDeviceName = string.Empty; if (obj is not null) { commands.Clear(); cmd = new Command("Join-Path"); cmd.Parameters.Add("Path", obj.Directory); cmd.Parameters.Add("ChildPath", obj.Target); cmd.Parameters.Add("Resolve"); commands.Add(cmd); blockDeviceName = await power.InvokeAsync(commands).ThrowOnError() .Select(n => (string)n.BaseObject) .FirstOrDefaultAsync(); } Assert.StartsWith("/dev/", blockDeviceName); }
public async Task MountDeviceAsync(HypervNodeMountRequest request, CancellationToken cancellationToken = default) { using var scope = _logger.BeginScope("mounting {VolumeName}", request.Name); Command cmd; var commands = new List <Command>(4); var blockDeviceName = string.Empty; if (request.VhdId != Guid.Empty) { var diskFilter = HypervUtils.GetDiskFilter(request.VhdId); cmd = new Command("Get-ChildItem"); cmd.Parameters.Add("Path", diskFilter); commands.Add(cmd); cmd = new Command("Select-Object"); cmd.Parameters.Add("Property", new[] { "Directory", "Target" }); commands.Add(cmd); //hack until Join-Path in pipe dynamic obj = await _power.InvokeAsync(commands).ThrowOnError() .FirstOrDefaultAsync(); if (obj is not null) { commands.Clear(); cmd = new Command("Join-Path"); cmd.Parameters.Add("Path", obj.Directory); cmd.Parameters.Add("ChildPath", obj.Target); cmd.Parameters.Add("Resolve"); commands.Add(cmd); blockDeviceName = await _power.InvokeAsync(commands).ThrowOnError() .Select(n => (string)n.BaseObject) .FirstOrDefaultAsync(); } } if (string.IsNullOrEmpty(blockDeviceName)) { //over controller number commands.Clear(); cmd = new Command("lsblk -S -o 'HCTL,NAME' -nr", true); commands.Add(cmd); var hctl = $"{request.ControllerNumber}:0:0:{request.ControllerLocation}"; var deviceInfo = await _power.InvokeAsync(commands).ThrowOnError() .Select((dynamic n) => (string[])n.Split(" ", 2, StringSplitOptions.RemoveEmptyEntries)) .Where(n => n.Length == 2 && n[0] == hctl) .Select(n => new { hctl = n[0], name = n[1] }) .FirstOrDefaultAsync(cancellationToken); if (deviceInfo is null) { throw new Exception($"device[{hctl}] not found"); } blockDeviceName = $"/dev/{deviceInfo.name}"; } if (string.IsNullOrEmpty(blockDeviceName)) { throw new Exception($"block device not found"); } //todo check name of blockDeviceName _logger.LogDebug("BlockDevice '{BlockDeviceName}' found", blockDeviceName); if (request.Raw) { throw new NotImplementedException("raw block device not implemented"); } commands.Clear(); var deviceName = $"{blockDeviceName}1"; var deviceLabel = string.Empty; var deviceUUID = string.Empty; var deviceFSType = string.Empty; cmd = new Command($"blkid -o export -c /dev/null {deviceName}", true); commands.Add(cmd); //BLKID_EXIT_NOTFOUND=2 /* * DEVNAME=/dev/sdb1 * LABEL=pvc-5a344250-ca2 * UUID=978388ae-6a2e-479a-8658-48ea495adda3 * TYPE=ext4 * PARTLABEL=primary * PARTUUID=90580121-4dd5-485a-9d07-e20b73cba4bf */ await foreach (var line in _power.InvokeAsync(commands).ThrowOnError() .Select(n => n.BaseObject).OfType <string>() .TakeWhile(n => !string.IsNullOrEmpty(n)) .WithCancellation(cancellationToken)) { var name = line; var value = string.Empty; int i = line.IndexOf('='); if (i > -1) { name = line.Substring(0, i); value = line.Substring(i + 1); } switch (name) { case "DEVNAME" when deviceName != value: throw new Exception("invalid device info"); case "LABEL": deviceLabel = value; break; case "UUID": deviceUUID = value; break; case "TYPE": deviceFSType = value; break; } } //_logger.LogDebug("{@Device}", deviceLabel); //invalid device mount protection if (request.ValidateLabel && !string.IsNullOrEmpty(deviceLabel) && !request.Name.StartsWith(deviceLabel)) { throw new Exception("device label ambiguous"); } //todo select multiple partitions var fsType = !string.IsNullOrEmpty(request.FSType) ? request.FSType : "ext4"; var setPermissions = false; if (string.IsNullOrEmpty(deviceUUID)) { _logger.LogInformation("creating partition on {BlockDeviceName} with {FSType}", blockDeviceName, deviceFSType); commands.Clear(); //parted /dev/sdb --script mklabel gpt //parted /dev/sdb --script mkpart primary ext4 0% 100% var script = $"parted --align=opt {blockDeviceName} --script mklabel gpt mkpart primary {fsType} 1MiB 100%"; cmd = new Command(script, true); commands.Add(cmd); var result = await _power.InvokeAsync(commands).ThrowOnError().ToListAsync(cancellationToken); } if (string.IsNullOrEmpty(deviceFSType)) { _logger.LogInformation("creating filesystem on {deviceName} with {FSType}", deviceName, deviceFSType); commands.Clear(); deviceLabel = request.Name ?? string.Empty; //labels are max 16-chars long (maybe for xfs max 12) if (deviceLabel.Length > 16) { deviceLabel = deviceLabel.Substring(0, 16); } //mkfs -t ext4 -G 4096 -L volume-test /dev/sdb1 //maybe add -G 4096 cmd = new Command($"& mkfs -t {fsType} -L \"{deviceLabel}\" {deviceName} 2>&1", true); commands.Add(cmd); //cmd = new Command("Out-String"); ////cmd.Parameters.Add("NoNewline"); //commands.Add(cmd); var results = await _power.InvokeAsync(commands) .ThrowOnError(error => !error.Exception.Message.StartsWith("mke2fs")) .ToListAsync(cancellationToken); deviceFSType = fsType; setPermissions = true; } if (deviceFSType != fsType) { throw new Exception("fsType ambiguous"); } _logger.LogDebug("try to find mount {DeviceName} to {TargetPath}", deviceName, request.TargetPath); commands.Clear(); cmd = new Command($"findmnt -fnr {deviceName} {request.TargetPath}", true); commands.Add(cmd); ///tmp/testdrive /dev/sdb1 ext4 rw,noatime,seclabel,discard var mountpoint = await _power.InvokeAsync(commands).ThrowOnError() .Select(n => n.BaseObject).OfType <string>() .FirstOrDefaultAsync(cancellationToken); if (string.IsNullOrEmpty(mountpoint)) { _logger.LogDebug("create {TargetPath}", request.TargetPath); commands.Clear(); cmd = new Command("New-Item"); cmd.Parameters.Add("ItemType", "directory"); cmd.Parameters.Add("Path", request.TargetPath); cmd.Parameters.Add("ErrorAction", "SilentlyContinue"); commands.Add(cmd); _ = await _power.InvokeAsync(commands).ThrowOnError() .FirstOrDefaultAsync(cancellationToken); _logger.LogInformation("mounting {DeviceName} to {TargetPath}", deviceName, request.TargetPath); commands.Clear(); var options = new HashSet <string> { "discard", "noatime" }; foreach (var opt in (request.Options ?? Array.Empty <string>())) { options.Add(opt); } if (request.Readonly) { options.Add("ro"); } //mount -o "discard,noatime,ro" /dev/sdb1 /drivetest cmd = new Command($"mkdir -p {request.TargetPath} && mount -o \"{string.Join(",", options)}\" {deviceName} {request.TargetPath}", true); commands.Add(cmd); //Labels are normaly only 16-chars long //Warning: label too long; will be truncated to 'pvc-5a344250-ca2' _ = await _power.InvokeAsync(commands).ThrowOnError() .ToListAsync(cancellationToken); mountpoint = request.TargetPath; } if (!setPermissions) { _logger.LogDebug("empty check of {Mountpoint}", mountpoint); commands.Clear(); cmd = new Command("Get-ChildItem"); cmd.Parameters.Add("Path", request.TargetPath); commands.Add(cmd); try { var hasFiles = await _power.InvokeAsync(commands).ThrowOnError() .AnyAsync((dynamic n) => n.Name switch { "lost+found" => false, _ => true }, cancellationToken); setPermissions = !hasFiles; }
// This method gets called by the runtime. Use this method to add services to the container. // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { services.AddOptions <HypervCsiDriverOptions>() .Bind(Configuration.GetSection("Driver")) .PostConfigure(opt => { switch (opt.Type) { case HypervCsiDriverType.Controller: //load hyperv host from kvp if (string.IsNullOrEmpty(opt.HostName)) { var(_, value) = HypervUtils.ReadKvpPoolAsync() .FirstOrDefaultAsync(n => n.Name == "PhysicalHostNameFullyQualified") .Result; if (!string.IsNullOrEmpty(value)) { opt.HostName = value; } if (string.IsNullOrEmpty(opt.UserName)) { opt.UserName = "******"; //aka windows root } } break; } }) .Validate(opt => { switch (opt.Type) { case HypervCsiDriverType.Controller: if (string.IsNullOrEmpty(opt.HostName)) { return(false); } return(true); case HypervCsiDriverType.Node: return(true); default: return(false); } }); var driverType = Configuration.GetValue <HypervCsiDriverType>("Driver:Type"); switch (driverType) { case HypervCsiDriverType.Controller: services.AddSingleton <IHypervVolumeService, HypervVolumeService>(); break; case HypervCsiDriverType.Node: services.AddSingleton <IHypervNodeService, LinuxNodeService>(); break; } services.AddGrpc(); }
public async Task create_disk_filter(Guid vhdId, string diskFilter) { var r = HypervUtils.GetDiskFilter(vhdId); Assert.Equal(diskFilter, r); }
public async Task can_parse_storage_name_from_win_file_name(string filePath, string storageName) { var r = HypervUtils.GetStorageNameFromPath(filePath); Assert.Equal(storageName, r); }
public async Task can_parse_win_file_name(string filePath, string fileName) { var r = HypervUtils.GetFileNameWithoutExtension(filePath); Assert.Equal(fileName, r); }