Inventory Large Folders

This is a fairly simple setup using DCM. Say you want to find out where space is being used on machines, either to find user caches of data that isn’t backed up or to help target folders to clean up when drives get full.

Warning: This is a very CPU intensive process. I recommend testing in your environment as execution times will vary greatly on individual configurations.

You will also need to extend DCM execution times so it doesn’t always report as failed. This is optional however as it will still run. See http://blogs.msdn.com/b/fei_xias_blog/archive/2013/10/21/system-center-2012-configmgr-using-vbs-to-extend-the-dcm-script-execution-timeout-value.aspx for instructions. I set mine to 1200 seconds, which is 20 minutes. You could also deploy this as an SCCM package that just runs the powershell file.

First we setup a configuration item

Large Folders 1 Large Folders 2 Large Folders 3 Large Folders 4

And put this in for the script to run:

Inspirations and some of the codebase documented in the comments. This has all been heavily modified for this purpose.

# Get-DirStats.ps1
# Written by Bill Stewart (bstewart@iname.com)
# Outputs file system directory statistics.
# https://gallery.technet.microsoft.com/scriptcenter/Outputs-directory-size-964d07ff
# http://blogs.technet.com/b/heyscriptingguy/archive/2012/05/25/getting-directory-sizes-in-powershell.aspx

#requires -version 2

<#
.SYNOPSIS
Outputs file system directory statistics.

.DESCRIPTION
Outputs file system directory statistics (number of files and the sum of all file sizes) for one or more directories.

.PARAMETER Path
Specifies a path to one or more file system directories. Wildcards are not permitted. The default path is the current directory (.).

.PARAMETER LiteralPath
Specifies a path to one or more file system directories. Unlike Path, the value of LiteralPath is used exactly as it is typed.

.PARAMETER Only
Outputs statistics for a directory but not any of its subdirectories.

.PARAMETER WriteWMI
Checks for and writes to custom WMI namespace

.PARAMETER Every
Outputs statistics for every directory in the specified path instead of only the first level of directories.

.PARAMETER FormatNumbers
Formats numbers in the output object to include thousands separators.

.PARAMETER ShowErrors
Disable error suppression

.PARAMETER Benchmarking
Show benchmarking statistics

.PARAMETER Size
Filter any directories under this amount in MB from being outputted

.PARAMETER Total
Outputs a summary object after all other output that sums all statistics.
#>

[CmdletBinding(DefaultParameterSetName="Path")]
param(
  [parameter(Position=0,Mandatory=$false,ParameterSetName="Path",ValueFromPipeline=$true)]
    [ValidateScript({Test-Path $_ -PathType 'Container'})] 
    $Path=(get-location).Path,
  [parameter(Position=0,Mandatory=$true,ParameterSetName="LiteralPath")]
    [ValidateScript({Test-Path $_ -PathType 'Container'})] 
    [String[]] $LiteralPath,
    [Switch] $Only,
    [Switch] $WriteWMI,
    [Switch] $Every,
    [Switch] $FormatNumbers,
    [Switch] $Total,
  [Switch] $ShowErrors,
  [Switch] $Benchmarking,	
    [ValidateRange(1,90000000)] 
    [Int] $Size
)

