Skip to content

Instantly share code, notes, and snippets.

@Calvindd2f
Last active September 24, 2024 22:31
Show Gist options
  • Select an option

  • Save Calvindd2f/e38f81e4b1b1a1e955939083d9409074 to your computer and use it in GitHub Desktop.

Select an option

Save Calvindd2f/e38f81e4b1b1a1e955939083d9409074 to your computer and use it in GitHub Desktop.
# Function to calculate the annual costs of the licenses assigned to a user account
Function Get-LicenseCosts {
[CmdletBinding()]
param (
[array]$Licenses
)
[int]$Costs = 0
foreach ($License in $Licenses) {
$LicenseCost = $PricingHashTable[$License]
if ([float]$LicenseCost) {
$AnnualCost = [float]$LicenseCost * 100 * 12 # Convert to cents and calculate annual
$Costs += $AnnualCost
}
}
return ($Costs / 100) # Return annual cost in dollars
}
# Function to import CSV data and populate HashTables
Function Import-LicenseData {
param (
[string]$SkuCsvPath,
[string]$ServicePlanCsvPath
)
if (!(Test-Path $SkuCsvPath) -or !(Test-Path $ServicePlanCsvPath)) {
Write-Error "License data files are missing. Exiting..."
return $false
}
$ImportSkus = Import-Csv $SkuCsvPath
$ImportServicePlans = Import-Csv $ServicePlanCsvPath
$SkuHashTable = @{}
$ServicePlanHashTable = @{}
foreach ($Line in $ImportSkus) { $SkuHashTable.Add([string]$Line.SkuId, [string]$Line.DisplayName) }
foreach ($Line in $ImportServicePlans) { $ServicePlanHashTable.Add([string]$Line.ServicePlanId, [string]$Line.ServicePlanDisplayName) }
$PricingInfoAvailable = $false
if ($ImportSkus[0].Price) {
$PricingInfoAvailable = $true
$Global:PricingHashTable = @{}
foreach ($Line in $ImportSkus) {
$PricingHashTable.Add([string]$Line.SkuId, [string]$Line.Price)
}
$Currency = if ($ImportSkus[0].Currency) { $ImportSkus[0].Currency } else { "USD" }
}
return @{
SkuHashTable = $SkuHashTable;
ServicePlanHashTable = $ServicePlanHashTable;
PricingInfoAvailable = $PricingInfoAvailable;
Currency = $Currency
}
}
# Function to process user data and generate the report
Function Process-UserLicenses {
param (
[array]$Users,
[hashtable]$SkuHashTable,
[hashtable]$ServicePlanHashTable,
[bool]$PricingInfoAvailable,
[string]$Currency
)
$TotalUserLicenseCosts = 0
$Report = [System.Collections.Generic.List[Object]]::new()
foreach ($User in $Users) {
$LicenseInfo = @()
$DisabledPlans = @()
$UserCosts = 0
$GroupAssignments = $User.LicenseAssignmentStates | Where-Object { $_.AssignedByGroup -and $_.State -eq "Active" }
$GroupLicensing = $null
# Group-based licensing assignments
foreach ($G in $GroupAssignments) {
$GroupName = (Get-MgGroup -GroupId $G.AssignedByGroup).DisplayName
$GroupProductName = $SkuHashTable[$G.SkuId]
$GroupLicensing += ("{0} assigned from {1}" -f $GroupProductName, $GroupName)
}
$GroupLicensingAssignments = $GroupLicensing -join ", "
# Direct License Assignments
$DirectAssignments = $User.AssignedLicenses.SkuId | Where-Object { $_ -notin $GroupAssignments.SkuId }
foreach ($License in $DirectAssignments) {
if ($SkuHashTable.ContainsKey($License)) {
$LicenseInfo += $SkuHashTable[$License]
} else {
$LicenseInfo += $License
}
}
# Disabled Plans Reporting
$License = $User.AssignedLicenses | Where-Object { $_.SkuId -eq $License }
if ($License.DisabledPlans) {
foreach ($DisabledPlan in $License.DisabledPlans) {
if ($ServicePlanHashTable.ContainsKey($DisabledPlan)) {
$DisabledPlans += $ServicePlanHashTable[$DisabledPlan]
} else {
$DisabledPlans += $DisabledPlan
}
}
}
# License Cost Calculation
if ($PricingInfoAvailable) {
$UserCosts = Get-LicenseCosts -Licenses $User.AssignedLicenses.SkuId
$TotalUserLicenseCosts += $UserCosts
}
# Generate Report Line
$ReportLine = [PSCustomObject][Ordered]@{
User = $User.DisplayName
UPN = $User.UserPrincipalName
Country = $User.Country
Department = $User.Department
Title = $User.JobTitle
"Direct assigned licenses" = $LicenseInfo -join ", "
"Disabled Plans" = $DisabledPlans -join ", "
"Group based licenses" = $GroupLicensingAssignments
"Annual License Costs" = if ($PricingInfoAvailable) { ("{0} {1}" -f $Currency, ($UserCosts.ToString('F2'))) } else { "N/A" }
"Account created" = $User.CreatedDateTime
"Last Signin" = $User.SignInActivity.LastSignInDateTime
}
$Report.Add($ReportLine)
}
return @{
Report = $Report;
TotalUserLicenseCosts = $TotalUserLicenseCosts
}
}
# Main Execution
[string]$RunDate = Get-Date -format "dd-MMM-yyyy HH:mm:ss"
[string]$Currency = "USD"
$LicenseData = Import-LicenseData -SkuCsvPath "c:\temp\SkuDataComplete.csv" -ServicePlanCsvPath "c:\temp\ServicePlanDataComplete.csv"
if (!$LicenseData) {
Write-Error "Failed to load license data."
return
}
$Users = Get-MgUser -Filter "assignedLicenses/`$count ne 0 and userType eq 'Member'" -All -Property id, displayName, userPrincipalName, country, department, assignedlicenses, LicenseAssignmentStates, createdDateTime, jobTitle, signInActivity
if (!$Users) {
Write-Error "No licensed user accounts found."
return
}
$ReportData = Process-UserLicenses -Users $Users -SkuHashTable $LicenseData.SkuHashTable -ServicePlanHashTable $LicenseData.ServicePlanHashTable -PricingInfoAvailable $LicenseData.PricingInfoAvailable -Currency $LicenseData.Currency
$Report = $ReportData.Report
# Generate CSV and HTML reports
$CSVOutputFile = "c:\temp\Microsoft365LicensesReport.CSV"
$ReportFile = "c:\temp\Microsoft365LicensesReport.html"
$Report | Export-Csv -Path $CSVOutputFile -NoTypeInformation
Write-Host "Report saved to $CSVOutputFile"
Disconnect-MgGraph
@Calvindd2f
Copy link
Author

