lib/public/lab.ps1

<#
.SYNOPSIS
    Loads a Lab Builder Configuration file and returns a Lab object
.DESCRIPTION
    Takes the path to a valid LabBuilder Configiration XML file and loads it.
 
    It will perform simple validation on the XML file and throw an exception
    if any of the validation tests fail.
 
    At load time it will also add temporary configuration attributes to the in
    memory configuration that are used by other LabBuilder functions. So loading
    XML Configurartion without using this function is not advised.
.PARAMETER ConfigPath
    This is the path to the Lab Builder configuration file to load.
.PARAMETER LabPath
    This is an optional path that is used to Override the LabPath in the config
    file passed.
.EXAMPLE
    $MyLab = Get-Lab -ConfigPath c:\MyLab\LabConfig1.xml
    Loads the LabConfig1.xml configuration and returns Lab object.
.OUTPUTS
    The Lab object representing the Lab Configuration that was loaded.
#>

function Get-Lab {
    [CmdLetBinding()]
    [OutputType([XML])]
    param
    (
        [Parameter(
            Position=1,
            Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [String] $ConfigPath,

        [Parameter(
            Position=2)]
        [ValidateNotNullOrEmpty()]
        [String] $LabPath,

        [Parameter(
            Position=3)]
        [Switch] $SkipXMLValidation
    ) # Param

    # If a relative path to the config has been specified
    # then convert it to absolute path
    if (-not [System.IO.Path]::IsPathRooted($ConfigPath))
    {
        $ConfigPath = Join-Path `
            -Path (Get-Location).Path `
            -ChildPath $ConfigPath
    } # if

    if (-not (Test-Path -Path $ConfigPath))
    {
        $ExceptionParameters = @{
            errorId = 'ConfigurationFileNotFoundError'
            errorCategory = 'InvalidArgument'
            errorMessage = $($LocalizedData.ConfigurationFileNotFoundError `
                -f $ConfigPath)
        }
        ThrowException @ExceptionParameters
    } # if

    $Content = Get-Content -Path $ConfigPath -Raw
    if (-not $Content)
    {
        $ExceptionParameters = @{
            errorId = 'ConfigurationFileEmptyError'
            errorCategory = 'InvalidArgument'
            errorMessage = $($LocalizedData.ConfigurationFileEmptyError `
                -f $ConfigPath)
        }
        ThrowException @ExceptionParameters
    } # if

    if (-not $SkipXMLValidation)
    {
        # Validate the XML
        ValidateConfigurationXMLSchema `
            -ConfigPath $ConfigPath `
            -ErrorAction Stop
    }

    # The XML passes the Schema check so load it.
    [XML] $Lab = New-Object System.Xml.XmlDocument
    $Lab.PreserveWhitespace = $true
    $Lab.LoadXML($Content)

    # Check the Required Windows Build
    $RequiredWindowsBuild = $Lab.labbuilderconfig.settings.requiredwindowsbuild
    if ($RequiredWindowsBuild -and `
        ($Script:CurrentBuild -lt $RequiredWindowsBuild))
    {
        $ExceptionParameters = @{
            errorId = 'RequiredBuildNotMetError'
            errorCategory = 'InvalidArgument'
            errorMessage = $($LocalizedData.RequiredBuildNotMetError `
                -f $Script:CurrentBuild,$RequiredWindowsBuild)
        }
        ThrowException @ExceptionParameters
    } # if

    # Figure out the Config path and load it into the XML object (if we can)
    # This path is used to find any additional configuration files that might
    # be provided with config
    [String] $ConfigPath = [System.IO.Path]::GetDirectoryName($ConfigPath)
    [String] $XMLConfigPath = $Lab.labbuilderconfig.settings.configpath
    if ($XMLConfigPath) {
        if (-not [System.IO.Path]::IsPathRooted($XMLConfigurationPath))
        {
            # A relative path was provided in the config path so add the actual path of the
            # XML to it
            [String] $FullConfigPath = Join-Path `
                -Path $ConfigPath `
                -ChildPath $XMLConfigPath
        } # if
    }
    else
    {
        [String] $FullConfigPath = $ConfigPath
    }
    $Lab.labbuilderconfig.settings.setattribute('fullconfigpath',$FullConfigPath)

    # if the LabPath was passed as a parameter, set it in the config
    if ($LabPath)
    {
        $Lab.labbuilderconfig.settings.SetAttribute('labpath',$LabPath)
    }
    else
    {
        [String] $LabPath = $Lab.labbuilderconfig.settings.labpath
    }

    # Get the VHDParentPathFull - if it isn't supplied default
    [String] $VHDParentPath = $Lab.labbuilderconfig.settings.vhdparentpath
    if (-not $VHDParentPath)
    {
        $VHDParentPath = 'Virtual Hard Disk Templates'
    }
    # if the resulting parent path is not rooted make the root the Lab Path
    if (-not ([System.IO.Path]::IsPathRooted($VHDParentPath)))
    {
        $VHDParentPath = Join-Path `
            -Path $LabPath `
            -ChildPath $VHDParentPath
    } # if
    $Lab.labbuilderconfig.settings.setattribute('vhdparentpathfull',$VHDParentPath)

    # Get the DSCLibraryPathFull - if it isn't supplied default
    [String] $DSCLibraryPath = $Lab.labbuilderconfig.settings.dsclibrarypath
    if (-not $DSCLibraryPath)
    {
        $DSCLibraryPath = 'DSCLibrary'
    } # if
    # if the resulting parent path is not rooted make the root the Full config path
    if (-not [System.IO.Path]::IsPathRooted($DSCLibraryPath))
    {
        $DSCLibraryPath = Join-Path `
            -Path $Lab.labbuilderconfig.settings.fullconfigpath `
            -ChildPath $DSCLibraryPath
    } # if
    $Lab.labbuilderconfig.settings.setattribute('dsclibrarypathfull',$DSCLibraryPath)

    # Get the ResourcePathFull - if it isn't supplied default
    [String] $ResourcePath = $Lab.labbuilderconfig.settings.resourcepath
    if (-not $ResourcePath)
    {
        $ResourcePath = 'Resource'
    } # if
    # if the resulting Resource path is not rooted make the root the Lab Path
    if (-not [System.IO.Path]::IsPathRooted($ResourcePath))
    {
        $ResourcePath = Join-Path `
            -Path $LabPath `
            -ChildPath $ResourcePath
    } # if
    $Lab.labbuilderconfig.settings.setattribute('resourcepathfull',$ResourcePath)

    # Determine the ModulePath where alternate Lab PowerShell Modules can be found.
    # If a path is specified but it is relative, make it relative to the lab path.
    # Otherwise use it as is.
    [String] $ModulePath = $Lab.labbuilderconfig.settings.modulepath
    if ($ModulePath)
    {
        if (-not [System.IO.Path]::IsPathRooted($ModulePath))
        {
            $ModulePath = Join-Path `
                -Path $LabPath `
                -ChildPath $ModulePath
        } # if
        # If the path is not included in the PSModulePath add it
        if (-not $env:PSModulePath.ToLower().Contains($ModulePath.ToLower() + ';'))
        {
            $env:PSModulePath = "$ModulePath;" + $env:PSModulePath
        } # if
    } # if

    Return $Lab
} # Get-Lab


<#
.SYNOPSIS
    Creates a new Lab Builder Configuration file and Lab folder.
.DESCRIPTION
    This function will take a path to a new Lab folder and a path or filename
    for a new Lab Configuration file and creates them using the standard XML
    template.
 
    It will also copy the DSCLibrary folder as well as the create an empty
    ISOFiles and VHDFiles folder in the Lab folder.
 
    After running this function the VMs, VMTemplates, Switches and VMTemplateVHDs
    in the new Lab Configuration file would normally be customized to for the new
    Lab.
.PARAMETER ConfigPath
    This is the path to the Lab Builder configuration file to create. If it is
    not rooted the configuration file is created in the LabPath folder.
.PARAMETER LabPath
    This is a required path of the new Lab to create.
.PARAMETER Name
    This is a required name of the Lab that gets added to the new Lab Configration
    file.
.PARAMETER Version
    This is a required version of the Lab that gets added to the new Lab Configration
    file.
.PARAMETER Id
    This is the optional Lab Id that gets set in the new Lab Configuration
    file.
.PARAMETER Description
    This is the optional Lab description that gets set in the new Lab Configuration
    file.
.PARAMETER DomainName
    This is the optional Lab domain name that gets set in the new Lab Configuration
    file.
.PARAMETER Email
    This is the optional Lab email address that gets set in the new Lab Configuration
    file.
.EXAMPLE
    $MyLab = New-Lab `
        -ConfigPath c:\MyLab\LabConfig1.xml `
        -LabPath c:\MyLab `
        -LabName 'MyLab' `
        -LabVersion '1.2'
    Creates a new Lab Configration file LabConfig1.xml and also a Lab folder
    c:\MyLab and populates it with default DSCLibrary file and supporting folders.
.OUTPUTS
    The Lab object representing the new Lab Configuration that was created.
#>

