UMN-SCCM.psm1

###
# Copyright 2017 University of Minnesota, Office of Information Technology

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with Foobar. If not, see <http://www.gnu.org/licenses/>.
###
#

function Get-SccmCollectionByComputer{
<#
    .Synopsis
        Get list of Collections a specific computer belongs to

    .DESCRIPTION
        Get list of Collections a specific computer belongs to

    .PARAMETER computer
        Name of computer to get Collections for

    .PARAMETER siteserver
        FQDN of the site server
            
    .PARAMETER sitecode
        SCCM Site Code

    .EXAMPLE
        get-ClientMaintWindow -computer 'test-machine' -sitecode 'sccmsite'
#>


    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory)]
        [string]$computer,

        [Parameter(Mandatory)]
        [string]$siteserver,

        [Parameter(Mandatory)]
        [string]$sitecode

    )

    Begin{}
    Process{
        (Get-WmiObject -ComputerName $siteserver  -Namespace "root/SMS/site_$sitecode" -Query "SELECT SMS_Collection.* FROM SMS_FullCollectionMembership, SMS_Collection where name = '$computer' and SMS_FullCollectionMembership.CollectionID = SMS_Collection.CollectionID").Name
    }
    End{}
}

function Get-ClientMaintWindow{
<#
    .Synopsis
        Requires SCCM PowerShell cmdlet to convert WMI class to readable schedule
    .DESCRIPTION
        Requires SCCM PowerShell cmdlet to convert WMI class to readable schedule

    .PARAMETER computer
        Name of computer object to be added

    .PARAMETER sitecode
        SCCM Site Code

    .EXAMPLE
        get-ClientMaintWindow -computer 'test-machine' -sitecode 'sccmsite'
#>


    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory)]
        [string]$computer,

        [Parameter(Mandatory)]
        [string]$sitecode

    )
 
    Begin
    {
        $Future = @()
        $location = $sitecode+":"
        Import-Module "C:\Program Files (x86)\Microsoft Configuration Manager\AdminConsole\bin\ConfigurationManager.psd1" -Force
        Push-Location
        set-location $location # see wiki about creating the PS-Drive in the first place

    }   

    Process
    {
        Get-WmiObject -query "SELECT * FROM CCM_ServiceWindow " -namespace "root\CCM\Policy\Machine\ActualConfig" -ComputerName $computer
        If ($wmiClass -eq $Null){throw "$VM WMI call failed"}

        Push-Location
        set-location $location # see wiki about creating the PS-Drive in the first place

        $wmiClass.schedules | % {
            $MaintWindow = Convert-CMSchedule -ScheduleString $_
            If ($MaintWindow.Starttime -ge (get-date)){$Future += $MaintWindow}
        }
    } 
       
    End
    {
    
        If ($Future -eq $Null) {Pop-Location;throw "$vm has no future mainteanance windows"}
        Else {$Future |Select-Object -Property HourDuration,StartTime}
        Pop-Location
    }
}

function Get-CMDeploymentTypePath {
    <#
        .SYNOPSIS
            Gets the paths for all deployment types on an application.
        .DESCRIPTION
            Takes in a IResultObject#SMS_Application object and then returns the paths for each deployment type.
        .EXAMPLE
            Get-CMApplication -Name "MyApp" | Get-CMDeploymentTypePath
        .PARAMETER CMApplication
            This is the application returned from the Get-CMApplication cmdlet.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true, Position=1)]
        [PSObject]$CMApplication
    )

    begin {
        $ReturnObject = New-Object System.Collections.ArrayList
    }

    process {
        $ApplicationName = $CMApplication.LocalizedDisplayName
        Write-Verbose -Message "Application: $ApplicationName"

        $AppXml = [xml]$CMApplication.SDMPackageXML
        $DeploymentTypes = $AppXml.AppMgmtDigest.DeploymentType
        
        foreach($DeploymentType in $DeploymentTypes) {
            $ReturnType = New-Object PSObject

            $DeploymentTypeName = $DeploymentType.Title.'#text'
            $DeploymentTypeLocation = $DeploymentType.Installer.Contents.Content.Location
            Write-Verbose -Message "Content Location: $DeploymentTypeLocation"

            $ReturnType | Add-Member -MemberType NoteProperty -Name "ApplicationName" -Value $ApplicationName
            $ReturnType | Add-Member -MemberType NoteProperty -Name "DeploymentType" -Value $DeploymentTypeName
            $ReturnType | Add-Member -MemberType NoteProperty -Name "DeploymentTypeLocation" -Value $DeploymentTypeLocation
            $null = $ReturnObject.Add($ReturnType)
        }
    }

    end {
        return $ReturnObject
    }
}