begin {
  #We're going to return the seconds it took to run for DCM
  $script:startTime = Get-Date
  if($Benchmarking){
  write-host "Script Started at $script:startTime"
  }

  #Setting the variable here since we are using DCM
  $WriteWMI = $true
  #Silence error output during writeWMI
  If($WriteWMI){$ErrorActionPreference = "silentlycontinue"}
  if ($ShowErrors){$ErrorActionPreference ="Continue"}

  #Create FSO to use later
  $fso = New-Object -comobject Scripting.FileSystemObject
    
  $ParamSetName = $PSCmdlet.ParameterSetName
  if ( $ParamSetName -eq "Path" ) {
    $PipelineInput = ( -not $PSBoundParameters.ContainsKey("Path") ) -and ( -not $Path )
  }
  elseif ( $ParamSetName -eq "LiteralPath" ) {
    $PipelineInput = $false
  }

  if ( $Size -gt 0 ) { $SizeFilter = $true } 
  else { $SizeFilter = $false }

  #Check and setup WMI if we are going to use that
  if ( $WriteWMI ) {
    #Check if there is a size filter, if not set to 2000MB because we only want really big folders to work with
    if ($Size -le 0) { 
  $Size = 2000
  $SizeFilter = $true 
  } 
    
    #Get Local Drive to search
    $script:LocalDrives = get-wmiobject win32_volume | ? { $_.DriveType -eq 3 } | % { get-psdrive $_.DriveLetter[0] } | Select Root

    #Delete existing instances
    $script:Class = "LargeFolders"
    If (Get-WmiObject -List -Namespace "root\cimv2" | Where-Object {$_.Name -eq $script:Class}) {
   Get-WmiObject -Namespace "root\cimv2" -Class $script:Class | Remove-WMIObject
       } 
  
  #Now rebuild it to make sure we have the right fields
  $subClass = New-Object System.Management.ManagementClass ("root\cimv2", [String]::Empty, $null); 
  $subClass["__CLASS"] = $script:Class; 
  $subClass.Qualifiers.Add("Static", $true)
  $subClass.Properties.Add("Path", [System.Management.CimType]::String, $false)
  $subClass.Properties["Path"].Qualifiers.Add("Key", $true)
  $subClass.Properties.Add("FileCount", [System.Management.CimType]::UInt32, $false)
    $subClass.Properties.Add("SubFolderCount", [System.Management.CimType]::UInt32, $false)
  $subClass.Properties.Add("SizeMB", [System.Management.CimType]::UInt32, $false)
    $subClass.Properties.Add("GatherMethod", [System.Management.CimType]::UInt8, $false)
    $subClass.Properties.Add("DateCreated", [System.Management.CimType]::String, $false)
    $subClass.Properties.Add("DateLastModified", [System.Management.CimType]::String, $false)
    $subClass.Properties.Add("SecondsToEnumerate", [System.Management.CimType]::UInt32, $false)
  #This would be WMI CIM_DateTime Native if needed
    #$subClass.Properties.Add("DateCreated", [System.Management.CimType]::DateTime, $false)
    #$subClass.Properties.Add("DateLastModified", [System.Management.CimType]::DateTime, $false)
    $WMIOutput = $subClass.Put()
    }

  # Script-level variables used with -Total.
  [UInt64] $script:totalcount = 0
  [UInt64] $script:totalbytes = 0

  # Returns a [System.IO.DirectoryInfo] object if it exists.
  function Get-Directory {
    param( $item )

    if ( $ParamSetName -eq "Path" ) {
      if ((Test-Path -Path $item -PathType Container) -and ($item.Attributes -notmatch [System.IO.FileAttributes]::ReparsePoint)) {
        $item = Get-Item -Path $item -Force
      }
    }
    elseif ( $ParamSetName -eq "LiteralPath" ) {
      if ( (Test-Path -LiteralPath $item -PathType Container) -and ($item.Attributes -notmatch [System.IO.FileAttributes]::ReparsePoint) ) {
        $item = Get-Item -LiteralPath $item -Force
      }
    }
    if ( $item -and ($item -is [System.IO.DirectoryInfo]) -and ($item.Attributes -notmatch [System.IO.FileAttributes]::ReparsePoint)) {
      return $item
    }
  }

  #Do the benchmark calculations
  function GetElapsedTime([datetime]$starttime) 
  {
    $runtime = [Math]::Round(($(get-date) - $starttime).TotalSeconds)

    If($WriteWMI){
        $retStr = $runtime}
    Else{
        $retStr = [string]::format("{0} sec(s)", $runtime)}
    $retStr
  }
  
  # Filter that outputs the custom object with formatted numbers.
  function Format-Output {
    process {
      $_ | Select-Object Path,
        @{Name="Files"; Expression={"{0:N0}" -f $_.Files}},
        @{Name="Size"; Expression={"{0:N0}" -f $_.Size}}
    }
  }

  # Outputs directory statistics for the specified directory. With -recurse,
  # the function includes files in all subdirectories of the specified
  # directory. With -format, numbers in the output objects are formatted with
  # the Format-Output filter.
  function Get-DirectoryStats {
    param( $directory, $recurse, $format )
    
    #Break out of here if we don't care about this folder
    If ($WriteWMI) 
    {
        #Start counting how long it takes to get data
        $enumerationTime = Get-Date
        #Create Exclude array at runtime to speed comparison so we can exclude items
        #http://blogs.technet.com/b/heyscriptingguy/archive/2011/02/18/speed-up-array-comparisons-in-powershell-with-a-runtime-regex.aspx
        $ExcludedFolders = @("Windows"
                            ,"Users"
                            ,"inetpub"
                            ,"Program Files"
                            ,"Program Files (x86)"
                            ,"ProgramData"
                            ,"Windows\System32"
                            ,"System Volume Information"
                            ,"Windows\Installer"
                            ,"Windows\assembly"
                            ,"Windows\System32\DriverStore"
                            ,"Windows\System32\DriverStore\FileRepository"
                            ,"ProgramData\Package Cache
                            ","ProgramData\Microsoft")
        [regex] $excludeFolders_regex = '(?i)^(' + (($ExcludedFolders | foreach {[regex]::escape($directory.Root.ToString() + $_)}) –join "$|") + ')$'    

        If($directory.FullName -match $excludeFolders_regex) {
        #Write-Warning "Skipping: " $directory.FullName
        Return}
        else {
        #Write-Information $directory.FullName " not in " $excludeFolders_regex
        }
    }

    Write-Progress -Activity "Get-DirStats.ps1" -Status "Reading '$($directory.FullName)'"

    #Use FSO if possible
    #https://gallery.technet.microsoft.com/get-foldersize-224916dd
    $folder = $fso.GetFolder($directory.FullName)

    #test if FSO worked, otherwise go to file by file
    if($folder.size -ge $size*1MB -AND $folder.Type -eq "File Folder") {
      $output = "" | Select Path, Files, SizeMB, GatherMethod
      $output.Path = $folder.Path.ToString()
      $output.Files = $folder.Files.Count
      $output.SizeMB = [Math]::Round($folder.Size/1MB)
      $output.GatherMethod = 1
    }
    elseif($folder.size -eq $null) {
      $files = $directory | Get-ChildItem -Force -Recurse:$recurse | 
       ? {$_.Attributes -notmatch [System.IO.FileAttributes]::ReparsePoint} | 
       Where-Object { -not $_.PSIsContainer }
  
      if ( $files ) {
      Write-Progress -Activity "Get-DirStats.ps1" -Status "Calculating '$($directory.FullName)'"
    
    	If ( $SizeFilter ) 
    {
      $output = $files | Measure-Object -Sum -Property Length | 
      Where-Object {$_.Sum -ge $Size*1MB} |  Select-Object `
  	          @{Name="Path"; Expression={$directory.FullName}},
            @{Name="Files"; Expression={$_.Count; $script:totalcount += $_.Count}},
            @{Name="Size"; Expression={$_.Sum; $script:totalbytes += $_.Sum}},
          @{Name="SizeMB"; Expression={[Math]::Round($_.Sum/1MB)}},
              @{Name="GatherMethod"; Expression={2}}
    }
        elseif ( $SizeFilter = $False ) {
          $output = "" | Select-Object `
          @{Name="Path"; Expression={$directory.FullName}},
          @{Name="Files"; Expression={0}},
          @{Name="Size"; Expression={0}}
        }
   	  Else 
    {
      $output = $files | Measure-Object -Sum -Property Length | Select-Object `
  	          @{Name="Path"; Expression={$directory.FullName}},
            @{Name="Files"; Expression={$_.Count; $script:totalcount += $_.Count}},
            @{Name="Size"; Expression={$_.Sum; $script:totalbytes += $_.Sum}},
      @{Name="SizeMB"; Expression={[Math]::Round($_.Sum/1MB)}}
    }
      }
    } #end elseif $folder.size is null manual query

    #Write data to WMI
    if ($WriteWMI -And $output.SizeMB -ge $Size) { 
    $WMIURL = 'root\cimv2:'+$script:Class
    $PushDataToWMI = ([wmiclass]$WMIURL).CreateInstance()
    $PushDataToWMI.Path =  $output.Path
    $PushDataToWMI.FileCount = $output.Files
        $PushDataToWMI.SubFolderCount = $folder.SubFolders.Count
    $PushDataToWMI.SizeMB = $output.SizeMB
        $PushDataToWMI.GatherMethod = $output.GatherMethod
    $PushDataToWMI.DateCreated = $folder.DateCreated
    $PushDataToWMI.DateLastModified = $folder.DateLastModified
        $elapsed = GetElapsedTime $enumerationTime
        $PushDataToWMI.SecondsToEnumerate = $elapsed
    #WMI CIM_DateTime Namtive if needed
        #$PushDataToWMI.DateCreated = [System.Management.ManagementDateTimeConverter]::ToDmtfDateTime($folder.DateCreated)
    #$PushDataToWMI.DateLastModified = [System.Management.ManagementDateTimeConverter]::ToDmtfDateTime($folder.DateLastModified)
    $WmiOutput = $PushDataToWMI.Put()
        #$TotalTime = GetElapsedTime $script:startTime
        #Write-Host $TotalTime ":" $output.Path "with time of" $elapsed "using method" $output.GatherMethod
  }

   if ($WriteWMI=$false){if ( -not $format ) { $output } else { $output | Format-Output }}
  }
}

process {
  # Get the item to process, no matter whether the input comes from the
  # pipeline or not.
  if ( $PipelineInput ) {
    $item = $_
  }
  else {
    if ( $ParamSetName -eq "Path" ) {
      $item = $Path
    }
    elseif ( $ParamSetName -eq "LiteralPath" ) {
      $item = $LiteralPath
    }
  }


  if($WriteWMI){
  foreach($drive in $script:LocalDrives){
    if($Benchmarking -OR $WriteWMI){$iterTime = Get-Date}
    $item = $drive.Root

    # Write an error if the item is not a directory in the file system.
    $directory = Get-Directory -item $item
    if ( -not $directory ) {
      Write-Error -Message "Path '$item' is not a directory in the file system." -Category InvalidType
      return
    }

    # Get the statistics for the first-level directory.
    Get-DirectoryStats -directory $directory -recurse:$false

    # Get the subdirectories of the first-level directory and get the statistics
    # for each of them.
    $directory | Get-ChildItem -Force -Recurse:$true |
      Where-Object { $_.PSIsContainer } | ? {$_.Attributes -notmatch [System.IO.FileAttributes]::ReparsePoint} | 
      ForEach-Object {Get-DirectoryStats -directory $_ -recurse:$true}


    if($Benchmarking -OR $WriteWMI){    
      $elapsed = GetElapsedTime $iterTime ;
        write-warning "   Iteration Time: " $elapsed " for " $item
    }
  }
  }
   else {
    # Write an error if the item is not a directory in the file system.
    $directory = Get-Directory -item $item
    if ( -not $directory ) {
      Write-Error -Message "Path '$item' is not a directory in the file system." -Category InvalidType
      return
    }
  
    # Get the statistics for the first-level directory.
    Get-DirectoryStats -directory $directory -recurse:$false -format:$FormatNumbers
    # -Only means no further processing past the first-level directory.
    if ( $Only ) { return }

    # Get the subdirectories of the first-level directory and get the statistics
    # for each of them.
    $directory | Get-ChildItem -Force -Recurse:$Every |
      Where-Object { $_.PSIsContainer } | ? {$_.Attributes -notmatch [System.IO.FileAttributes]::ReparsePoint} | 
    ForEach-Object {Get-DirectoryStats -directory $_ -recurse:(-not $Every) -format:$FormatNumbers
      }
   }
}

end {
  # If -Total specified, output summary object.
  if ( $Total ) {
    $output = "" | Select-Object `
      @{Name="Path"; Expression={"<Total>"}},
      @{Name="Files"; Expression={$script:totalcount}},
      @{Name="Size"; Expression={$script:totalbytes}}
    if ( -not $FormatNumbers ) { $output } else { $output | Format-Output }
  }
  
  $elapsed = GetElapsedTime $script:startTime;
  if($Benchmarking){
  write-host "Script Ended at $(get-date)"
  write-host "Total Elapsed Time: " $elapsed
  }
  Write-Host $elapsed
}

You can modify any of the variables like size and folders to exclude to meet your needs. I didn’t want to focus on folders I probably could not clean out easily like system folders and the Windows folder will always show over 2GB on machines.

Then setup the configuration baseline to run either on a simple schedule or when it is good for your environment. This is a very processor intensive script that could easily use 50% or more of the CPU for 20+ minutes. I did every saturday here. You could also target a custom collection where machines have hard drive space less than X amount.

Large Folders 5 Large Folders 6 Large Folders 7 Large Folders 8

Then add the WMI entry to hardware inventory. This will need done from a system where this has run already.

Large Folders 10

Then wait for the data and profit

Large Folders 11

 

Inventory Mapped Network Printers and Drives With SCCM

A common issue we have is knowing what drives and printers are actively mapped and being used. There are several ways to get usage data via the file server and print servers, but these are not always adequate and fail to produce a complete picture of the user environment. You can currently return local printers as the system account has rights to that data, but anything mapped under the user account will not be accessible.

What I have done is modify some of the work done at https://social.technet.microsoft.com/Forums/en-US/c08c393d-1ea4-4f6b-8f07-affc0f743193/network-printer-inventory-in-system-centre-configuration-manager-sccm-2012 and extend it to allow for multiple users on a machine to have data reported as well as cleaning out anything that is no longer current. This allows an administrator to see how many users and how many machines are connecting to the network drive and printer resources and also report on users attempting to connect to resources that have been decommissioned. When a machine tries connecting to a resource that is no longer available, this impacts the user’s environment and causes degraded performance.

I am setting this up in a SCCM 2012 R2 SP1 environment. This setup should account for both 32 and 64 systems as well as multiple users on a system, such as a Citrix or terminal server environment. All files needed are attached to this posting here, but I will be putting the code blocks inline in case you want to copy and paste or your environment does not permit file downloads.

I did discover during testing, that since this runs as the user, the powershell script will not run unless signed due to the default permissions of restricted. You could of course change powershell’s execution policy to unrestricted, but this is no good for an enterprise, so we’ll check out Scott and Ed’s posts at http://www.hanselman.com/blog/SigningPowerShellScripts.aspx and http://blogs.technet.com/b/heyscriptingguy/archive/2010/06/16/hey-scripting-guy-how-can-i-sign-windows-powershell-scripts-with-an-enterprise-windows-pki-part-1-of-2.aspx and Part 2 on how to sign code, which is the proper way. I also setup a GPO to enable running local and remote signed code to allow the scripts to execute using the information from http://www.techrepublic.com/blog/the-enterprise-cloud/set-the-powershell-execution-policy-via-group-policy/


First you need to extend the configuration.mof file. We just need to add the following code to your configuration.mof and run the command (modify to match your install location at the primary or central site server) (see file Custom_Printer_Drives_Configuration.mof)

%WINDIR%\System32\WBEM\Mofcomp.exe “C:\Program Files\Microsoft Configuration Manager\inboxes\clifiles.src\hinv\configuration.mof”

Paste the codeblock between these lines. You may have material in there already, just add this after your own custom inventory information.

//========================
// Added extensions start
//========================

//========================
// Added extensions end
//========================

//==== Start custom printer and drive reporting ====

#pragma namespace ("\\\\.\\root\\cimv2")
#pragma deleteclass("MAPPEDDRIVES", NOFAIL)
[dynamic, provider("RegProv"), ClassContext("Local|HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node\\SCCMINVENTORY\\MAPPEDDRIVES")]
Class MAPPEDDRIVES
{
[key] string KeyName;
[PropertyContext("UserDomain")] String UserDomain;
[PropertyContext("UserName")] String UserName;
[PropertyContext("ShareName")] String ShareName;
[PropertyContext("DriveLetter")] String DriveLetter;
[PropertyContext("Size")] Uint32 Size;
[PropertyContext("FreeSpace")] Uint32 FreeSpace;
[PropertyContext("System")] String System;
[PropertyContext("FileSystem")] String FileSystem;
[PropertyContext("DateInventoried")] String DateInventoried;
};

#pragma namespace ("\\\\.\\root\\cimv2")
#pragma deleteclass("MAPPEDDRIVES_64", NOFAIL)
[dynamic, provider("RegProv"), ClassContext("Local|HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node\\SCCMINVENTORY\\MAPPEDDRIVES")]
Class MAPPEDDRIVES_64
{
[key] string KeyName;
[PropertyContext("UserDomain")] String UserDomain;
[PropertyContext("UserName")] String UserName;
[PropertyContext("ShareName")] String ShareName;
[PropertyContext("DriveLetter")] String DriveLetter;
[PropertyContext("Size")] Uint32 Size;
[PropertyContext("FreeSpace")] Uint32 FreeSpace;
[PropertyContext("System")] String System;
[PropertyContext("FileSystem")] String FileSystem;
[PropertyContext("DateInventoried")] String DateInventoried;
};

#pragma namespace ("\\\\.\\root\\cimv2")
#pragma deleteclass("NETWORKPRINTERS", NOFAIL)
[dynamic, provider("RegProv"), ClassContext("Local|HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node\\SCCMINVENTORY\\NETWORKPRINTERS")]
Class NETWORKPRINTERS
{
[key] string KeyName;
[PropertyContext("UserDomain")] String UserDomain;
[PropertyContext("UserName")] String UserName;
[PropertyContext("PrintServer")] String PrintServer;
[PropertyContext("PrinterQueue")] String PrinterQueue;
[PropertyContext("PrinterLocation")] String PrinterLocation;
[PropertyContext("PrinterDriver")] String PrinterDriver;
[PropertyContext("PrintProcessor")] String PrintProcessor;
[PropertyContext("PrinterPortName")] String PrinterPortName;
[PropertyContext("DateInventoried")] String DateInventoried;
};

#pragma namespace ("\\\\.\\root\\cimv2")
#pragma deleteclass("NETWORKPRINTERS_64", NOFAIL)
[dynamic, provider("RegProv"), ClassContext("Local|HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node\\SCCMINVENTORY\\NETWORKPRINTERS")]
Class NETWORKPRINTERS_64
{
[key] string KeyName;
[PropertyContext("UserDomain")] String UserDomain;
[PropertyContext("UserName")] String UserName;
[PropertyContext("PrintServer")] String PrintServer;
[PropertyContext("PrinterQueue")] String PrinterQueue;
[PropertyContext("PrinterLocation")] String PrinterLocation;
[PropertyContext("PrinterDriver")] String PrinterDriver;
[PropertyContext("PrintProcessor")] String PrintProcessor;
[PropertyContext("PrinterPortName")] String PrinterPortName;
[PropertyContext("DateInventoried")] String DateInventoried;
};

//===== End custom printer and drive reporting ======

It should look something like this

edited configuration mof

Recompile_Mof

Then we need to add this to the hardware inventory. This should be as simple as importing the mof file Printer_and_Drive_Inventory_To_Import.mof and making sure everything is checked

#pragma namespace ("\\\\.\\root\\cimv2\\SMS")
#pragma deleteclass("NETWORKPRINTERS", NOFAIL)
[SMS_Report(TRUE),SMS_Group_Name("NETWORKPRINTERS"),SMS_Class_ID("NETWORKPRINTERS"),
SMS_Context_1("__ProviderArchitecture=32|uint32"),
SMS_Context_2("__RequiredArchitecture=true|boolean")]
Class NETWORKPRINTERS: SMS_Class_Template
{
[SMS_Report(TRUE),key] string KeyName;
[SMS_Report(TRUE)] String UserDomain;
[SMS_Report(TRUE)] String UserName;
[SMS_Report(TRUE)] String PrintServer;
[SMS_Report(TRUE)] String PrinterQueue;
[SMS_Report(TRUE)] String PrinterLocation;
[SMS_Report(TRUE)] String PrinterDriver;
[SMS_Report(TRUE)] String PrintProcessor;
[SMS_Report(TRUE)] String PrinterPortName;
[SMS_Report(TRUE)] String DateInventoried;
};

#pragma namespace ("\\\\.\\root\\cimv2\\SMS")
#pragma deleteclass("NETWORKPRINTERS_64", NOFAIL)
[SMS_Report(TRUE),SMS_Group_Name("NETWORKPRINTERS64"),SMS_Class_ID("NETWORKPRINTERS64"),
SMS_Context_1("__ProviderArchitecture=64|uint32"),
SMS_Context_2("__RequiredArchitecture=true|boolean")]
Class NETWORKPRINTERS_64 : SMS_Class_Template
{
[SMS_Report(TRUE),key] string KeyName;
[SMS_Report(TRUE)] String UserDomain;
[SMS_Report(TRUE)] String UserName;
[SMS_Report(TRUE)] String PrintServer;
[SMS_Report(TRUE)] String PrinterQueue;
[SMS_Report(TRUE)] String PrinterLocation;
[SMS_Report(TRUE)] String PrinterDriver;
[SMS_Report(TRUE)] String PrintProcessor;
[SMS_Report(TRUE)] String PrinterPortName;
[SMS_Report(TRUE)] String DateInventoried;
};

#pragma namespace ("\\\\.\\root\\cimv2\\SMS")
#pragma deleteclass("MAPPEDDRIVES", NOFAIL)
[SMS_Report(TRUE),SMS_Group_Name("MAPPEDDRIVES"),SMS_Class_ID("MAPPEDDRIVES"),
SMS_Context_1("__ProviderArchitecture=32|uint32"),
SMS_Context_2("__RequiredArchitecture=true|boolean")]
Class MAPPEDDRIVES: SMS_Class_Template
{
[SMS_Report(TRUE),key] string KeyName;
[SMS_Report(TRUE)] String UserDomain;
[SMS_Report(TRUE)] String UserName;
[SMS_Report(TRUE)] String ShareName;
[SMS_Report(TRUE)] String DriveLetter;
[SMS_Report(TRUE)] Uint32 Size;
[SMS_Report(TRUE)] Uint32 FreeSpace;
[SMS_Report(TRUE)] String System;
[SMS_Report(TRUE)] String FileSystem;
[SMS_Report(TRUE)] String DateInventoried;
};

#pragma namespace ("\\\\.\\root\\cimv2\\SMS")
#pragma deleteclass("MAPPEDDRIVES_64", NOFAIL)
[SMS_Report(TRUE),SMS_Group_Name("MAPPEDDRIVES64"),SMS_Class_ID("MAPPEDDRIVES64"),
SMS_Context_1("__ProviderArchitecture=64|uint32"),
SMS_Context_2("__RequiredArchitecture=true|boolean")]
Class MAPPEDDRIVES_64 : SMS_Class_Template
{
[SMS_Report(TRUE),key] string KeyName;
[SMS_Report(TRUE)] String UserDomain;
[SMS_Report(TRUE)] String UserName;
[SMS_Report(TRUE)] String ShareName;
[SMS_Report(TRUE)] String DriveLetter;
[SMS_Report(TRUE)] Uint32 Size;
[SMS_Report(TRUE)] Uint32 FreeSpace;
[SMS_Report(TRUE)] String System;
[SMS_Report(TRUE)] String FileSystem;
[SMS_Report(TRUE)] String DateInventoried;
};

import_window

Imported_Inventory

Then we need to create a script to generate a registry setting to write the data to and assign permissions that will allow users to write this location (see file: Create_Registry_Inventory.ps1)

if (!(Test-Path HKLM:\SOFTWARE\SCCMINVENTORY)) {new-item HKLM:\SOFTWARE\SCCMINVENTORY  -ErrorAction SilentlyContinue}
$perm = get-acl HKLM:\SOFTWARE\SCCMINVENTORY  -ErrorAction SilentlyContinue
$rule = New-Object System.Security.AccessControl.RegistryAccessRule("Authenticated Users","FullControl", "ContainerInherit, ObjectInherit", "InheritOnly", "Allow")  -ErrorAction SilentlyContinue
$perm.SetAccessRule($rule)
Set-Acl -Path HKLM:\SOFTWARE\SCCMINVENTORY $perm  -ErrorAction SilentlyContinue
if (!(Test-Path HKLM:\SOFTWARE\SCCMINVENTORY\NETWORKPRINTERS)) {new-item HKLM:\SOFTWARE\SCCMINVENTORY\NETWORKPRINTERS -ErrorAction SilentlyContinue}
if (!(Test-Path HKLM:\SOFTWARE\SCCMINVENTORY\MAPPEDDRIVES)) {new-item HKLM:\SOFTWARE\SCCMINVENTORY\MAPPEDDRIVES -ErrorAction SilentlyContinue}

You can setup a package to run this file on all machines in your environment once or limit to a smaller collection.

I am using the command line as follows and all files for this project will be in the same folder as they are small, but you can split them into multiple folders or create multiple programs and advertise each individually.

powershell.exe -ExecutionPolicy Bypass -NoLogo -NonInteractive -NoProfile -WindowStyle Hidden -File .\Create_Registry_Inventory.ps1

step 1 - create registry package

step 2 - create command line

Then setup a program for the user piece using the following command line (see file Printer_Drive_Inventory.ps1):

powershell.exe -ExecutionPolicy Bypass -NoLogo -NonInteractive -NoProfile -WindowStyle Hidden -File .\Printer_Drive_Inventory.ps1

Step 3 - setup user program

Step 4- two programs setup

Then setup the deployment and schedule to run only if previously failed

step 5 - create setup deployment

step 6 - specify collection

step 7 - schedule

step 8 - content options

Then setup the deployment for the user piece. This will run the following code

# https://social.technet.microsoft.com/Forums/en-US/c08c393d-1ea4-4f6b-8f07-affc0f743193/network-printer-inventory-in-system-centre-configuration-manager-sccm-2012?forum=configmanagergeneral#c08c393d-1ea4-4f6b-8f07-affc0f743193
# http://blogs.technet.com/b/breben/archive/2013/08/26/inventory-mapped-drives-in-configmgr-2012.aspx

# run with user rights
# PowerShell.exe -NonInteractive -WindowStyle Hidden -noprofile -ExecutionPolicy Bypass -file .\Printer_Drive_Inventory.ps1

$printers = Get-WMIObject -class Win32_Printer -ErrorAction SilentlyContinue|select-Object -Property ServerName,ShareName,Location,DriverName,PrintProcessor,PortName,Local |Where-Object {$_.Local -ne $true}|Where-Object {$_.ServerName.length -gt 2} -ErrorAction SilentlyContinue
$user = $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name).Replace('\','-')

#Remove previous entries
Get-ChildItem -Path HKLM:\SOFTWARE\SCCMINVENTORY\NETWORKPRINTERS\ -Recurse -Include $user* -ErrorAction SilentlyContinue | Remove-Item

ForEach($printer in $printers){

Try {
    $PServerName= $printer.ServerName -replace ('\\','')
    $PShareName = $printer.ShareName
    $PLocation = $printer.Location
    $PDriverName = $printer.DriverName
    $PPrintProcessor = $printer.PrintProcessor
    $PPortName = $printer.PortName

    if ((Test-Path HKLM:\SOFTWARE\SCCMINVENTORY\NETWORKPRINTERS)) {
        if ((Test-Path "HKLM:\SOFTWARE\SCCMINVENTORY\NETWORKPRINTERS\$user $PShareName on $PServerName")) {
            Remove-item "HKLM:\SOFTWARE\SCCMINVENTORY\NETWORKPRINTERS\$user $PShareName on $PServerName" -Force -ErrorAction SilentlyContinue
        }
        New-item "HKLM:\SOFTWARE\SCCMINVENTORY\NETWORKPRINTERS\$user $PShareName on $PServerName" -ErrorAction SilentlyContinue
        New-ItemProperty "HKLM:\SOFTWARE\SCCMINVENTORY\NETWORKPRINTERS\$user $PShareName on $PServerName" -Name "UserDomain" -Value $user.Split('-')[0] -PropertyType "String" -ErrorAction SilentlyContinue
        New-ItemProperty "HKLM:\SOFTWARE\SCCMINVENTORY\NETWORKPRINTERS\$user $PShareName on $PServerName" -Name "UserName" -Value $user.Split('-')[1] -PropertyType "String" -ErrorAction SilentlyContinue
        New-ItemProperty "HKLM:\SOFTWARE\SCCMINVENTORY\NETWORKPRINTERS\$user $PShareName on $PServerName" -Name "PrintServer" -Value $PServerName -PropertyType "String" -ErrorAction SilentlyContinue
        New-ItemProperty "HKLM:\SOFTWARE\SCCMINVENTORY\NETWORKPRINTERS\$user $PShareName on $PServerName" -Name "PrinterQueue" -Value $PShareName -PropertyType "String" -ErrorAction SilentlyContinue
        New-ItemProperty "HKLM:\SOFTWARE\SCCMINVENTORY\NETWORKPRINTERS\$user $PShareName on $PServerName" -Name "PrinterLocation" -Value $PLocation -PropertyType "String" -ErrorAction SilentlyContinue
        New-ItemProperty "HKLM:\SOFTWARE\SCCMINVENTORY\NETWORKPRINTERS\$user $PShareName on $PServerName" -Name "PrinterDriver" -Value $PDriverName -PropertyType "String" -ErrorAction SilentlyContinue
        New-ItemProperty "HKLM:\SOFTWARE\SCCMINVENTORY\NETWORKPRINTERS\$user $PShareName on $PServerName" -Name "PrintProcessor" -Value $PPrintProcessor -PropertyType "String" -ErrorAction SilentlyContinue
        New-ItemProperty "HKLM:\SOFTWARE\SCCMINVENTORY\NETWORKPRINTERS\$user $PShareName on $PServerName" -Name "PrinterPortName" -Value $PPortName -PropertyType "String" -ErrorAction SilentlyContinue
        New-ItemProperty "HKLM:\SOFTWARE\SCCMINVENTORY\NETWORKPRINTERS\$user $PShareName on $PServerName" -Name "DateInventoried" -Value $(get-date) -PropertyType "String" -ErrorAction SilentlyContinue
    } # End If
    } # End Try
Catch  {}
} #End For Each


#now inventory drives
$drives = Get-WMIObject -class Win32_MappedLogicalDisk -ErrorAction SilentlyContinue|select-Object -Property Caption,Name,FreeSpace,ProviderName,Size,SystemName,FileSystem |Where-Object {$_.Local -ne $true}|Where-Object {$_.ProviderName.length -gt 3} -ErrorAction SilentlyContinue

#Remove previous entries
Get-ChildItem -Path HKLM:\SOFTWARE\SCCMINVENTORY\MAPPEDDRIVES\ -Recurse -Include $user* -ErrorAction SilentlyContinue | Remove-Item

ForEach($drive in $drives){
Try {
    $DShareName = $drive.ProviderName -Replace ('\\','\')
    $DName = $drive.Name
    #convert to GB
    $DSize = $drive.Size/1000000000
    $DFreeSpace = $drive.FreeSpace/1000000000
    $DSystem = $drive.SystemName
    $DFileSystem = $drive.FileSystem

    if ((Test-Path HKLM:\SOFTWARE\SCCMINVENTORY\MAPPEDDRIVES)) {
        if ((Test-Path "HKLM:\SOFTWARE\SCCMINVENTORY\MAPPEDDRIVES\$user $DName")) {
            Remove-item "HKLM:\SOFTWARE\SCCMINVENTORY\MAPPEDDRIVES\$user $DName" -Force -ErrorAction SilentlyContinue
        }
        New-item "HKLM:\SOFTWARE\SCCMINVENTORY\MAPPEDDRIVES\$user $DName" -ErrorAction SilentlyContinue
        New-ItemProperty "HKLM:\SOFTWARE\SCCMINVENTORY\MAPPEDDRIVES\$user $DName" -Name "UserDomain" -Value $user.Split('-')[0] -PropertyType "String" -ErrorAction SilentlyContinue
        New-ItemProperty "HKLM:\SOFTWARE\SCCMINVENTORY\MAPPEDDRIVES\$user $DName" -Name "UserName" -Value $user.Split('-')[1] -PropertyType "String" -ErrorAction SilentlyContinue
        New-ItemProperty "HKLM:\SOFTWARE\SCCMINVENTORY\MAPPEDDRIVES\$user $DName" -Name "ShareName" -Value $DShareName -PropertyType "String" -ErrorAction SilentlyContinue
        New-ItemProperty "HKLM:\SOFTWARE\SCCMINVENTORY\MAPPEDDRIVES\$user $DName" -Name "DriveLetter" -Value $DName -PropertyType "String" -ErrorAction SilentlyContinue
        New-ItemProperty "HKLM:\SOFTWARE\SCCMINVENTORY\MAPPEDDRIVES\$user $DName" -Name "Size" -Value $DSize -PropertyType "DWord" -ErrorAction SilentlyContinue
        New-ItemProperty "HKLM:\SOFTWARE\SCCMINVENTORY\MAPPEDDRIVES\$user $DName" -Name "FreeSpace" -Value $DFreeSpace -PropertyType "DWord" -ErrorAction SilentlyContinue
        New-ItemProperty "HKLM:\SOFTWARE\SCCMINVENTORY\MAPPEDDRIVES\$user $DName" -Name "System" -Value $DSystem -PropertyType "String" -ErrorAction SilentlyContinue
        New-ItemProperty "HKLM:\SOFTWARE\SCCMINVENTORY\MAPPEDDRIVES\$user $DName" -Name "FileSystem" -Value $DFileSystem -PropertyType "String" -ErrorAction SilentlyContinue
        New-ItemProperty "HKLM:\SOFTWARE\SCCMINVENTORY\MAPPEDDRIVES\$user $DName" -Name "DateInventoried" -Value $(get-date) -PropertyType "String" -ErrorAction SilentlyContinue
    } # End If
    } # End Try
Catch {}
} #End For Each

step 9 - deploy user piece

Assign a collection

step 10 - assign to collection

Setting the schedule to run every 4 hours should capture most use cases. You can accomplish this with a login script to call this file also

custom schedule 2

custom schedule

Also go back in and make sure to set it to run once for each user that is logged in and that the Run setting under the General tab is set to Hidden

run once for every user

So now we have 2 deployments

2 deployments

Then, provided everything is working, you should start to see data come in. Remember this is setup to run every 4 hours, but we’re patient and looking for environmental data for a large group of people.

You can then query the data in SQL and write SSRS reports to show this data

Mapped drives can be uncovered here

SELECT [MachineID]
      ,[InstanceKey]
      ,[TimeKey]
      ,[RevisionID]
      ,[AgentID]
      ,[rowversion]
      ,[DateInventoried00]
      ,[DriveLetter00]
      ,[FileSystem00]
      ,[FreeSpace00]
      ,[KeyName00]
      ,[ShareName00]
      ,[Size00]
      ,[System00]
      ,[UserDomain00]
      ,[UserName00]
  FROM [MAPPEDDRIVES_DATA]

  Union

  SELECT [MachineID]
      ,[InstanceKey]
      ,[TimeKey]
      ,[RevisionID]
      ,[AgentID]
      ,[rowversion]
      ,[DateInventoried00]
      ,[DriveLetter00]
      ,[FileSystem00]
      ,[FreeSpace00]
      ,[KeyName00]
      ,[ShareName00]
      ,[Size00]
      ,[System00]
      ,[UserDomain00]
      ,[UserName00]
  FROM [MAPPEDDRIVES64_DATA]

reported_data

And printers can be found here

SELECT [MachineID]
      ,[InstanceKey]
      ,[TimeKey]
      ,[RevisionID]
      ,[AgentID]
      ,[rowversion]
      ,[DateInventoried00]
      ,[KeyName00]
      ,[PrinterDriver00]
      ,[PrinterLocation00]
      ,[PrinterPortName00]
      ,[PrinterQueue00]
      ,[PrintProcessor00]
      ,[PrintServer00]
      ,[UserDomain00]
      ,[UserName00]
  FROM [NETWORKPRINTERS_DATA]

  Union

  SELECT [MachineID]
      ,[InstanceKey]
      ,[TimeKey]
      ,[RevisionID]
      ,[AgentID]
      ,[rowversion]
      ,[DateInventoried00]
      ,[KeyName00]
      ,[PrinterDriver00]
      ,[PrinterLocation00]
      ,[PrinterPortName00]
      ,[PrinterQueue00]
      ,[PrintProcessor00]
      ,[PrintServer00]
      ,[UserDomain00]
      ,[UserName00]
  FROM [NETWORKPRINTERS64_DATA]

reported_data_printers

Or the even easier queries of the following which just throws everything on the screen

SELECT * FROM [MAPPEDDRIVES_DATA]
SELECT * FROM [MAPPEDDRIVES64_DATA]
Select * From [NETWORKPRINTERS_DATA]
Select * From [NETWORKPRINTERS64_DATA]

multiple sql

This is obviously a small environment. Use at your own caution and test before deployment into production. I do need to add in some filtering so it does not populate some obviously bad data. There are a lot of moving parts here, so I would expect issues, but I hope this helps you get started.

Let me know if you have any questions or suggestions. Please email eric@holzhueter.us


Files if you missed the link in the document: http://plainlytechnical.com/files/Printer_And_Drive_Inventory.zip


Edited script text for Printer_Drive_Inventory.ps1 on 5/19/15 to a filter share and server information that is blank as this does no help in reporting. There is also a reg key to increase the MIF file size to 50MB if needed.

Edited script test for Printer_Drive_Inventory.ps1 on 5/21 to add a try/catch into the logic. This will require Windows 7 and Server 2008 as minimum requirements with this change. Those lines can be removed for older environments.