Hyper-V: Ottenere un thumbnail di una VM in VB.NET

Hyper-V può essere gestito tramite WMI in quanto espone un completo modello ad oggetti tramite cui è possibile ricavare informazioni ed eseguire operazioni.

Tra le varie operazioni vi è anche la possibilità di ottenere il thumbnail di una macchina virtuale in esecuzione, funzionalità sfruttata dalla console di gestione di Hyper-V (virtmgmt.msc).

image 

Questa possibilità oltre che essere sfruttata per ottenere un thumbnail può essere utilizzata anche per ottenere degli screen shoot approccio che può tornare utile nel caso nella VM venga eseguito un’applicativo di puro monitoraggio.

Si pensi ad esempio ad applicativi per il controllo di macchine industriali in questi contesti spesso torna utile separare la rete “industriale” da quella aziendale, ma spesso occorre poter monitorare dei processi da computer che appartengono alla rete aziendale. Infatti in questo modo la VM apparterà alla rete “industriale” e comunicherà con i PLC di gestione dell’impianto industriale, il computer apparterà alla rete aziendale a cui apparterà anche l’Hyper-V server che fornirà lo screenshot della VM  comunicando tramite il VMBus in modo sicuro senza necessità di mettere in comunicazione le due reti.

image

Un altra applicazione può essere quella di applicazione sempre di solo monitoraggio la cui esecuzione è dispendiosa in termini di risorse e/o traffico di rete per cui non prevista la possibilità di essere fruita via HTTP (scenario comune in applicazioni legacy). In questo caso infatti l’applicazione verrà fatta risiedere in una VM con risorse sufficienti in termini di processore, RAM e traffico di rete, i client invece richiederanno screenshot al server Hyper-V.

imagePer ottenere un thumbnail occorre utilizzare il metodo GetVirtualSystemThumbnailImage della classe Msvm_VirtualSystemManagementService. Nella documentazione MSDN relativa al metodo GetVirtualSystemThumbnailImage viene anche riportato un esempio in C# e uno in VBScript.

Il metodo consente di ricavare lo screenshot sottoforma di un’array di byte e il formato dell’immagine è RGB con 16 bit per pixel (5 bit per il rosso, 6 bit per il verde e 5 per il blu) ovvero il formato rappresentato dal valore Format16bppRgb565 dell’enumerativo PixelFormat.

A tal proposito il codice proposto per la conversione dell’immagine in file nell’esempio non risulta corretto:

static void SaveImageData(byte[] imageData)
        {
            FileStream fs = new FileStream(@”c:\test.bin”, FileMode.Create, FileAccess.Write);
            fs.Write(imageData, 0, imageData.Length);
            fs.Flush();
            fs.Close();
        }

Occorre prima eseguire una conversione di formato come mostrato nell’esempio di  Adam Semel nel post Getting a thumbnail from Hyper-V or SCVMM in C#:

static void SaveImageData(byte[] imageData, string vmName)
  {
    path += @”\Thumbnails\”;
    Bitmap VMThumbnail = new Bitmap(x, y, PixelFormat.Format16bppRgb565);
    Rectangle rectangle = new Rectangle(0, 0, x, y);
    BitmapData VMThumbnailBiltmapData = VMThumbnail.LockBits(rectangle, ImageLockMode.WriteOnly, PixelFormat.Format16bppRgb565);
    System.Runtime.InteropServices.Marshal.Copy(imageData, 0, VMThumbnailBiltmapData.Scan0, x * y * 2);
    VMThumbnail.UnlockBits(VMThumbnailBiltmapData);
    VMThumbnail.Save(path + vmName + “.jpg”);
  }

Si noti anche che un’immagine di 1024×768 richiederà un’array di 1.572.864 byte ovvero di 1,5 MB quindi occorrerà trovare il giusto compromesso tra dimensione dell’immagine e tempo di refresh per evitare di saturare la connessione di rete.

Questo ovviamente spiega anche la best practies che indica di non lasciare la console di gestione di Hyper-V (virtmgmt.msc) attiva se non utilizzata. Infatti durante il suo funzionamento la console esegue il refresh del thubnail delle VM attive richiedendo quindi un’immagine di 80×60 pari ad una array di 9600 byte quindi se vi sono 4 VM attive con un tempo di refresh di 0.5 sec vengono traferiti circa 4,4 MB al minuto solo per gestire i thumbnail.

Un esempio in PowerShell per ricavare il thumbnail è disponibile nel seguente post Hyper-V WMI: Creating a Thumbnail Image di Taylor Brown.

Mentre al fondo del seguente articolo è disponibile del codice in VB.NET per realizzare una classe VirtualMachine (che potrete espandere a piacimento) e che consente di ottenere nella proprietà BitMap Thubnail lo screenshot di una VM. La classe richiede la versione 2.0 del framework .NET e l’aggiunta di una reference all’assembly System.Management.