Dependencies

# Read-only section for variables and dependencies
#read_only
####################################################
########## INPUT
#####################################################
$DownloadPath = 'C:\temp'
$CSVFileUrl = 'https://download.microsoft.com/download/e/3/e/e3e9faf2-f28b-490a-9ada-c6089a1fc5b0/Product%20names%20and%20service%20plan%20identifiers%20for%20licensing.csv'
$ScriptUrl = 'https://gist.githubusercontent.com/Calvindd2f/e38f81e4b1b1a1e955939083d9409074/raw/d68d0cf62420d7ea8557e6a32bc86a814bbdfdd0/licenseReports.psm1'
$DependencyPaths = @(
    @{url = $CSVFileUrl; path = "$DownloadPath\LicenseData.csv" },
    @{url = $ScriptUrl; path = "$DownloadPath\ReportUserAssignedLicenses.ps1" }
)
####################################################
########## OUTPUT
#####################################################
$variableProps = @{}
$outputProps = @{output = $(New-Object psobject -Property $variableProps); success = $false; debug = $null; error = $null; }
$activityOutput = New-Object psobject -Property $outputProps;
#/read_only

# Function to verify download path and create if not exists
function Verify
{
    param (
        [string]$Path
    )
    try
    {
        if (!(Test-Path -Path $Path))
        {
            Write-Host "Creating directory $Path..."
            New-Item -ItemType Directory -Path $Path -Force > $null;
        }
        Write-Host "Directory exists`: $Path"
    }
    catch
    {
        Write-Error "Failed to create or access directory $Path`: $_"
        throw $_
    }
}

# Function to download files asynchronously
function DownloadFiles
{
    param (
        [array]$Dependencies
    )
    $webClient = New-Object System.Net.WebClient
    $jobs = @()

    foreach ($dependency in $Dependencies)
    {
        try
        {
            $job = Start-Job -ScriptBlock {
                param ($url, $path)
                (New-Object System.Net.WebClient).DownloadFile($url, $path)
            } -ArgumentList $dependency.url, $dependency.path
            $jobs += $job
        }
        catch
        {
            Write-Error "Failed to start download for $($dependency.url)`: $_"
        }
    }

    Write-Host 'Waiting for all downloads to complete...'
    $jobs | Wait-Job
    $completed = $jobs | Receive-Job
    return $completed
}

# Function to run the scripts after they are Download
function Download
{
    param (
        [string]$ScriptPath
    )
    if (Test-Path -Path $ScriptPath)
    {
        try
        {
            Write-Host "Running script $ScriptPath..."
            & $ScriptPath
        }
        catch
        {
            Write-Error "Error running script $ScriptPath`: $_"
            throw $_
        }
    }
    else
    {
        Write-Error "Script not found`: $ScriptPath"
        throw "Script not found: $ScriptPath"
    }
}

# Main function to execute the entire process
function Execute
{
    try
    {
        # Step 1: Verify path
        Verify -Path $DownloadPath

        # Step 2: Download dependencies
        $downloadResult = DownloadFiles -Dependencies $DependencyPaths

        if ($null -eq $downloadResult)
        {
            throw 'File download failed'
        }

        # Step 3: Run the Download scripts
        foreach ($dependency in $DependencyPaths)
        {
            Download -ScriptPath $dependency.path
        }

        # Final output
        $activityOutput.success = $true
        $activityOutput.output = 'All scripts executed successfully.'
    }
    catch
    {
        $activityOutput.success = $false
        $activityOutput.error = $_.Exception.Message
        Write-Error "Execution failed`: $_"
    }

    return $activityOutput
}

# Call the main function to execute the process
$Result = Execute
[Console]::WriteLine($Result)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment