Showing posts with label PowerShell. Show all posts
Showing posts with label PowerShell. Show all posts

Tuesday, April 8, 2014

Enumerating TFS Permissions for items within the TPC for audit and logging

Below is a powershell cmdlet that I threw together to enumerate data out of our TFS instance.  This is used to keep track of permissions in the event we need to re-create them and to allow us to audit it to make sure things don't change.

This can also be used to keep a pre-production environment's permissions in sync with Production.

In 2014 I was not the best at Powershell but it works :)

param ($serverName = $(throw 'please specify a TFS server name'))

function GetGroupMembership {
 [CmdletBinding()]
 [OutputType([System.Data.Datatable])]
 PARAM (
  [Parameter(Mandatory=$true, Position = 0)]
  [Microsoft.TeamFoundation.Framework.Client.TeamFoundationIdentity]
  $TFIdentity
 )
 
 $tabName = $TFIdentity.DisplayName + "Membership"
 $table = New-Object system.Data.DataTable “$tabName”
 $col1 = New-Object system.Data.DataColumn GroupName,([string])
 $col2 = New-Object system.Data.DataColumn DisplayName,([string])
 $col3 = New-Object system.Data.DataColumn UniqueName,([string])

 #Add the Columns
 $table.columns.add($col1)
 $table.columns.add($col2)
 $table.columns.add($col3)
  
 #Membership
 $GroupIdentity = $ims.ReadIdentity($TFIdentity.Descriptor,[Microsoft.TeamFoundation.Framework.Common.MembershipQuery]::Direct,[Microsoft.TeamFoundation.Framework.Common.ReadIdentityOptions]::TrueSid)
 $members = $ims.ReadIdentities($GroupIdentity.Members,[Microsoft.TeamFoundation.Framework.Common.MembershipQuery]::Direct,[Microsoft.TeamFoundation.Framework.Common.ReadIdentityOptions]::TrueSid)
 foreach($member in $members)
 {
  $row = $table.NewRow()

  #Enter data in the row
  $row.GroupName = $TFIdentity.DisplayName
  $row.DisplayName = $member.DisplayName
  $row.UniqueName = $member.UniqueName
  
  #Add the row to the table
  $table.Rows.Add($row)
 }
 
 return @(,$table )
}

function GetPermissions {
 [CmdletBinding()]
 [OutputType([System.Data.Datatable])]
 PARAM (
  [Parameter(Mandatory=$true, Position = 0)]
  $QueryName,
  [Parameter(Mandatory=$true, Position = 1)]
  [Microsoft.TeamFoundation.Framework.Client.AccessControlList]
  $acl,
  [Parameter(Mandatory=$true, Position = 2)]
  $namespace,
  [Parameter(Mandatory=$true, Position = 3)]
  $objectType,
  [Parameter(Mandatory=$true, Position = 4)]
  $objectPath
 )
 
 $PermissionstabName = $QueryName
 
 $table = New-Object system.Data.DataTable “$PermissionstabName”
 $col1 = New-Object system.Data.DataColumn ObjectPath,([string])
 $col2 = New-Object system.Data.DataColumn ObjectType,([string])
 $col3 = New-Object system.Data.DataColumn Name,([string])
 $col4 = New-Object system.Data.DataColumn Permission,([string])
 $col5 = New-Object system.Data.DataColumn Value,([string])

 #Add the Columns
 $table.columns.add($col1)
 $table.columns.add($col2)
 $table.columns.add($col3)
 $table.columns.add($col4)
 $table.columns.add($col5)

 foreach ($ace in $acl.AccessControlEntries)
 {
  if ($ace.IsEmpty)
  {
   continue
  }
  
  $haspermission = $false
  $DenyPermissions = @{}
  $CalculatedPermissions = @{}
  $AllowPermissions = @{}
  foreach ($action in $namespace.description.Actions)
  {
   
   $allowed = ($action.bit -band $ace.Allow) 
   $denied =  ($action.bit -band $ace.Deny)
   if (-not $allowed -and -not $denied)
   {
    continue
   }
   $haspermission  =$true
   if ($allowed)
   { 
    $CalculatedPermissions.Add($action.Name,"Allow")
    $AllowPermissions.Add($action.Name,$action.Name)
   }
   else
   {
    $CalculatedPermissions.Add($action.Name,"Deny")
    $DenyPermissions.Add($action.Name,$action.Name)
   }
   
  }
  
  if ($haspermission)
  {
   
   $identity = $ims.ReadIdentity($ace.Descriptor,[Microsoft.TeamFoundation.Framework.Common.MembershipQuery]::None,[Microsoft.TeamFoundation.Framework.Common.ReadIdentityOptions]::IncludeReadFromSource)
   
   $name = $identity.DisplayName
   
   Foreach($permission in $CalculatedPermissions.GetEnumerator())
   {
    $row = $table.NewRow()

    #Enter data in the row
    $row.ObjectPath = $objectPath 
    $row.ObjectType = $objectType
    $row.Name = $name
    $row.Permission = $permission.Name
    $row.Value = $permission.value

    #Add the row to the table
    $table.Rows.Add($row)
   }
  }
 }
 
 return @(,$table )
}