Di seguito un esempio di utilizzo:

Public Class Form1
    Private vm As New VirtualMachine(“hvHostname”, “vmName”, “user”, “pass”)

    Private Sub btnRefresh_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnRefresh.Click
        Try
            vm.ThumbnailWidth = Me.picScreenshoot.Width
               vm.ThumbnailHeight = Me.picScreenshoot.Height
               vm.GetInfo()
            Me.picScreenshoot.Image = Me.vm.Thumbnail
            Me.lblStatus.Text = “Refresh successfully at ” & _
                Now.ToLongTimeString()
        Catch ex As Exception
            Me.lblStatus.Text = “Refresh failed! ” & ex.Message
        End Try
    End Sub
End Class

image

Public Class VirtualMachine
    Public Sub New(ByVal hvServerHostname As String, ByVal name As String)
        Me.New(hvServerHostname, name, String.Empty, String.Empty)
    End Sub

    Public Sub New(ByVal hvServerHostname As String, ByVal name As String, ByVal username As String, ByVal password As String)
        Me.hvServerHostnameValue = hvServerHostname
        Me.nameValue = name
        Me.usernameValue = username
        Me.passwordValue = password
    End Sub

    Private hvServerHostnameValue As String = String.Empty

    Public ReadOnly Property HVServerHostname() As String
        Get
            Return Me.hvServerHostnameValue
        End Get
    End Property

    Private nameValue As String = String.Empty

    Public ReadOnly Property Name() As String
        Get
            Return Me.nameValue
        End Get
    End Property

    Private usernameValue As String = String.Empty

    Public ReadOnly Property Username() As String
        Get
            Return Me.usernameValue
        End Get
    End Property

    Private passwordValue As String = String.Empty

    Public ReadOnly Property Password() As String
        Get
            Return Me.passwordValue
        End Get
    End Property

    Private thumbnailValue As System.Drawing.Bitmap = Nothing

    Public ReadOnly Property Thumbnail() As System.Drawing.Bitmap
        Get
            Return Me.thumbnailValue
        End Get
    End Property

    Private thumbnailWidthValue As Integer = 320

    Public Property ThumbnailWidth() As Integer
        Get
            Return Me.thumbnailWidthValue
        End Get
        Set(ByVal value As Integer)
            Me.thumbnailWidthValue = value
        End Set
    End Property

    Private thumbnailHeightValue As Integer = 240

    Public Property ThumbnailHeight() As Integer
        Get
            Return Me.thumbnailHeightValue
        End Get
        Set(ByVal value As Integer)
            Me.thumbnailHeightValue = value
        End Set
    End Property

    Private Enum ReturnCodes As UInt32
        Completed = 0
        Started = 4096
        Failed = 32768
        AccessDenied = 32769
        NotSupported = 32770
        Unknown = 32771
        Timeout = 32772
        InvalidParameter = 32773
        SystemInUse = 32774
        InvalidState = 32775
        IncorrectDataType = 32776
        SystemNotAvailable = 32777
        OutofMemory = 32778
    End Enum

    Private Enum JobState As UInt16
        [New] = 2
        Starting = 3
        Running = 4
        Suspended = 5
        ShuttingDown = 6
        Completed = 7
        Terminated = 8
        Killed = 9
        Exception = 10
        Service = 11
    End Enum

    Public Sub GetInfo()
        ‘Impostazione ConnectionOptions per autenticazione
        Dim co As System.Management.ConnectionOptions = Nothing

        If Not System.String.IsNullOrEmpty(Me.usernameValue) Then
            co = New System.Management.ConnectionOptions()
            co.Username = Me.usernameValue
            co.Password = Me.passwordValue
        End If

        ‘Impostazione Scope
        Dim scope = New System.Management.ManagementScope( _
            “\\” & Me.hvServerHostnameValue & “\root\virtualization”, co)
        scope.Connect()

        ‘Ricerca VM
        Dim vm As System.Management.ManagementObject = Nothing

        Dim queryVM As New System.Management.ObjectQuery( _
            String.Format(“SELECT * FROM Msvm_ComputerSystem WHERE ElementName = ‘{0}'”, _
                          Me.nameValue))
        Using searcherVM As New System.Management.ManagementObjectSearcher(scope, queryVM)
            Using vmCollection = searcherVM.Get()
                Using vmEnumerator = vmCollection.GetEnumerator()
                    vmEnumerator.MoveNext()
                    vm = DirectCast(vmEnumerator.Current, System.Management.ManagementObject)
                End Using
            End Using
        End Using

        ‘Impostazione TargetSystem
        Dim target As System.Management.ManagementObject = Nothing

        Using vm
            Using targetCollection = vm.GetRelated( _
                “Msvm_VirtualSystemSettingData”, _
                “Msvm_SettingsDefineState”, _
                Nothing, _
                Nothing, _
                “SettingData”, _
                “ManagedElement”, _
                False, _
                Nothing)
                Using targetEnumerator = targetCollection.GetEnumerator()
                    targetEnumerator.MoveNext()
                    target = DirectCast(targetEnumerator.Current, System.Management.ManagementObject)
                End Using
            End Using
        End Using

        ‘Impostazione VirtualSystemManagementService
        Dim vmsService As System.Management.ManagementObject = Nothing

        Dim queryVMSService As New System.Management.ObjectQuery( _
            “SELECT * FROM Msvm_VirtualSystemManagementService”)

        Using searcherVMSService As New System.Management.ManagementObjectSearcher(scope, queryVMSService)
            Using vmsServiceCollection = searcherVMSService.Get()
                Using vmsServiceEnumerator = vmsServiceCollection.GetEnumerator()
                    vmsServiceEnumerator.MoveNext()
                    vmsService = DirectCast(vmsServiceEnumerator.Current, System.Management.ManagementObject)
                End Using
            End Using
        End Using

        ‘Richiesta dati Thumbnail
        Dim imageData As Byte() = Nothing
        Using vmsService
            Using target
                Using inParams = vmsService.GetMethodParameters(“GetVirtualSystemThumbnailImage”)
                    inParams(“WidthPixels”) = Me.thumbnailWidthValue
                    inParams(“HeightPixels”) = Me.thumbnailHeightValue
                    inParams(“TargetSystem”) = target.Path.Path

                    Using outParams = vmsService.InvokeMethod(“GetVirtualSystemThumbnailImage”, inParams, Nothing)
                        If System.Convert.ToUInt32(outParams(“ReturnValue”)) = ReturnCodes.Started Then
                            Me.WaitJobCompleted(outParams, scope)
                        ElseIf System.Convert.ToUInt32(outParams(“ReturnValue”)) <> ReturnCodes.Completed Then
                            Throw New System.Exception(String.Format(“Failed to retrieve VM thumbnail [{0}].”, DirectCast(outParams(“ReturnValue”), ReturnCodes)))
                        End If

                        imageData = DirectCast(outParams(“ImageData”), Byte())
                    End Using
                End Using
            End Using
        End Using

        ‘Conversione dati Thumbnail
        If imageData IsNot Nothing Then
            Me.thumbnailValue = New System.Drawing.Bitmap( _
                Me.thumbnailWidthValue, Me.thumbnailHeightValue, _
                System.Drawing.Imaging.PixelFormat.Format16bppRgb565)

            Dim rect As New System.Drawing.Rectangle( _
                0, 0, Me.thumbnailValue.Width, Me.thumbnailValue.Height)

            Dim bitmapData = Me.thumbnailValue.LockBits(rect, System.Drawing.Imaging.ImageLockMode.WriteOnly, System.Drawing.Imaging.PixelFormat.Format16bppRgb565)

            System.Runtime.InteropServices.Marshal.Copy( _
                imageData, 0, bitmapData.Scan0, _
                Me.thumbnailValue.Width * Me.thumbnailValue.Height * 2)

            Me.thumbnailValue.UnlockBits(bitmapData)

            bitmapData = Nothing
            imageData = Nothing
        End If

        ‘Rilascio risorse
        queryVMSService = Nothing
        queryVM = Nothing
        scope = Nothing
        If co IsNot Nothing Then co = Nothing
    End Sub

    Private Sub WaitJobCompleted(ByVal outParams As System.Management.ManagementBaseObject, ByVal scope As System.Management.ManagementScope)
        ‘Ricerca msvc_StorageJob path (full wmi path)
        Dim jobPath As String = DirectCast(outParams(“Job”), String)
        Using job As New System.Management.ManagementObject(scope, New System.Management.ManagementPath(jobPath), Nothing)
            ‘Ricerca job information
            job.[Get]()
            While DirectCast(job(“JobState”), UInt16) = JobState.Starting OrElse _
                DirectCast(job(“JobState”), UInt16) = JobState.Running
                ‘Console.WriteLine(“In progress… {0}% completed.”, job(“PercentComplete”))
                System.Threading.Thread.Sleep(500)
                job.[Get]()
            End While

            If DirectCast(job(“JobState”), UInt16) <> JobState.Completed Then
                Throw New System.Exception( _
                    String.Format(“{0} (Error code = {1).”, _
                        DirectCast(job(“ErrorDescription”), String), _
                        DirectCast(job(“ErrorCode”), UInt16)))
            End If
        End Using
    End Sub

End Class