function Get-PendingReboot {
    <#
        .SYNOPSIS
            Simply returns what possible pending reboots there are on the current machine.

        .DESCRIPTION
            Takes in no values and returns a hashtable of what reboots are pending and which are not.

        .NOTES
            Name: Get-PendingReboot
            Author: Jeff Bolduan
            LASTEDIT: 09/01/2016

            Based on xPendingReboot DSC functions
            https://github.com/PowerShell/xPendingReboot
        .EXAMPLE

    #>

    [CmdletBinding()]
    [OutputType([Hashtable])]
    param(
        
    )

    # Check the Componenet Based Servicing registry location for a pending reboot
    $ComponentBasedServicingKeys = (Get-ChildItem "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing").Name
    if($ComponentBasedServicingKeys) {
        $ComponentBasedServicing = $ComponentBasedServicingKeys.Split("\") -contains "RebootPending"
    } else {
        $ComponentBasedServicing = $false
    }

    $WindowsUpdateKeys = (Get-ChildItem "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update").Name
    if($WindowsUpdateKeys) {
        $WindowsUpdate = $WindowsUpdateKeys.Split("\") -contains "RebootRequired"
    } else {
        $WindowsUpdate = $false
    }

    $PendingFileRename = (Get-ItemProperty -LiteralPath "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager").PendingFileRenameOperations.Length -gt 0
    $ActiveComputerName = (Get-ItemProperty -LiteralPath "HKLM:\SYSTEM\CurrentControlSet\Control\ComputerName\ActiveComputerName").ComputerName
    $PendingComputerName = (Get-ItemProperty -LiteralPath "HKLM:\SYSTEM\CurrentControlSet\Control\ComputerName\ComputerName").ComputerName
    $PendingComputerRename = $ActiveComputerName -ne $PendingComputerName

    try {
        $CCMClientSDK = Invoke-WmiMethod -Namespace "root\ccm\ClientSDK" -Class "CCM_ClientUtilities" -Name "DetermineIfRebootPending" -ErrorAction Stop
    } catch {
        Write-Warning -Message "Unable to query CCM_ClientUtilities: $_"
    }

    $SCCMSDK = ($CCMClientSDK.ReturnVal -eq 0) -and ($CCMClientSDK.IsHardRebootPending -or $CCMClientSDK.RebootPending)

    return @{
        ComponenetBasedServicing = $ComponentBasedServicing
        WindowsUpdate = $WindowsUpdate
        PendingFileRename = $PendingFileRename
        PendingComputerRename = $PendingComputerRename
        CCMClientSDK = $SCCMSDK
    }
}

function Import-ComputerObjectSCCM
{
    <#
        .Synopsis
            Add computer object to sccm, add to a collection WMI only
        .DESCRIPTION
            Add computer object to sccm, add to a collection and waits to return until the device is in the collection
            Uses direct add and accepts smsbiosGuid or Mac Address.
    
        .PARAMETER computer
            Name of computer object to be added

        .PARAMETER siteserver
            FQDN of the site server

        .PARAMETER smBiosGuid
            If the computer is a vm from VMWARE use Get-SMBiosGuidVmware from our module UMN-VMWare to get the UUID and convert it to smsbiosguild

        .PARAMETER macAddress
            If you don't have the smasbiosguild, you can use the mac address
        
        .PARAMETER CollectionName
            Device Collection to add the computer object to.

        .PARAMETER sitecode
            SCCM Site Code

        .EXAMPLE
            Import-ComputerObjectSCCM -computer $computer -smBiosGUID $smBiosGUID -siteserver "siteserver.mysite.org" - sitecode "Site"
    #>


    [CmdletBinding()]
    Param
    (
        [ValidateNotNullOrEmpty()]
        [string]$computer,

        [ValidateNotNullOrEmpty()]
        [string] $siteserver,

        [Parameter(ParameterSetName='smbios')]
        [string]$smBiosGUID,

        [Parameter(ParameterSetName='mac')][ValidatePattern("([a-zA-Z0-9]{2}:){5}[a-zA-Z0-9]{2}")]
        [string]$macAddress,

        [ValidateNotNullOrEmpty()]
        [string]$CollectionName,

        [ValidateNotNullOrEmpty()]
        [string]$siteCode

    )

    Begin
    {
        $namespace = "root\sms\site_$siteCode"
    }
    Process
    {
        # validate Collection name
        if ((Get-WmiObject -Query "SELECT * FROM SMS_Collection WHERE CollectionType = 2 AND Name='$CollectionName'" -ComputerName $siteserver -Namespace $namespace) -eq $null){throw "Error -- unable to find collection"}
        ## validate device doesn't already exist, we don't want to over-write
        if ((Get-WmiObject -Query "SELECT * FROM SMS_R_System WHERE Name = '$computer'" -ComputerName $siteserver -Namespace $namespace) -ne $null){throw "machine already exits in sccm"}

        $CollectionQuery = Get-WmiObject -Namespace $namespace -Class 'SMS_Collection' -Filter "Name='$CollectionName'" -ComputerName $siteserver

        # New computer account information
        $WMIConnection = ([WMIClass]"\\$siteserver\$namespace`:SMS_Site")
        $NewEntry = $WMIConnection.psbase.GetMethodParameters("ImportMachineEntry")
        if ($smBiosGUID){$NewEntry.SMBIOSGUID = $smBiosGUID}
        else{$NewEntry.MACAddress = $macAddress}            
        $NewEntry.NetbiosName = $computer
        $NewEntry.OverwriteExistingRecord = $True
        $Resource = $WMIConnection.psbase.InvokeMethod("ImportMachineEntry",$NewEntry,$null)

        #Create the Direct MemberShip Rule
        $NewRule = ([WMIClass]"\\$siteserver\$namespace`:SMS_CollectionRuleDirect").CreateInstance()
        $NewRule.ResourceClassName = "SMS_R_SYSTEM"
        $NewRule.ResourceID = $Resource.ResourceID
        $NewRule.Rulename = $computer

        #Add the newly created machine to collection
        $null = $CollectionQuery.AddMemberShipRule($NewRule)

        return $Resource
        
    }
    End
    {
    }
}

function New-ComputerObjectSCCM
{
    <#
        .Synopsis
            Add computer object to sccm, add to a collection
        .DESCRIPTION
            Add computer object to sccm, add to a collection and waits to return until the device is in the collection
            Uses direct add and accepts smsbiosGuid or Mac Address.
    
        .PARAMETER computer
            Name of computer object to be added

        .PARAMETER siteserver
            FQDN of the site server

        .PARAMETER smBiosGuid
            If the computer is a vm from VMWARE use Get-SMBiosGuidVmware from our module UMN-VMWare to get the UUID and convert it to smsbiosguild

        .PARAMETER macAddress
            If you don't have the smasbiosguild, you can use the mac address
        
        .PARAMETER CollectionName
            Device Collection to add the computer object to.

        .PARAMETER sitecode
            SCCM Site Code

        .EXAMPLE
            New-ComputerObjectSCCM -computer $computer -smBiosGUID $smBiosGUID -siteserver "siteserver.mysite.org" - sitecode "Site"
    #>


    [CmdletBinding()]
    Param
    (
        [ValidateNotNullOrEmpty()]
        [string]$computer,

        [ValidateNotNullOrEmpty()]
        [string] $siteserver,

        [Parameter(ParameterSetName='smbios')]
        [string]$smBiosGUID,

        [Parameter(ParameterSetName='mac')][ValidatePattern("([a-zA-Z0-9]{2}:){5}[a-zA-Z0-9]{2}")]
        [string]$macAddress,

        [ValidateNotNullOrEmpty()]
        [string]$CollectionName,

        [ValidateNotNullOrEmpty()]
        [string] $sitecode

    )

    Begin
    {
        $namespace = "root\sms\site_$siteCode"
    }
    Process
    {
        if ($smBiosGUID){$null = Import-ComputerObjectSCCM -computer $computer -siteserver $siteserver -CollectionName $CollectionName -siteCode $siteCode -smBiosGUID $smBiosGUID}
        else{$null = Import-ComputerObjectSCCM -computer $computer -siteserver $siteserver -CollectionName $CollectionName -siteCode $siteCode -MacAddress $macAddress}
        "Waiting for object to show up in collection"
        # Right now the lag on colleciton refresh is around 5 minutes, loop to wait until the computer object finally shows up in the collection
        # hour cap, after that .. error
        $count = 0
        do {
            start-sleep 60
            $count++
            $device = Get-WmiObject -Query "SELECT * FROM SMS_FullCollectionMembership WHERE CollectionID='SMS00001' AND name='$computer'" -ComputerName $siteserver -Namespace $namespace
            "check $count"
        } while ($device -eq $null -and $count -lt 60)
        if ($device -eq $null){Throw "$computer never added to All Systmes"}
        "Found in All Systems, moving to $CollectionName"

        # Force collection updates.
        $CollectionQueryAllS = Get-WmiObject -Namespace "Root\SMS\Site_$sitecode" -Class SMS_Collection -Filter "Name='$CollectionName'" -computername $siteserver
        $null = $CollectionQueryAllS.RequestRefresh()
        $colID = $CollectionQueryAllS.CollectionID
        $count = 0
        do {
            start-sleep 60
            $count++
            #if (($count % 5) -eq 0){"Refreshing $CollectionName";$null = $CollectionQueryAllS.RequestRefresh()}
            $device = Get-WmiObject -Query "SELECT * FROM SMS_FullCollectionMembership WHERE CollectionID='$colID' AND name='$computer'" -ComputerName $siteserver -Namespace $namespace
            "check $count"
        } while ($device -eq $null -and $count -lt 60)
        if ($device -eq $null){Throw "$computer never added to $CollectionName"}
        
    }
    End
    {
    }
}

function New-ComputerVariablesSCCM
{
    <#
        .Synopsis
            Add variables to a computer object in SCCM
        .DESCRIPTION
            Add variables to a computer object in SCCM
    
        .PARAMETER computer
            Name of computer object to be added

        .PARAMETER deviceVariables
            Hastable of variable to add. Key = variable name, Value = Value of the variable
        
        .PARAMETER CollectionName
            Device Collection to search for computer object, defaults to "All Systems"

        .PARAMETER sitecode
            SCCM Site Code

        .EXAMPLE
            Example of how to use this cmdlet
    #>


    [CmdletBinding()]
    Param
    (
        [ValidateNotNullOrEmpty()]
        [string]$computer,

        [ValidateNotNullOrEmpty()]
        [System.Collections.Hashtable]$deviceVariables,
        
        [ValidateNotNullOrEmpty()]
        [string]$siteserver,

        [ValidateNotNullOrEmpty()]
        [string]$sitecode,

        [string]$smBiosGUID

    )

    Begin
    {
        $namespace = "root\sms\site_$siteCode"
    }
    Process
    {
        try
        {
            $machineSettings = [WMIClass]"\\$siteserver\$namespace`:SMS_MachineSettings"
            $ResourceID = (Get-WmiObject -ComputerName $siteserver -Namespace $namespace -Query "SELECT * FROM SMS_R_System where Name='$computer'").ResourceID
            if ($ResourceID.Count -ne 1){Throw "SCCM returned more/less that one record for $computer : $($ResourceID.Count) records" }
            $object =  $machineSettings.CreateInstance()
            $object.psbase.properties["ResourceID"].value = $ResourceID[0]
            $object.psbase.properties["SourceSite"].value = $SiteCode

            ForEach ($key in $deviceVariables.Keys){
                $object.MachineVariables = $object.MachineVariables + [WMIClass]"\\$siteserver\$namespace`:SMS_MachineVariable"
            }
            $machineVariables =  $object.MachineVariables
            $count = 0
            ForEach ($key in $deviceVariables.Keys){
                $machineVariables[$count].name = $key
                $machineVariables[$count].value = $deviceVariables[$key]
                $count++
            }


            $object.MachineVariables = $machineVariables

            $object.put()
        }catch{Throw ($_.Exception.Message + $_.InvocationInfo.Line + $_.InvocationInfo.PositionMessage)}
    }
    End
    {

    }
}



function Remove-sccmDevice
{
    <#
        .Synopsis
            Remove computer object from SCCM
        .DESCRIPTION
            Remove computer object from SCCM
        .EXAMPLE
            Example of how to use this cmdlet
        .EXAMPLE
            Another example of how to use this cmdlet
    #>


    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory)]
        [string]$computer,

        [Parameter(Mandatory)]
        [string]$sitecode

    )

    Begin
    {
        $location = $sitecode+":"
        Push-Location
        Import-Module "C:\Program Files (x86)\Microsoft Configuration Manager\AdminConsole\bin\ConfigurationManager.psd1" -Force
        set-location $location # see wiki about creating the PS-Drive in the first place
    }
    Process
    {
        try{Remove-CMDevice -DeviceName $computer -Force}
        catch{Pop-Location;throw ($_.Exception.Message + $_.InvocationInfo.Line + $_.InvocationInfo.PositionMessage)}
    }
    End
    {
        Pop-Location
    }
}