[void][Reflection.Assembly]::LoadWithPartialName('Microsoft.TeamFoundation.Client')
[void][Reflection.Assembly]::LoadWithPartialName('Microsoft.TeamFoundation.VersionControl.Client')
[void][Reflection.Assembly]::LoadWithPartialName('Microsoft.TeamFoundation.WorkItemTracking.Client')
[void][Reflection.Assembly]::LoadWithPartialName('Microsoft.TeamFoundation.Build.Client')
[void][Reflection.Assembly]::LoadWithPartialName('Microsoft.TeamFoundation.Build.Common')
[void][Reflection.Assembly]::LoadWithPartialName('Microsoft.TeamFoundation')

$tfs = [Microsoft.TeamFoundation.Client.TeamFoundationServerFactory]::GetServer($serverName)

$css = $tfs.GetService([Microsoft.TeamFoundation.Server.ICommonStructureService])
$auth = $tfs.GetService([Microsoft.TeamFoundation.Server.IAuthorizationService])
$gss = $tfs.GetService([Microsoft.TeamFoundation.Server.IGroupSecurityService])
$ss = $tfs.GetService([Microsoft.TeamFoundation.Framework.Client.ISecurityService])
$vcs = $tfs.GetService([Microsoft.TeamFoundation.VersionControl.Client.VersionControlServer])
$ims = $tfs.GetService([Microsoft.TeamFoundation.Framework.Client.IIdentityManagementService])
$wis = $tfs.GetService([Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemStore]) 
$cssNamespace = $ss.GetSecurityNamespace([Microsoft.TeamFoundation.Server.AuthorizationSecurityConstants]::CommonStructureNodeSecurityGuid)

$membershipds = New-Object System.Data.DataSet
$permissiondt = New-Object System.Data.DataSet
$Objectpermissiondt = New-Object System.Data.DataSet

#get all TPC Groups
$tpcGroups = $IMS.ListApplicationGroups($null, [Microsoft.TeamFoundation.Framework.Common.ReadIdentityOptions]::None)

foreach ($tpcgroup in $tpcGroups)
{
 
 $members = GetGroupMembership $tpcgroup
 if ($members.rows.count -gt 0)
 {
  $membershipds.Tables.Add($members)
 }
}
$Namespaces = $ss.GetSecurityNamespaces()
#Get all the permissions for top level NameSpaces
foreach($namespace in $Namespaces)
{
 
 $NameSpaceToken = ""
 
    switch ($namespace.Description.DisplayName)
 {
  "Server" { $NameSpaceToken =  [Microsoft.TeamFoundation.Framework.Common.FrameworkSecurity]::FrameworkNamespaceToken}
  "Build" { $NameSpaceToken =  [Microsoft.TeamFoundation.Build.Common.BuildSecurity]::PrivilegesToken}
  "BuildAdministration" { $NameSpaceToken =  [Microsoft.TeamFoundation.Build.Common.BuildSecurity]::PrivilegesToken}
  "Workspaces" {  $NameSpaceToken =  [Microsoft.TeamFoundation.VersionControl.Common.SecurityConstants]::GlobalSecurityResource}
  "Collection" {}#Namespace:
  "WorkItemTrackingAdministration" {} #
  "CSS" {  }
  "Iteration" {}
  "VersionControlPrivileges" {$NameSpaceToken =  [Microsoft.TeamFoundation.VersionControl.Common.SecurityConstants]::GlobalSecurityResource} 
  "WorkItemQueryFolders" {}
  "Project" {  $NameSpaceToken = [Microsoft.TeamFoundation.PermissionNamespaces]::Project}
  default
  {
  continue
  }
 }
 try
 {
  $groupAcl = $namespace.QueryAccessControlList($NameSpaceToken,$null,$false)
  $table = GetPermissions $namespace.Discription.Name $groupAcl $namespace "Group" $namespace.Discription.Name
  $permissiondt.Tables.Add($table)
 }
 Catch
 {
 
 }
}

foreach ($project in $css.ListProjects())
{
 $projectGroups = $IMS.ListApplicationGroups($project.Uri,[Microsoft.TeamFoundation.Framework.Common.ReadIdentityOptions]::TrueSid)
  
 foreach($group in $projectGroups)
 {
 
  #only get the groups we care about
  if ($group.DisplayName -eq "[$($project.Name)]\Build Administrators" -or $group.DisplayName -eq "[$($project.Name)]\Project Administrators" -or $group.DisplayName -eq "[$($project.Name)]\Contributors" -or $group.DisplayName -eq "[$($project.Name)]\Readers")
  {
   
  }
  else
  {
   continue;
  }
  
  $members = GetGroupMembership $group
  if ($members.rows.count -gt 0)
  {
   $membershipds.Tables.Add($members)
  }
  
  $projectsecNameSpace = $ss.GetSecurityNamespace([Microsoft.TeamFoundation.Server.AuthorizationSecurityConstants]::ProjectSecurityGuid)
  
  #Get Project Permissions
  $ProjectSecurityToken = [Microsoft.TeamFoundation.Server.AuthorizationSecurityConstants]::ProjectSecurityPrefix + $project.Uri
  $groupacl = $projectsecNameSpace.QueryAccessControlList($ProjectSecurityToken,  [Microsoft.TeamFoundation.Framework.Client.IdentityDescriptor[]]@($group.descriptor), $false)
   
  $table =  GetPermissions $group.DisplayName  $groupAcl $projectsecNameSpace "Project" $project.Name
  $permissiondt.Tables.Add($table)
  
  #Get Top Level Build Permissions for the Project
  $BuildsecNameSpace = $ss.GetSecurityNamespace([Microsoft.TeamFoundation.Build.Common.BuildSecurity]::BuildNamespaceId)
  
  $teamProjectGuid = [Microsoft.TeamFoundation.LinkingUtilities]::DecodeUri($project.Uri).ToolSpecificId
  $groupacl = $BuildsecNameSpace.QueryAccessControlList($teamProjectGuid,  $null, $false)
  $tablename = $group.DisplayName + "Build Permissions"
  $table = GetPermissions $tablename $groupAcl $BuildsecNameSpace "Build" $project.Name
  
  $permissiondt.Tables.Add($table)
 }
 
 #Get Root Area Path Permissions
 $rootAreaNodeACL = $cssNamespace.QueryAccessControlList($wis.Projects[$project.Name].AreaRootNodeUri,$null,$false)
 $table = GetPermissions "$($project.Name) Root AreaPath Permissions" $rootAreaNodeACL  $cssNamespace "Area Path" $wis.Projects[$project.Name].Name
 $Objectpermissiondt.Tables.Add($table)

  
 #Get Child Area Path Permissions 
 foreach ($area in $wis.Projects[$project.Name].AreaRootNodes)
 {
  $areapath = $area.Path
  
  $areaseclist = $cssNamespace.QueryAccessControlList($area.uri, $null, $true)
  
  $table = GetPermissions "$areapath AreaPath Permissions" $areaseclist  $cssNamespace "Area Path" $areapath
  $Objectpermissiondt.Tables.Add($table)
 }
 
 try
 {
  $vcproject = $vcs.TryGetTeamProject($project.Name)
 }
 catch
 {
  continue
 }
 
 #Get Version Control permissions on all VC objects
 $vcAcls = $vcs.GetPermissions(@($vcproject.ServerItem), [Microsoft.TeamFoundation.VersionControl.Client.RecursionType]::Full)
  
 $table = New-Object system.Data.DataTable "$($project.Name)VCPermissions"
 $col1 = New-Object system.Data.DataColumn ObjectPath,([string])
 $col2 = New-Object system.Data.DataColumn ObjectType,([string])
 $col3 = New-Object system.Data.DataColumn Name,([string])
 $col4 = New-Object system.Data.DataColumn Permission,([string])
 $col5 = New-Object system.Data.DataColumn Value,([string])

 #Add the Columns
 $table.columns.add($col1)
 $table.columns.add($col2)
 $table.columns.add($col3)
 $table.columns.add($col4)
 $table.columns.add($col5)
 
 foreach($perm in $vcAcls)
 {
  foreach($entry in $perm.Entries)
  {
   foreach($allow in $entry.Allow)
   {
    
    $row = $table.NewRow()
    $row.ObjectPath = $perm.ServerItem
    $row.ObjectType = "VC"
    $row.Name = $entry.IdentityName
    $row.Permission = $allow
    $row.Value = "allow"
    
    #Add the row to the table
    $table.Rows.Add($row)
   }
   foreach($deny in $entry.Deny)
   {
    
    $row = $table.NewRow()
    $row.ObjectPath = $perm.ServerItem
    $row.ObjectType = "VC"
    $row.Name = $entry.IdentityName
    $row.Permission = $deny
    $row.Value = "deny"
    
    #Add the row to the table
    $table.Rows.Add($row)
   }
  }
 }
 
 $Objectpermissiondt.Tables.Add($table)
}