function New-Lab {
    [CmdLetBinding(
        SupportsShouldProcess = $true)]
    [OutputType([XML])]
    param
    (
        [Parameter(
            Position=1,
            Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [String] $ConfigPath,

        [Parameter(
            Position=2,
            Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [String] $LabPath,

        [Parameter(
            Position=3,
            Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [String] $Name,

        [Parameter(
            Position=4)]
        [ValidateNotNullOrEmpty()]
        [String] $Version = '1.0',

        [Parameter(
            Position=5)]
        [ValidateNotNullOrEmpty()]
        [String] $Id,

        [Parameter(
            Position=6)]
        [ValidateNotNullOrEmpty()]
        [String] $Description,

        [Parameter(
            Position=7)]
        [ValidateNotNullOrEmpty()]
        [String] $DomainName,

        [Parameter(
            Position=8)]
        [ValidateNotNullOrEmpty()]
        [String] $Email
    ) # Param

    # Determine the full Lab Path
    if (-not [System.IO.Path]::IsPathRooted($LabPath))
    {
        $LabPath = Join-Path `
            -Path Get-Location `
            -ChildPath $LabPath
    } # if

    # Does the Lab Path exist?
    if (Test-Path -Path $LabPath -Type Container)
    {
        # It does - exit if the user declines
        if (-not $PSCmdlet.ShouldProcess( 'LocalHost', `
            ($LocalizedData.ShouldOverwriteLab `
            -f $LabPath )))
        {
            return
        }
    }
    else
    {
        WriteMessage -Message $($LocalizedData.CreatingLabFolderMessage `
            -f 'LabPath',$LabPath)

        New-Item `
            -Path $LabPath `
            -Type Directory
    } # if

    # Determine the full Lab configuration Path
    if (-not [System.IO.Path]::IsPathRooted($ConfigPath))
    {
        $ConfigPath = Join-Path `
            -Path $LabPath `
            -ChildPath $ConfigPath
    } # if

    # Does the lab configuration path already exist?
    if (Test-Path -Path $ConfigPath)
    {
        # It does - exit if the user declines
        if (-not $PSCmdlet.ShouldProcess( 'LocalHost', `
            ($LocalizedData.ShouldOverwriteLabConfig `
            -f $ConfigPath )))
        {
            return
        }
    } # if

    # Get the Config Template into a variable
    $Content = Get-Content `
        -Path $Script:ConfigurationXMLTemplate

    # The XML passes the Schema check so load it.
    [XML] $Lab = New-Object System.Xml.XmlDocument
    $Lab.PreserveWhitespace = $true
    $Lab.LoadXML($Content)

    # Populate the Lab Entries
    $Lab.labbuilderconfig.name = $Name
    $Lab.labbuilderconfig.version = $Version
    $Lab.labbuilderconfig.settings.labpath = $LabPath
    if ($PSBoundParameters.ContainsKey('Id'))
    {
        $Lab.labbuilderconfig.settings.SetAttribute('Id',$Id)
    } # if
    if ($PSBoundParameters.ContainsKey('Description'))
    {
        $Lab.labbuilderconfig.description = $Description
    } # if
    if ($PSBoundParameters.ContainsKey('DomainName'))
    {
        $Lab.labbuilderconfig.settings.SetAttribute('DomainName',$DomainName)
    } # if
    if ($PSBoundParameters.ContainsKey('Email'))
    {
        $Lab.labbuilderconfig.settings.SetAttribute('Email',$Email)
    } # if

    # Save Configiration XML
    $Lab.Save($ConfigPath)

    # Create ISOFiles folder
    New-Item `
        -Path (Join-Path -Path $LabPath -ChildPath 'ISOFiles')`
        -Type Directory `
        -ErrorAction SilentlyContinue

    # Create VDFFiles folder
    New-Item `
        -Path (Join-Path -Path $LabPath -ChildPath 'VHDFiles')`
        -Type Directory `
        -ErrorAction SilentlyContinue

    # Copy the DSCLibrary
    Copy-Item `
        -Path $Script:DSCLibraryPath `
        -Destination $LabPath `
        -Recurse `
        -Force `
        -ErrorAction SilentlyContinue

    Return (Get-Lab `
        -ConfigPath $ConfigPath `
        -LabPath $LabPath)
} # New-Lab


<#
.SYNOPSIS
    Installs or Update a Lab.
.DESCRIPTION
    This cmdlet will install an entire Hyper-V lab environment defined by the
    LabBuilder configuration file provided.
 
    If components of the Lab already exist, they will be updated if they differ
    from the settings in the Configuration file.
 
    The Hyper-V component can also be optionally installed if it is not.
.PARAMETER ConfigPath
    The path to the LabBuilder configuration XML file.
.PARAMETER LabPath
    The optional path to install the Lab to - overrides the LabPath setting in the
    configuration file.
.PARAMETER Lab
    The Lab object returned by Get-Lab of the lab to install.
.PARAMETER CheckEnvironment
    Whether or not to check if Hyper-V is installed and install it if missing.
.PARAMETER Force
    This will force the Lab to be installed, automatically suppressing any confirmations.
.EXAMPLE
    Install-Lab -ConfigPath c:\mylab\config.xml
    Install the lab defined in the c:\mylab\config.xml LabBuilder configuration file.
.EXAMPLE
    Get-Lab -ConfigPath c:\mylab\config.xml | Install-Lab
    Install the lab defined in the c:\mylab\config.xml LabBuilder configuration file.
.OUTPUTS
    None
#>

Function Install-Lab {
    [CmdLetBinding(DefaultParameterSetName="Lab")]
    param
    (
        [parameter(
            Position=1,
            ParameterSetName="File",
            Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [String] $ConfigPath,

        [parameter(
            Position=2,
            ParameterSetName="File")]
        [ValidateNotNullOrEmpty()]
        [String] $LabPath,

        [Parameter(
            Position=3,
            ParameterSetName="Lab",
            Mandatory=$true,
            ValueFromPipeline=$true)]
        [ValidateNotNullOrEmpty()]
        $Lab,

        [Parameter(
            Position=4)]
        [Switch] $CheckEnvironment,

        [Parameter(
            Position=5)]
        [Switch] $Force,
        
        [Parameter(
            Position=6)]
        [Switch] $OffLine

    ) # Param

    begin
    {
        # Create a splat array containing force if it is set
        $ForceSplat = @{}
        if ($PSBoundParameters.ContainsKey('Force'))
        {
            $ForceSplat = @{ Force = $true }
        } # if

        # Remove some PSBoundParameters so we can Splat
        $null = $PSBoundParameters.Remove('CheckEnvironment')
        $null = $PSBoundParameters.Remove('Force')

        if ($CheckEnvironment)
        {
            # Check Hyper-V
            InstallHyperV `
                -ErrorAction Stop
        } # if

        # Ensure WS-Man is enabled
        EnableWSMan `
            @ForceSplat `
            -ErrorAction Stop

        if (!($PSBoundParameters.ContainsKey('OffLine')))
        {
        # Install Package Providers
        InstallPackageProviders `
            @ForceSplat `
            -ErrorAction Stop

        # Register Package Sources
        RegisterPackageSources `
            @ForceSplat `
            -ErrorAction Stop
        }
        
        $null = $PSBoundParameters.Remove('Offline')

        if ($PSCmdlet.ParameterSetName -eq 'File')
        {
            # Read the configuration
            $Lab = Get-Lab `
                @PSBoundParameters `
                -ErrorAction Stop
        } # if
    } # begin

    process
    {
        # Initialize the core Lab components
        # Check Lab Folder structure
        WriteMessage -Message $($LocalizedData.InitializingLabFoldersMesage)

        # Check folders are defined
        [String] $LabPath = $Lab.labbuilderconfig.settings.labpath
        if (-not (Test-Path -Path $LabPath))
        {
            WriteMessage -Message $($LocalizedData.CreatingLabFolderMessage `
                -f 'LabPath',$LabPath)

            $null = New-Item `
                -Path $LabPath `
                -Type Directory
        }

        [String] $VHDParentPath = $Lab.labbuilderconfig.settings.vhdparentpathfull
        if (-not (Test-Path -Path $VHDParentPath))
        {
            WriteMessage -Message $($LocalizedData.CreatingLabFolderMessage `
                -f 'VHDParentPath',$VHDParentPath)

            $null = New-Item `
                -Path $VHDParentPath `
                -Type Directory
        }

        [String] $ResourcePath = $Lab.labbuilderconfig.settings.resourcepathfull
        if (-not (Test-Path -Path $ResourcePath))
        {
            WriteMessage -Message $($LocalizedData.CreatingLabFolderMessage `
                -f 'ResourcePath',$ResourcePath)

            $null = New-Item `
                -Path $ResourcePath `
                -Type Directory
        }

        # Install Hyper-V Components
        WriteMessage -Message $($LocalizedData.InitializingHyperVComponentsMesage)

        # Create the LabBuilder Management Network switch and assign VLAN
        # Used by host to communicate with Lab VMs
        [String] $ManagementSwitchName = GetManagementSwitchName `
            -Lab $Lab
        if ($Lab.labbuilderconfig.switches.ManagementVlan)
        {
            [Int32] $ManagementVlan = $Lab.labbuilderconfig.switches.ManagementVlan
        }
        else
        {
            [Int32] $ManagementVlan = $Script:DefaultManagementVLan
        }
        if ((Get-VMSwitch | Where-Object -Property Name -eq $ManagementSwitchName).Count -eq 0)
        {
            $null = New-VMSwitch `
                -SwitchType Internal `
                -Name $ManagementSwitchName `
                -ErrorAction Stop

            WriteMessage -Message $($LocalizedData.CreatingLabManagementSwitchMessage `
                -f $ManagementSwitchName,$ManagementVlan)
        }
        # Check the Vlan ID of the adapter on the switch
        $ExistingManagementAdapter = Get-VMNetworkAdapter `
            -ManagementOS `
            -Name $ManagementSwitchName `
            -ErrorAction Stop
        $ExistingVlan = (Get-VMNetworkAdapterVlan `
            -VMNetworkAdaptername $ExistingManagementAdapter.Name `
            -ManagementOS).AccessVlanId

        if ($ExistingVlan -ne $ManagementVlan)
        {
            WriteMessage -Message $($LocalizedData.UpdatingLabManagementSwitchMessage `
                -f $ManagementSwitchName,$ManagementVlan)

            Set-VMNetworkAdapterVlan `
                -VMNetworkAdapterName $ManagementSwitchName `
                -ManagementOS `
                -Access `
                -VlanId $ManagementVlan `
                -ErrorAction Stop
        }

        # Download any Resource Modules required by this Lab
        $ResourceModules = Get-LabResourceModule `
            -Lab $Lab
        Initialize-LabResourceModule `
            -Lab $Lab `
            -ResourceModules $ResourceModules `
            -ErrorAction Stop

        # Download any Resource MSUs required by this Lab
        $ResourceMSUs = Get-LabResourceMSU `
            -Lab $Lab
        Initialize-LabResourceMSU `
            -Lab $Lab `
            -ResourceMSUs $ResourceMSUs `
            -ErrorAction Stop

        # Initialize the Switches
        $Switches = Get-LabSwitch `
            -Lab $Lab
        Initialize-LabSwitch `
            -Lab $Lab `
            -Switches $Switches `
            -ErrorAction Stop

        # Initialize the VM Template VHDs
        $VMTemplateVHDs = Get-LabVMTemplateVHD `
            -Lab $Lab
        Initialize-LabVMTemplateVHD `
            -Lab $Lab `
            -VMTemplateVHDs $VMTemplateVHDs `
            -ErrorAction Stop

        # Initialize the VM Templates
        $VMTemplates = Get-LabVMTemplate `
            -Lab $Lab
        Initialize-LabVMTemplate `
            -Lab $Lab `
            -VMTemplates $VMTemplates `
            -ErrorAction Stop

        # Initialize the VMs
        $VMs = Get-LabVM `
            -Lab $Lab `
            -VMTemplates $VMTemplates `
            -Switches $Switches
        Initialize-LabVM `
            -Lab $Lab `
            -VMs $VMs `
            -ErrorAction Stop

        WriteMessage -Message $($LocalizedData.LabInstallCompleteMessage `
            -f $Lab.labbuilderconfig.name,$Lab.labbuilderconfig.settings.labpath)
    } # process
    end
    {
    } # end
} # Install-Lab


<#
.SYNOPSIS
    Update a Lab.
.DESCRIPTION
    This cmdlet will update the existing Hyper-V lab environment defined by the
    LabBuilder configuration file provided.
 
    If components of the Lab are missing they will be added.
 
    If components of the Lab already exist, they will be updated if they differ
    from the settings in the Configuration file.
.PARAMETER ConfigPath
    The path to the LabBuilder configuration XML file.
.PARAMETER LabPath
    The optional path to update the Lab in - overrides the LabPath setting in the
    configuration file.
.PARAMETER Lab
    The Lab object returned by Get-Lab of the lab to update.
.EXAMPLE
    Update-Lab -ConfigPath c:\mylab\config.xml
    Update the lab defined in the c:\mylab\config.xml LabBuilder configuration file.
.EXAMPLE
    Get-Lab -ConfigPath c:\mylab\config.xml | Update-Lab
    Update the lab defined in the c:\mylab\config.xml LabBuilder configuration file.
.OUTPUTS
    None
#>

Function Update-Lab {
    [CmdLetBinding(DefaultParameterSetName="Lab")]
    param
    (
        [parameter(
            Position=1,
            ParameterSetName="File",
            Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [String] $ConfigPath,

        [parameter(
            Position=2,
            ParameterSetName="File")]
        [ValidateNotNullOrEmpty()]
        [String] $LabPath,

        [Parameter(
            Position=3,
            ParameterSetName="Lab",
            Mandatory=$true,
            ValueFromPipeline=$true)]
        [ValidateNotNullOrEmpty()]
        $Lab
    ) # Param

    begin
    {
        if ($PSCmdlet.ParameterSetName -eq 'File')
        {
            # Read the configuration
            $Lab = Get-Lab `
                @PSBoundParameters
        } # if
    } # begin

    process
    {
        Install-Lab `
            @PSBoundParameters

        WriteMessage -Message $($LocalizedData.LabUpdateCompleteMessage `
            -f $Lab.labbuilderconfig.name,$Lab.labbuilderconfig.settings.fullconfigpath)
    } # process

    end
    {
    } # end
} # Update-Lab


<#
.SYNOPSIS
     Uninstall the components of an existing Lab.
.DESCRIPTION
    This function will attempt to remove the components of the lab specified
    in the provided LabBuilder configuration file.
 
    It will always remove any Lab Virtual Machines, but can also optionally
    remove:
    Switches
    VM Templates
    VM Template VHDs
.PARAMETER ConfigPath
    The path to the LabBuilder configuration XML file.
.PARAMETER LabPath
    The optional path to uninstall the Lab from - overrides the LabPath setting in the
    configuration file.
.PARAMETER Lab
    The Lab object returned by Get-Lab of the lab to uninstall.
.PARAMETER RemoveSwitch
    Causes the switches defined by this to be removed.
.PARAMETER RemoveVMTemplate
    Causes the VM Templates created by this to be be removed.
.PARAMETER RemoveVMFolder
    Causes the VM folder created to contain the files for any the
    VMs in this Lab to be removed.
.PARAMETER RemoveVMTemplateVHD
    Causes the VM Template VHDs that are used in this lab to be
    deleted.
.PARAMETER RemoveLabFolder
    Causes the entire folder containing this Lab to be deleted.
.EXAMPLE
    Uninstall-Lab `
        -ConfigPath c:\mylab\config.xml `
        -RemoveSwitch`
        -RemoveVMTemplate `
        -RemoveVMFolder `
        -RemoveVMTemplateVHD `
        -RemoveLabFolder
    Completely uninstall all components in the lab defined in the
    c:\mylab\config.xml LabBuilder configuration file.
.EXAMPLE
    Get-Lab -ConfigPath c:\mylab\config.xml | Uninstall-Lab `
        -RemoveSwitch`
        -RemoveVMTemplate `
        -RemoveVMFolder `
        -RemoveVMTemplateVHD `
        -RemoveLabFolder
    Completely uninstall all components in the lab defined in the
    c:\mylab\config.xml LabBuilder configuration file.
.OUTPUTS
    None
#>

Function Uninstall-Lab {
    [CmdLetBinding(DefaultParameterSetName="Lab",
        SupportsShouldProcess = $true,
        ConfirmImpact = 'High')]
    param
    (
        [parameter(
            Position=1,
            ParameterSetName="File",
            Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [String] $ConfigPath,

        [parameter(
            Position=2,
            ParameterSetName="File")]
        [ValidateNotNullOrEmpty()]
        [String] $LabPath,

        [Parameter(
            Position=3,
            ParameterSetName="Lab",
            Mandatory=$true,
            ValueFromPipeline=$true)]
        [ValidateNotNullOrEmpty()]
        $Lab,

        [Parameter(
            Position=4)]
        [Switch] $RemoveSwitch,

        [Parameter(
            Position=5)]
        [Switch] $RemoveVMTemplate,

        [Parameter(
            Position=6)]
        [Switch] $RemoveVMFolder,

        [Parameter(
            Position=7)]
        [Switch] $RemoveVMTemplateVHD,

        [Parameter(
            Position=8)]
        [Switch] $RemoveLabFolder
    ) # Param

    begin
    {
        # Remove some PSBoundParameters so we can Splat
        $null = $PSBoundParameters.Remove('RemoveSwitch')
        $null = $PSBoundParameters.Remove('RemoveVMTemplate')
        $null = $PSBoundParameters.Remove('RemoveVMFolder')
        $null = $PSBoundParameters.Remove('RemoveVMTemplateVHD')
        $null = $PSBoundParameters.Remove('RemoveLabFolder')

        if ($PSCmdlet.ParameterSetName -eq 'File')
        {
            # Read the configuration
            $Lab = Get-Lab `
                @PSBoundParameters
        } # if
    } # begin

    process
    {
        if ($PSCmdlet.ShouldProcess( 'LocalHost', `
            ($LocalizedData.ShouldUninstallLab `
            -f $Lab.labbuilderconfig.name,$Lab.labbuilderconfig.settings.labpath )))
        {
            # Remove the VMs
            $VMSplat = @{}
            if ($RemoveVMFolder)
            {
                $VMSplat += @{ RemoveVMFolder = $true }
            } # if
            $null = Remove-LabVM `
                -Lab $Lab `
                @VMSplat

            # Remove the VM Templates
            if ($RemoveVMTemplate)
            {
                if ($PSCmdlet.ShouldProcess( 'LocalHost', `
                    ($LocalizedData.ShouldRemoveVMTemplate `
                    -f $Lab.labbuilderconfig.name,$Lab.labbuilderconfig.settings.labpath )))
                {
                    $null = Remove-LabVMTemplate `
                        -Lab $Lab
                } # if
            } # if

            # Remove the VM Switches
            if ($RemoveSwitch)
            {
                if ($PSCmdlet.ShouldProcess( 'LocalHost', `
                    ($LocalizedData.ShouldRemoveSwitch `
                    -f $Lab.labbuilderconfig.name,$Lab.labbuilderconfig.settings.labpath )))
                {
                    $null = Remove-LabSwitch `
                        -Lab $Lab
                } # if
            } # if

            # Remove the VM Template VHDs
            if ($RemoveVMTemplateVHD)
            {
                if ($PSCmdlet.ShouldProcess( 'LocalHost', `
                    ($LocalizedData.ShouldRemoveVMTemplateVHD `
                    -f $Lab.labbuilderconfig.name,$Lab.labbuilderconfig.settings.labpath )))
                {
                    $null = Remove-LabVMTemplateVHD `
                        -Lab $Lab
                } # if
            } # if

            # Remove the Lab Folder
            if ($RemoveLabFolder)
            {
                if (Test-Path -Path $Lab.labbuilderconfig.settings.labpath)
                {
                    if ($PSCmdlet.ShouldProcess( 'LocalHost', `
                        ($LocalizedData.ShouldRemoveLabFolder `
                        -f $Lab.labbuilderconfig.name,$Lab.labbuilderconfig.settings.labpath )))
                    {
                        Remove-Item `
                            -Path $Lab.labbuilderconfig.settings.labpath `
                            -Recurse `
                            -Force
                    } # if
                } # if
            } # if

            # Remove the LabBuilder Management Network switch
            [String] $ManagementSwitchName = GetManagementSwitchName `
                -Lab $Lab
            if ((Get-VMSwitch | Where-Object -Property Name -eq $ManagementSwitchName).Count -ne 0)
            {
                $null = Remove-VMSwitch `
                    -Name $ManagementSwitchName

                WriteMessage -Message $($LocalizedData.RemovingLabManagementSwitchMessage `
                    -f $ManagementSwitchName)
            }

            WriteMessage -Message $($LocalizedData.LabUninstallCompleteMessage `
                -f $Lab.labbuilderconfig.name,$Lab.labbuilderconfig.settings.labpath )
        } # if
    } # process

    end
    {
    } # end
} # Uninstall-Lab


<#
.SYNOPSIS
    Starts an existing Lab.
.DESCRIPTION
    This cmdlet will start all the Hyper-V virtual machines definied in a Lab
    configuration.
 
    It will use the Bootorder attribute (if defined) for any VMs to determine
    the order they should be booted in. If a Bootorder is not specified for a
    machine, it will be booted after all machines with a defined boot order.
 
    The lower the Bootorder value for a machine the earlier it will be started
    in the start process.
 
    Machines will be booted in series, with each machine starting once the
    previous machine has completed startup and has a management IP address.
 
    If a Virtual Machine in the Lab is already running, it will be ignored
    and the next machine in series will be started.
 
    If more than one Virtual Machine shares the same Bootorder value, then
    these machines will be booted in parallel, with the boot process only
    continuing onto the next Bootorder when all these machines are booted.
 
    If a Virtual Machine specified in the configuration is not found an
    exception will be thrown.
 
    If a Virtual Machine takes longer than the StartupTimeout then an exception
    will be thown but the Start process will continue.
 
    If a Bootorder of 0 is specifed then the Virtual Machine will not be booted at
    all. This is useful for things like Root CA VMs that only need to started when
    the Lab is created.
.PARAMETER ConfigPath
    The path to the LabBuilder configuration XML file.
.PARAMETER LabPath
    The optional path to install the Lab to - overrides the LabPath setting in the
    configuration file.
.PARAMETER Lab
    The Lab object returned by Get-Lab of the lab to start.
.PARAMETER StartupTimeout
    The maximum number of seconds that the process will wait for a VM to startup.
    Defaults to 90 seconds.
.EXAMPLE
    Start-Lab -ConfigPath c:\mylab\config.xml
    Start the lab defined in the c:\mylab\config.xml LabBuilder configuration file.
.EXAMPLE
    Get-Lab -ConfigPath c:\mylab\config.xml | Start-Lab
    Start the lab defined in the c:\mylab\config.xml LabBuilder configuration file.
.OUTPUTS
    None
#>

Function Start-Lab {
    [CmdLetBinding(DefaultParameterSetName="Lab")]
    param
    (
        [parameter(
            Position=1,
            ParameterSetName="File",
            Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [String] $ConfigPath,

        [parameter(
            Position=2,
            ParameterSetName="File")]
        [ValidateNotNullOrEmpty()]
        [String] $LabPath,

        [Parameter(
            Position=3,
            ParameterSetName="Lab",
            Mandatory=$true,
            ValueFromPipeline=$true)]
        [ValidateNotNullOrEmpty()]
        $Lab,

        [Parameter(
            Position=4)]
        [Int] $StartupTimeout = $Script:StartupTimeout
    ) # Param

    begin
    {
        # Remove some PSBoundParameters so we can Splat
        $null = $PSBoundParameters.Remove('StartupTimeout')

        if ($PSCmdlet.ParameterSetName -eq 'File')
        {
            # Read the configuration
            $Lab = Get-Lab `
                @PSBoundParameters
        } # if
    } # begin

    process
    {
        # Get the VMs
        $VMs = Get-LabVM `
            -Lab $Lab

        # Get the bootorders by lowest first and ignoring 0 and call
        $BootOrders = @( ($VMs |
            Where-Object -FilterScript { ($_.Bootorder -gt 0) } ).Bootorder )
        $BootPhases = @( ($Bootorders |
            Sort-Object -Unique) )

        # Step through each of these "Bootphases" waiting for them to complete
        foreach ($BootPhase in $BootPhases)
        {
            # Process this "Bootphase"
            WriteMessage -Message $($LocalizedData.StartingBootPhaseVMsMessage `
                -f $BootPhase)

            # Get all VMs in this "Bootphase"
            $BootVMs = @( $VMs |
                Where-Object -FilterScript { ($_.BootOrder -eq $BootPhase) } )

            [DateTime] $StartPhase = Get-Date
            [boolean] $PhaseComplete = $False
            [boolean] $PhaseAllBooted = $True
            [int] $VMCount = $BootVMs.Count
            [int] $VMNumber = 0

            # Loop through all the VMs in this "Bootphase" repeatedly
            # until timeout occurs or PhaseComplete is marked as complete
            while (-not $PhaseComplete `
                -and ((Get-Date) -lt $StartPhase.AddSeconds($StartupTimeout)))
            {
                # Get the VM to boot/check
                $VM = $BootVMs[$VMNumber]
                $VMName = $VM.Name

                # Get the actual Hyper-V VM object
                $VMObject = Get-VM `
                    -Name $VMName `
                    -ErrorAction SilentlyContinue
                if (-not $VMObject)
                {
                    # if the VM does not exist then throw a non-terminating exception
                    $ExceptionParameters = @{
                        errorId = 'VMDoesNotExistError'
                        errorCategory = 'InvalidArgument'
                        errorMessage = $($LocalizedData.VMDoesNotExistError `
                            -f $VMName)

                    }
                    ThrowException @ExceptionParameters
                } # if

                # Start the VM if it is off
                if ($VMObject.State -eq 'Off')
                {
                    WriteMessage -Message $($LocalizedData.StartingVMMessage `
                        -f $VMName)
                    Start-VM `
                        -VM $VMObject
                } # if

                # Use the allocation of a Management IP Address as an indicator
                # the machine has booted
                $ManagementIP = GetVMManagementIPAddress `
                    -Lab $Lab `
                    -VM $VM `
                    -ErrorAction SilentlyContinue
                if (-not ($ManagementIP))
                {
                    # It has not booted
                    $PhaseAllBooted = $False
                } # if
                $VMNumber++
                if ($VMNumber -eq $VMCount)
                {
                    # We have stepped through all VMs in this Phase so check
                    # if all have booted, otherwise reset the loop.
                    if ($PhaseAllBooted)
                    {
                        # if we have gone through all VMs in this "Bootphase"
                        # and they're all marked as booted then we can mark
                        # this phase as complete and allow moving on to the next one
                        WriteMessage -Message $($LocalizedData.AllBootPhaseVMsStartedMessage `
                            -f $BootPhase)
                        $PhaseComplete = $True
                    }
                    else
                    {
                        $PhaseAllBooted = $True
                    } # if
                    # Reset the VM Loop
                    $VMNumber = 0
                } # if
            } # while

            # Did we timeout?
            if (-not ($PhaseComplete))
            {
                # Yes, throw an exception
                $ExceptionParameters = @{
                    errorId = 'BootPhaseVMsTimeoutError'
                    errorCategory = 'InvalidArgument'
                    errorMessage = $($LocalizedData.BootPhaseStartVMsTimeoutError `
                        -f $BootPhase)

                }
                ThrowException @ExceptionParameters
            } # if
        } # foreach

        WriteMessage -Message $($LocalizedData.LabStartCompleteMessage `
            -f $Lab.labbuilderconfig.name,$Lab.labbuilderconfig.settings.fullconfigpath)
    } # process

    end
    {
    } # end
} # Start-Lab


<#
.SYNOPSIS
    Stop an existing Lab.
.DESCRIPTION
    This cmdlet will stop all the Hyper-V virtual machines definied in a Lab
    configuration.
 
    It will use the Bootorder attribute (if defined) for any VMs to determine
    the order they should be shutdown in. If a Bootorder is not specified for a
    machine, it will be shutdown before all machines with a defined boot order.
 
    The higher the Bootorder value for a machine the earlier it will be shutdown
    in the stop process.
 
    The Virtual Machines will be shutdown in REVERSE Bootorder.
 
    Machines will be shutdown in series, with each machine shutting down once the
    previous machine has completed shutdown.
 
    If a Virtual Machine in the Lab is already shutdown, it will be ignored
    and the next machine in series will be shutdown.
 
    If more than one Virtual Machine shares the same Bootorder value, then
    these machines will be shutdown in parallel, with the shutdown process only
    continuing onto the next Bootorder when all these machines are shutdown.
 
    If a Virtual Machine specified in the configuration is not found an
    exception will be thrown.
 
    If a Virtual Machine takes longer than the ShutdownTimeout then an exception
    will be thown but the Stop process will continue.
.PARAMETER ConfigPath
    The path to the LabBuilder configuration XML file.
.PARAMETER LabPath
    The optional path to install the Lab to - overrides the LabPath setting in the
    configuration file.
.PARAMETER Lab
    The Lab object returned by Get-Lab of the lab to start.
.PARAMETER ShutdownTimeout
    The maximum number of seconds that the process will wait for a VM to shutdown.
    Defaults to 30 seconds.
.EXAMPLE
    Stop-Lab -ConfigPath c:\mylab\config.xml
    Stop the lab defined in the c:\mylab\config.xml LabBuilder configuration file.
.EXAMPLE
    Get-Lab -ConfigPath c:\mylab\config.xml | Stop-Lab
    Stop the lab defined in the c:\mylab\config.xml LabBuilder configuration file.
.OUTPUTS
    None
#>

Function Stop-Lab {
    [CmdLetBinding(DefaultParameterSetName="Lab")]
    param
    (
        [parameter(
            Position=1,
            ParameterSetName="File",
            Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [String] $ConfigPath,

        [parameter(
            Position=2,
            ParameterSetName="File")]
        [ValidateNotNullOrEmpty()]
        [String] $LabPath,

        [Parameter(
            Position=3,
            ParameterSetName="Lab",
            Mandatory=$true,
            ValueFromPipeline=$true)]
        [ValidateNotNullOrEmpty()]
        $Lab
    ) # Param

    begin
    {
        # Remove some PSBoundParameters so we can Splat
        if ($PSCmdlet.ParameterSetName -eq 'File')
        {
            # Read the configuration
            $Lab = Get-Lab `
                @PSBoundParameters
        } # if
    } # begin

    process
    {
        # Get the VMs
        $VMs = Get-LabVM `
            -Lab $Lab

        # Get the bootorders by highest first and ignoring 0
        $BootOrders = @( ($VMs |
            Where-Object -FilterScript { ($_.Bootorder -gt 0) } ).Bootorder )
        $BootPhases = @( ($Bootorders |
            Sort-Object -Unique -Descending) )

        # Step through each of these "Bootphases" waiting for them to complete
        foreach ($BootPhase in $BootPhases)
        {
            # Process this "Bootphase"
            WriteMessage -Message $($LocalizedData.StoppingBootPhaseVMsMessage `
                -f $BootPhase)

            # Get all VMs in this "Bootphase"
            $BootVMs = @( $VMs |
                Where-Object -FilterScript { ($_.BootOrder -eq $BootPhase) } )

            [DateTime] $StartPhase = Get-Date
            [boolean] $PhaseComplete = $False
            [boolean] $PhaseAllStopped = $True
            [int] $VMCount = $BootVMs.Count
            [int] $VMNumber = 0

            # Loop through all the VMs in this "Bootphase" repeatedly
            while (-not $PhaseComplete)
            {
                # Get the VM to boot/check
                $VM = $BootVMs[$VMNumber]
                $VMName = $VM.Name

                # Get the actual Hyper-V VM object
                $VMObject = Get-VM `
                    -Name $VMName `
                    -ErrorAction SilentlyContinue
                if (-not $VMObject)
                {
                    # if the VM does not exist then throw a non-terminating exception
                    $ExceptionParameters = @{
                        errorId = 'VMDoesNotExistError'
                        errorCategory = 'InvalidArgument'
                        errorMessage = $($LocalizedData.VMDoesNotExistError `
                            -f $VMName)

                    }
                    ThrowException @ExceptionParameters
                } # if

                # Shutodwn the VM if it is off
                if ($VMObject.State -eq 'Running')
                {
                    WriteMessage -Message $($LocalizedData.StoppingVMMessage `
                        -f $VMName)
                    $null = Stop-VM `
                        -VM $VMObject `
                        -Force `
                        -ErrorAction Continue
                } # if

                # Determine if the VM has stopped.
                if ((Get-VM -VMName $VMName).State -ne 'Off')
                {
                    # It has not stopped
                    $PhaseAllStopped = $False
                } # if
                $VMNumber++
                if ($VMNumber -eq $VMCount)
                {
                    # We have stepped through all VMs in this Phase so check
                    # if all have stopped, otherwise reset the loop.
                    if ($PhaseAllStopped)
                    {
                        # if we have gone through all VMs in this "Bootphase"
                        # and they're all marked as stopped then we can mark
                        # this phase as complete and allow moving on to the next one
                        WriteMessage -Message $($LocalizedData.AllBootPhaseVMsStoppedMessage `
                            -f $BootPhase)
                        $PhaseComplete = $True
                    }
                    else
                    {
                        $PhaseAllStopped = $True
                    } # if
                    # Reset the VM Loop
                    $VMNumber = 0
                } # if
            } # while
        } # foreach

        WriteMessage -Message $($LocalizedData.LabStopCompleteMessage `
            -f $Lab.labbuilderconfig.name,$Lab.labbuilderconfig.settings.fullconfigpath)
    } # process

    end
    {
    } # end
} # Stop-Lab