Renombrar dispositivos desde un CSV
Cambia en bloque el nombre de equipos Windows administrados con la acción remota setDeviceName de Intune, a partir de un CSV con el nombre actual y el nuevo.
Ejemplo en México
Cadena de tiendas de conveniencia en Hermosillo, 220 personas
La gerencia cambió la convención de nombres para identificar sucursal y tipo de equipo y hay 80 computadoras con nombres genéricos.
.\rename-devices-from-csv.ps1 -CsvPath ".\renombrar.csv" -WhatIfLa vista previa confirma que 80 nombres son válidos y los equipos existen. Tras ejecutarlo, las computadoras toman el nombre nuevo al reiniciar.
Cómo implementarlo
- Crea un CSV con las columnas DeviceName (nombre actual en Intune) y NewName.
- Cuida los nombres nuevos: máximo 15 caracteres, solo letras, números y guiones, y no solo dígitos.
- Corre con -CsvPath y -WhatIf (o -DryRun "true") para validar nombres y equipos sin enviar nada.
- Ejecuta sin simulación; el cambio se aplica en el siguiente contacto del equipo y suele requerir reinicio.
Ver el código completo de rename-devices-from-csv.ps1
<#
.TITLE
Rename Devices from CSV
.SYNOPSIS
Bulk-renames Intune Windows devices from a CSV mapping file using the setDeviceName remote action.
.DESCRIPTION
This script reads a CSV file with DeviceName and NewName columns and triggers the
Intune setDeviceName remote action for each matching Windows device. Every rename
supports -WhatIf preview, name validation catches illegal computer names before
any action is sent, and results are summarized per device. Devices are renamed on
their next check-in; Windows devices typically need a restart for the new name to
fully apply.
.TAGS
Devices,Operational
.MINROLE
Intune Administrator
.PERMISSIONS
DeviceManagementManagedDevices.PrivilegedOperations.All,DeviceManagementManagedDevices.Read.All
.AUTHOR
Ugur Koc
.VERSION
1.3
.CHANGELOG
1.3 - Added a portal-safe DryRun mode for Azure Automation rename previews
1.2 - Added Azure Automation contract validation, portal-safe boolean parameters, beta Graph endpoints, and terminating paging errors
1.1 - Azure Automation now records script progress, outcomes, and summaries in job history
1.0 - Initial release
.LASTUPDATE
2026-07-30
.EXAMPLE
.\rename-devices-from-csv.ps1 -CsvPath ".\renames.csv" -WhatIf
Previews all renames without sending any action
.EXAMPLE
.\rename-devices-from-csv.ps1 -CsvPath ".\renames.csv"
Renames all devices listed in the CSV (columns: DeviceName,NewName)
.EXAMPLE
.\rename-devices-from-csv.ps1 -CsvContent $csvContent
Uses CSV text supplied at runtime, which is suitable for Azure Automation
.EXAMPLE
.\rename-devices-from-csv.ps1 -CsvContent $csvContent -DryRun "true"
Validates devices and proposed names without sending a rename action
.NOTES
- Requires Microsoft.Graph.Authentication module
- CSV must contain the columns DeviceName (current Intune device name) and NewName
- Windows computer names: max 15 characters, letters/digits/hyphens, not all digits
- The rename applies at next device check-in; a restart completes it on Windows
- setDeviceName requires the DeviceManagementManagedDevices.PrivilegedOperations.All scope
- Uses beta Graph endpoints because the setDeviceName action is exposed there for all platforms
- Local interactive sign-in uses the MgGraphCommunity module to avoid the Graph SDK's mandatory WAM broker on Windows
#>
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[Parameter(Mandatory = $false, HelpMessage = "Path to CSV file with DeviceName,NewName columns")]
[string]$CsvPath,
[Parameter(Mandatory = $false, HelpMessage = "CSV text with DeviceName,NewName columns, suitable for Azure Automation")]
[string]$CsvContent = "",
[Parameter(Mandatory = $false, HelpMessage = "Export results to CSV")]
[ValidateSet("true", "false", "1", "0", '$true', '$false')]
[string]$ExportToCsv,
[Parameter(Mandatory = $false, HelpMessage = "Validate proposed renames without sending an action")]
[ValidateSet("true", "false", "1", "0", '$true', '$false')]
[string]$DryRun,
[Parameter(Mandatory = $false, HelpMessage = "Output path for exports")]
[string]$OutputPath = ".",
[Parameter(Mandatory = $false, HelpMessage = "Force module installation without prompting")]
[ValidateSet("true", "false", "1", "0", '$true', '$false')]
[string]$ForceModuleInstall
)
# Normalize the local module-install override for Azure Automation parameter binding.
$forceModuleInstallRaw = [string]$ForceModuleInstall
Remove-Variable -Name ForceModuleInstall
if ([string]::IsNullOrWhiteSpace($forceModuleInstallRaw)) {
$ForceModuleInstall = $false
}
elseif ($forceModuleInstallRaw.Trim().ToLowerInvariant() -in @("true", "1", '$true')) {
$ForceModuleInstall = $true
}
elseif ($forceModuleInstallRaw.Trim().ToLowerInvariant() -in @("false", "0", '$false')) {
$ForceModuleInstall = $false
}
else {
throw "Parameter 'ForceModuleInstall' accepts only true, false, 1, 0, $true, or $false."
}
# Azure Automation supplies portal parameter values as strings. Normalize the
# public boolean parameters once so local and runbook execution use real booleans.
foreach ($runbookBooleanParameter in @('ExportToCsv', 'DryRun')) {
$runbookBooleanRaw = [string](Get-Variable -Name $runbookBooleanParameter -ValueOnly)
Remove-Variable -Name $runbookBooleanParameter
if ([string]::IsNullOrWhiteSpace($runbookBooleanRaw)) {
Set-Variable -Name $runbookBooleanParameter -Value $false
continue
}
switch ($runbookBooleanRaw.Trim().ToLowerInvariant()) {
{ $_ -in @("true", "1", '$true') } {
Set-Variable -Name $runbookBooleanParameter -Value $true
}
{ $_ -in @("false", "0", '$false') } {
Set-Variable -Name $runbookBooleanParameter -Value $false
}
default {
throw "Parameter '$runbookBooleanParameter' accepts only true, false, 1, 0, $true, or $false."
}
}
}
# ============================================================================
# ENVIRONMENT DETECTION AND SETUP
# ============================================================================
function Initialize-RequiredModule {
param(
[string[]]$ModuleNames,
[bool]$IsAutomationEnvironment,
[bool]$ForceInstall = $false
)
foreach ($ModuleName in $ModuleNames) {
Write-Verbose "Checking module: $ModuleName"
$module = Get-Module -ListAvailable -Name $ModuleName | Select-Object -First 1
if (-not $module) {
if ($IsAutomationEnvironment) {
throw "Module '$ModuleName' is not available in Azure Automation"
}
else {
Write-Information "Module '$ModuleName' not found. Installing..." -InformationAction Continue
if (-not $ForceInstall) {
$response = Read-Host "Install module '$ModuleName'? (Y/N)"
if ($response -notmatch '^[Yy]') {
throw "Module '$ModuleName' is required but installation was declined."
}
}
try {
$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")
$scope = if ($isAdmin) { "AllUsers" } else { "CurrentUser" }
Install-Module -Name $ModuleName -Scope $scope -Force -AllowClobber -Repository PSGallery
Write-Information "✓ Successfully installed '$ModuleName'" -InformationAction Continue
}
catch {
throw "Failed to install module '$ModuleName': $($_.Exception.Message)"
}
}
}
Import-Module -Name $ModuleName -Force -ErrorAction Stop
}
}
# Detect execution environment
$IsAzureAutomation = $null -ne $PSPrivateMetadata.JobId.Guid
# Initialize required modules
$RequiredModules = @("Microsoft.Graph.Authentication")
# MgGraphCommunity gives WAM-free interactive sign-in for local runs
if (-not $IsAzureAutomation) {
$RequiredModules += "MgGraphCommunity"
}
try {
Initialize-RequiredModule -ModuleNames $RequiredModules -IsAutomationEnvironment $IsAzureAutomation -ForceInstall $ForceModuleInstall
Write-Verbose "✓ All required modules are available"
}
catch {
Write-Error "Module initialization failed: $_"
exit 1
}
# ============================================================================
# AUTHENTICATION
# ============================================================================
try {
if ($IsAzureAutomation) {
Write-Output "Connecting to Microsoft Graph using Managed Identity..."
Connect-MgGraph -Identity -NoWelcome -ErrorAction Stop
}
else {
Write-Output "Connecting to Microsoft Graph..."
$Scopes = @(
"DeviceManagementManagedDevices.PrivilegedOperations.All",
"DeviceManagementManagedDevices.Read.All"
)
Connect-MgGraphCommunity -Scopes $Scopes -NoWelcome -ErrorAction Stop
}
Write-Output "✓ Successfully connected to Microsoft Graph"
}
catch {
Write-Error "Failed to connect to Microsoft Graph: $($_.Exception.Message)"
exit 1
}
# ============================================================================
# HELPER FUNCTIONS
# ============================================================================
function Get-MgGraphAllPage {
param(
[string]$Uri,
[int]$DelayMs = 100
)
$allResults = @()
$nextLink = $Uri
do {
try {
if ($allResults.Count -gt 0) {
Start-Sleep -Milliseconds $DelayMs
}
$response = Invoke-MgGraphRequest -Uri $nextLink -Method GET
if ($null -ne $response.value) {
$allResults += $response.value
}
else {
$allResults += $response
}
$nextLink = $response.'@odata.nextLink'
}
catch {
if ($_.Exception.Message -like "*429*") {
Write-Information "Rate limit hit, waiting 60 seconds..." -InformationAction Continue
Start-Sleep -Seconds 60
continue
}
throw "Error fetching data: $($_.Exception.Message)"
}
} while ($nextLink)
return $allResults
}
function Test-ValidComputerName {
param([string]$Name)
if ([string]::IsNullOrWhiteSpace($Name)) { return $false }
if ($Name.Length -gt 15) { return $false }
if ($Name -match '^[0-9]+$') { return $false }
if ($Name -notmatch '^[A-Za-z0-9-]+$') { return $false }
if ($Name.StartsWith("-") -or $Name.EndsWith("-")) { return $false }
return $true
}
# ============================================================================
# MAIN SCRIPT LOGIC
# ============================================================================
try {
$csvInputs = @(
if (-not [string]::IsNullOrWhiteSpace($CsvPath)) { 'CsvPath' }
if (-not [string]::IsNullOrWhiteSpace($CsvContent)) { 'CsvContent' }
)
if ($csvInputs.Count -ne 1) {
throw "Specify exactly one CSV input: CsvPath or CsvContent."
}
if (-not [string]::IsNullOrWhiteSpace($CsvContent)) {
$renameEntries = @($CsvContent | ConvertFrom-Csv -ErrorAction Stop)
$csvSourceDescription = "runtime CSV content"
}
else {
if (-not (Test-Path $CsvPath)) {
throw "CSV file '$CsvPath' does not exist"
}
$renameEntries = @(Import-Csv -Path $CsvPath -ErrorAction Stop)
$csvSourceDescription = $CsvPath
}
if ($renameEntries.Count -eq 0) {
throw "CSV input contains no rows"
}
$csvColumns = $renameEntries[0].PSObject.Properties.Name
if ($csvColumns -notcontains "DeviceName" -or $csvColumns -notcontains "NewName") {
throw "CSV must contain the columns 'DeviceName' and 'NewName' (found: $($csvColumns -join ', '))"
}
Write-Output "✓ Loaded $($renameEntries.Count) rename entries from $csvSourceDescription"
[System.Collections.Generic.List[Object]]$report = @()
$renamed = 0
$failed = 0
$skipped = 0
foreach ($entry in $renameEntries) {
$currentName = $entry.DeviceName.Trim()
$newName = $entry.NewName.Trim()
# Validate before touching Graph so one bad row cannot burn an action
if (-not (Test-ValidComputerName -Name $newName)) {
Write-Warning "Skipping '$currentName': new name '$newName' is not a valid computer name (max 15 chars, letters/digits/hyphens, not all digits)"
$report.Add([PSCustomObject]@{ DeviceName = $currentName; NewName = $newName; Result = "InvalidName" })
$skipped++
continue
}
$escapedName = $currentName -replace "'", "''"
$found = Get-MgGraphAllPage -Uri "https://graph.microsoft.com/beta/deviceManagement/managedDevices?`$filter=deviceName eq '$escapedName'&`$select=id,deviceName,operatingSystem"
if (@($found).Count -eq 0) {
Write-Warning "Skipping '$currentName': device not found in Intune"
$report.Add([PSCustomObject]@{ DeviceName = $currentName; NewName = $newName; Result = "NotFound" })
$skipped++
continue
}
if (@($found).Count -gt 1) {
Write-Warning "Skipping '$currentName': $(@($found).Count) devices share this name - rename manually to avoid hitting the wrong one"
$report.Add([PSCustomObject]@{ DeviceName = $currentName; NewName = $newName; Result = "AmbiguousName" })
$skipped++
continue
}
$device = @($found)[0]
if ($DryRun) {
Write-Output "[DRY RUN] Would rename '$currentName' to '$newName'"
$report.Add([PSCustomObject]@{ DeviceName = $currentName; NewName = $newName; Result = "DryRun" })
continue
}
if ($PSCmdlet.ShouldProcess("$currentName -> $newName", "Send setDeviceName action")) {
try {
$body = @{ deviceName = $newName } | ConvertTo-Json
Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/beta/deviceManagement/managedDevices/$($device.id)/setDeviceName" -Method POST -Body $body -ContentType "application/json"
Write-Output "✓ Rename queued: $currentName -> $newName"
$report.Add([PSCustomObject]@{ DeviceName = $currentName; NewName = $newName; Result = "Queued" })
$renamed++
}
catch {
Write-Warning "Failed to rename '$currentName': $($_.Exception.Message)"
$report.Add([PSCustomObject]@{ DeviceName = $currentName; NewName = $newName; Result = "Failed" })
$failed++
}
}
else {
$report.Add([PSCustomObject]@{ DeviceName = $currentName; NewName = $newName; Result = "WhatIf" })
}
}
# Summary
Write-Output "`n========================================"
Write-Output "Rename Summary"
Write-Output "========================================"
Write-Output "CSV entries: $($renameEntries.Count)"
Write-Output "Renames queued: $renamed"
Write-Output "Failed: $failed"
Write-Output "Skipped: $skipped"
Write-Output "Note: renames apply at next device check-in; Windows devices need a restart to complete"
Write-Output "========================================"
# Export to CSV if requested
if ($ExportToCsv) {
if ($IsAzureAutomation) {
Write-Output "CSV report follows in the Azure Automation job output:"
$report | ConvertTo-Csv -NoTypeInformation | ForEach-Object { Write-Output $_ }
}
else {
$timestamp = Get-Date -Format "yyyy-MM-dd_HH-mm-ss"
$csvOutPath = Join-Path $OutputPath "Device_Rename_Results_$timestamp.csv"
$report | Export-Csv -Path $csvOutPath -NoTypeInformation -Encoding UTF8
Write-Output "✓ CSV report saved: $csvOutPath"
}
}
}
catch {
Write-Error "Script execution failed: $($_.Exception.Message)"
exit 1
}
finally {
try {
$null = Disconnect-MgGraph
Write-Output "✓ Disconnected from Microsoft Graph"
}
catch {
Write-Verbose "Graph disconnection completed"
}
}
# Copia sin cambios de https://github.com/ugurkocde/IntuneAutomation (commit 797bb2e).
# MIT License
#
# Copyright (c) 2025 Ugur Koc | Microsoft MVP
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.