$membershipds.Tables | Format-Table -AutoSize 
$permissiondt.Tables | Format-Table -AutoSize 
$Objectpermissiondt.Tables | Format-Table -AutoSize

Wednesday, August 7, 2013

Migrating TFS (2010 or 2012) Personal Queries from one project to another via Powershell

The powershell script below can be saved run on a users system to migrate their personal TFS Queries from one project to another.  This is useful when migrating from TFS 2010 to TFS 2012.

You will need to update the script Source and Target Server URLs, TPC and Projects.  I went down the path to hard code them and not take them as arguments so users did not have to enter anything.  The script will throw errors if the query being migrated is not valid on the target project.
function ProcessQueryItem ($queryFolder, $parentFolder, $sourceProjectName)
{
 $newItem = $null
 foreach ($subQuery in $queryFolder)
 {
  if ($subQuery -is [Microsoft.TeamFoundation.WorkItemTracking.Client.QueryFolder])
  { 
   $newItem = new-object Microsoft.TeamFoundation.WorkItemTracking.Client.QueryFolder -ArgumentList    @($subQuery.Name)
   if (-not $parentFolder.Contains($subQuery.Name))
   {
    Write-Host "Adding Query Folder $subQuery"
    $parentFolder.Add($newItem);
    $WIStoreRight.Projects[$RightProject].QueryHierarchy.Save()
    ProcessQueryItem $subQuery $newItem $sourceProjectName
   }
   else
   {
    Write-Host "Query Folder "$subQuery.name" already exists" -foregroundcolor Yellow
   }

  }
  else
  {
   Write-Host "Creating query "$subQuery.Name
   $oldDef = [Microsoft.TeamFoundation.WorkItemTracking.Client.QueryDefinition]$subQuery
   $queryText = $oldDef.QueryText.Replace("$sourceProjectName\", "$RightProject\")
   $queryText = $queryText.Replace("[System.IterationPath] under '" + $sourceProjectName + "'", "[System.IterationPath] under '" + $RightProject +"'");
   $newItem = new-object Microsoft.TeamFoundation.WorkItemTracking.Client.QueryDefinition -ArgumentList @($subQuery.Name,$queryText)
      
    if ($parentFolder.Contains($subQuery.Name) -ne $true )
    {
    $parentFolder.Add($newItem)
    $WIStoreRight.Projects[$RightProject].QueryHierarchy.Save()
    }
    else
    {
    Write-Host "Query Definition $subQuery already exists" -foregroundcolor Yellow
    }
  
  }
 }
}

[URI]$Projecturileft = "http://sourceServer:8080/tfs/SourceTPC/"
$leftProject = "SourceProject"
[URI]$ProjecturiRight = "http://targetServer:8080/tfs/TargetTPC/"
$RightProject = "TargetProject"

[void][Reflection.Assembly]::LoadWithPartialName('Microsoft.TeamFoundation.WorkItemTracking.Client')
[void][Reflection.Assembly]::LoadWithPartialName('Microsoft.TeamFoundation.Client')
[void][Reflection.Assembly]::LoadWithPartialName('Microsoft.TeamFoundation.Server')

$tfsleft = [Microsoft.TeamFoundation.Client.TeamFoundationServerFactory]::GetServer($Projecturileft) 
$WIStoreleft = $tfsleft.GetService([Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemStore]) 
$WIStoreleft.RefreshCache($true)
$WIStoreleft.SyncToCache() | out-null

$LeftQuerystore = $WIStoreleft.Projects[$LeftProject].QueryHierarchy

$tfsRight = [Microsoft.TeamFoundation.Client.TeamFoundationServerFactory]::GetServer($ProjecturiRight) 
$WIStoreRight = $tfsRight.GetService([Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemStore]) 
$WIStoreRight.RefreshCache($true)
$WIStoreRight.SyncToCache() | out-null
foreach ($queryFolder in $LeftQuerystore)
{
 if ($queryFolder.IsPersonal)
 {
  $RightMyQueriesFolder = [Microsoft.TeamFoundation.WorkItemTracking.Client.QueryFolder]$WIStoreRight.Projects[$RightProject].QueryHierarchy["My Queries"];
  
  ProcessQueryItem $queryFolder $RightMyQueriesFolder $leftProject
  Write-Host "Personal Queries Migrated"
 }
}

Wednesday, July 27, 2011

How to create differencing disk VM from template on SCVMM 2012 via powershell

This quick post will provide an example of how to create a Virtual Machine (VM) based off an VMM template that utilizes differencing disks. 

This is special because VMM does not support this natively.  So you have to contact the target HyperV host (via WMI) to create the Diff disk.  In addition to need to make sure that the HyperV host has the parent VHD already in place. 

The Powershell cmdlet below does the following
  1. Looks up the specified Hardware profile
  2. Looks up the specified template (this is used to get the parent VHD location/name)
  3. Looks up info on the specified target HyperV host (so we can find the placement drive)
  4. Checks to see if the ParentVHD already exists on the HyperV hosts Placement Drive
  5. If the VHD does not exist it copies it to the HyperV host
  6. Creates Diff Disk via WMI on hyperV host pointing to parent VHD
  7. Creates the VM via the New-SCVirtualMachine cmdlet using the UseLocalVirtualHardDisk command. (this tells VMM to allow Diff disks).
It can be downloaded from http://dl.dropbox.com/u/3275573/Create_Diff_VM_From_Template.ps1
    param($VMName,$TemplateName = "Windows Server 2008 R2 SP1 Enterprise", $HardwareProfileName = "ServerHardware", $VMMHost = "SCVMM",$targethostname = $null)
    
    $Err = 0
    
    If ($VMName -eq $null)
    {
      Write-Host "`nERROR: Incorrect arguments" -foregroundcolor red -backgroundcolor black
       Write-Host "`nREQUIRED ARGUMENTS:" -foregroundcolor cyan
       Write-Host "`n   -VMName `"Name of the VM to create`"" -foregroundcolor cyan
       Write-Host "`n   -TemplateName `"Name of the VMM Template`"" -foregroundcolor cyan
       Write-Host "`n   -HardwareProfileName `"Name of the VMM Hardware Profile to use with the template`"" -foregroundcolor cyan
       Write-Host "`n   -VMMHost `"VMM Computername`"" -foregroundcolor cyan
       Write-Host "`n   -targetHostname `"Computername of HyperV host to target`"" -foregroundcolor cyan
      Exit $Err
    }
    
    function CreateDiffDiskOnHost
    {
                    param([string]$hostComputerName, [string]$childPath, [string]$parentPath)
                    #get the image mgmt service instance for the host computer
                    $VHDService = get-wmiobject -class "Msvm_ImageManagementService" -namespace "root\virtualization" -computername $hostComputerName
                    
                    #create a differencing disk from the base disk
                    $Result = $VHDService.CreateDifferencingVirtualHardDisk($childPath, $parentPath)
    }
    function TestFileLock {
        ## Attempts to open a file and trap the resulting error if the file is already open/locked
        param ([string]$filePath )
        $filelocked = $false
        $fileInfo = New-Object System.IO.FileInfo $filePath
        trap {
            Set-Variable -name locked -value $true -scope 1
            continue
        }
        $fileStream = $fileInfo.Open( [System.IO.FileMode]::OpenOrCreate, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None )
        if ($fileStream) {
            $fileStream.Close()
        }
        $obj = New-Object Object
        $obj | Add-Member Noteproperty FilePath -value $filePath
        $obj | Add-Member Noteproperty IsLocked -value $filelocked
        $obj
    }
    
    
    write-host "VMName is $VMName"
    write-host "Template is $TemplateName"
    write-host "Hardware Profile is $HardwareProfileName"
    write-host "VMMHost is $VMMHost"
    write-host "Target Host is $targethostname"
    
    $guid = [guid]::NewGuid()
    
    write-host "Lookup the base hardware Profile $HardwareProfileName"
    $HardwareProfile = Get-SCHardwareProfile -VMMServer $VMMHost  | where {$_.Name -eq $HardwareProfileName}
    if ($HardwareProfile -eq $null)
        {
            write-host "Failed to lookup HardwareProfile $HardwareProfileName"  -foregroundcolor red -backgroundcolor black
         EXIT 1
        }
    
    write-host "Getting information for template $TemplateName"
    $Template = Get-SCVMTemplate -VMMServer $VMMHost  -All | where {$_.Name -eq $TemplateName}
     if ($Template -eq $null)
        {
            write-host "Failed to lookup Template $TemplateName"  -foregroundcolor red -backgroundcolor black
         EXIT 1
        }
        
    #Get the VHD info for the template
    $VHD =  Get-SCVirtualharddisk | where {$_.Name -eq $template.VirtualDiskDrives[0].VirtualHardDisk}
    
    write-host "Template VHD Path " $VHD.SharePath
    
    if ($targethostname -eq $null)
    {
        write-host "Invalid targethostname"  -foregroundcolor red -backgroundcolor black
        exit 1
    }
    $vmhost = Get-SCVMHost $targethostname
    
    $vmHostPath = [string]$vmhost.vmpaths
    write-host "Target VMHost: "  $vmhost.name
    write-host "Target Drive: "  $vmhost.VMPaths
    
    $remotePath = "\\" + $vmhost.name + "\" + $vmHostPath.substring(0,1) + "$\" + [system.io.path]::GetFileName($VHD.SharePath)
    "Checking to see if Parent VHD exists on HV Host Drive"
    "RemotePath: $remotePath"
    
    if (Test-Path $remotePath)
    {
     "Parent VHD already exists on HV Host"
    }
    else
    {
     "$RemotePath does not exist, going to Copy"
     [System.IO.File]::Copy($VHD.SharePath,$remotePath); 
    }
    
    "Creating Diff Disk"
    $targetVHDPath = $vmHostPath + $VMName + ".vhd"
    $ParentVHDPath = $vmHostPath + [system.io.path]::GetFileName($VHD.SharePath)
    CreateDiffDiskOnHost "$vmhost" "$targetVHDPath" "$ParentVHDPath"
    Start-Sleep -s 20
    
    $targetVHDRemotePath = "\\" + $vmhost.name + "\" + $vmHostPath.substring(0,1) + "$\" + $VMName + ".vhd"
    if (Test-Path $targetVHDRemotePath)
    {
     "Diff Disk Created at:$targetVHDRemotePath"
    }
    else
    {
     "$targetVHDRemotePath does not exist yet.  Sleeping for a few"
     Start-Sleep -s 30
        if (Test-Path $targetVHDRemotePath)
        {
            "Disk was created."
        }
        else
        {
            write-host "Failed to find VHD $targetVHDRemotePath after sleep, exiting" -foregroundcolor red -backgroundcolor black
            exit 1
        }
    
    }
    #Set a random startup delay to reduce VM startup IOPs overload
    $startDelay = Get-Random -minimum 1 -maximum 30
    
    
    $lockstate = TestFileLock "$targetVHDRemotePath"
    if ($lockstate.IsLocked)
    {
        "File locked, sleeping"
        Start-Sleep -s 30
    }
    
    $mv = move-SCvirtualharddisk -Bus 0 -Lun 0 -IDE -path $targetVHDPath  -jobgroup $guid
    $description = $template.tag
    New-SCVirtualMachine -Name $vmName -VMHost $vmHost -VMTemplate $template -UseLocalVirtualHardDisk -HardwareProfile $HardwareProfile -ComputerName $vmName -path $vmHostPath -delaystartseconds $startDelay  -Description "$description" -mergeanswerfile $true -BlockDynamicOptimization $false -StartVM -JobGroup "$guid" -RunAsynchronously -StartAction "AlwaysAutoTurnOnVM" -StopAction "SaveVM"
    

    10 Years from last post

     Well world!   After the last almost 10 years I have been up to a few things in life and work.  Most recently I was working at Microsoft on ...