function Remove-sccmDeviceFromCollection
{
    <#
        .Synopsis
            Remove Computer Object from specific device collection
        .DESCRIPTION
            Remove Computer Object from specific device collection
        .EXAMPLE
            Example of how to use this cmdlet
        .EXAMPLE
            Another example of how to use this cmdlet
    #>


    [CmdletBinding()]
    Param
    (
        [ValidateNotNullOrEmpty()]
        [string]$computer,

        [ValidateNotNullOrEmpty()]
        [string]$sitecode,

        [ValidateNotNullOrEmpty()]
        [string]$siteserver,

        [ValidateNotNullOrEmpty()]
        [string]$colName
    )

    Begin
    {
        $location = $sitecode+":"
        Push-Location
        Import-Module "C:\Program Files (x86)\Microsoft Configuration Manager\AdminConsole\bin\ConfigurationManager.psd1" -Force
        set-location $location # see wiki about creating the PS-Drive in the first place
    }
    Process
    {
        ## sccm is case sensative but -mathc is not so ... yeah. Fetch list of vms, match and remove with valid case
        $found = $false
        ## Fetch list of vms
        $rules = Get-CMDeviceCollectionDirectMembershipRule -CollectionName $colName
        foreach ($rule in $rules)
        {
            if($rule.RuleName -match $computer)
            {
                $found = $true
                Remove-CMDeviceCollectionDirectMembershipRule -CollectionName $colName -ResourceName $rule.RuleName -Confirm:$false -Force
                Start-Sleep -Seconds 5
                $CollectionQueryAllS = Get-WmiObject -Namespace "Root\SMS\Site_$sitecode" -Class SMS_Collection -Filter "Name='$colName'" -computername $siteserver
                $null = $CollectionQueryAllS.RequestRefresh()
                break
            }
        }
        if (!($found)){Pop-Location;return (throw "Failed to find $computer in $colName" )}
        else
        {
            ## neither success nor failure seems to return anything, so have to rerun and make sure no match.
            $rules = Get-CMDeviceCollectionDirectMembershipRule -CollectionName $colName
            foreach ($rule in $rules)
            {
                if($rule.RuleName -match $computer){Pop-Location;return (throw "$computer NOT removed from $colName")}
            }
            $true
        }
        
    }
    End
    {
        Pop-Location
    }
}

Export-ModuleMember -Function *