PI Services

Le blog des collaborateurs de PI Services

SCCM - Les clients ne reçoivent plus les mises à jour après une restauration ou un changement de serveur

Suite au remplacement du serveur de site dans une infrastructure SCCM, les clients ne recevaient plus aucune mise à jour.

L’analyse du fichier de log UpdatesDeployment.log révèle le message “EnumerateUpdates for action (UpdateActionInstall) - Total actionable updates = 0 ' “.

Bien qu’il ne s’agisse pas à proprement parler d’une erreur, cette information est manifestement erronée : il y a bien des mises à jour manquantes, et l’exécution de la commande powershell suivante sur un client en donne la liste :

get-wmiobject -query "SELECT * FROM CCM_UpdateStatus" -namespace "root\ccm\SoftwareUpdates\UpdatesStore" | where {$_.status -eq "Missing"} 

Le client SCCM n’en reconnait pourtant aucune comme actionnable, comme le montre cette nouvelle commande qui ne renvoie rien :

get-wmiobject -query "SELECT * FROM CCM_SoftwareUpdate" -namespace "ROOT\ccm\ClientSDK"

Il faut savoir qu’à chaque synchronisation du catalogue WSUS, SCCM incrémente la version du catalogue. Cela permet aux agents de savoir si une version plus récente que celle qu’ils ont déjà téléchargé est disponible.

Or, lors du changement de serveur ou de la restauration d’une sauvegarde, il arrive que ce compteur reparte de 0… Si votre infrastructure SCCM existe depuis longtemps, il pourrait donc falloir des milliers de mise à jour du catalogue avant que la version n’atteigne à nouveau celle attendue par les agents!

Heureusement, le correctif est simple : il suffit de modifier dans le registre la version du catalogue afin de reprendre là où les agents s’étaient arrêté.

Commencons donc par identifier la version du catalogue qu’attendent les agents, à l’aide de la requête SQL suivante qui indique la plus haute valeur attendue :

;WITH XMLNAMESPACES ( DEFAULT 'http://schemas.microsoft.com/SystemsCenterConfigurationManager/2009/07/10/DesiredConfiguration') 
SELECT MAX(CI.SDMPackageDigest.value('(/DesiredConfigurationDigest/SoftwareUpdateBundle/ConfigurationMetadata/Provider/Operation[@Name="Detect"]/Parameter/Property[@Name="MinCatalogVersion"]/@Value)[1]', 'int')) MinCatalogVersion  
FROM [CI_ConfigurationItems] as CI  
WHERE CIType_ID = 8  

Il faut ensuite incrémenter d’une unité cette valeur, puis l’insérer dans la base de registre dans les clés suivantes :

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\SMS\Components\SMS_WSUS_SYNC_MANAGER]
"ContentVersion"=x
"SyncToVersion"=x
"LastAttemptVersion"=x

Enfin, il faut exécuter une nouvelle synchronisation du catalogue dans SCCM afin que sa valeur reflète les modifications effectuées (ou attendre la prochaine synchronisation planifiée), puis exécuter un Machine policy refresh suivi d’un Update evaluation cycle sur les agents si vous souhaitez forcer le téléchargement des mises à jour.

SCOM 1801 - La console web affiche une page blanche sous IE11

Encore un petit souci de jeunesse de la nouvelle console web introduite dans SCOM 1801 lorsque vous tentez d’y accéder depuis Internet Explorer 11, une page blanche est affichée alors qu’elle fonctionne parfaitement sous Chrome/Firefox/Edge.

Pour pallier ce problème, il suffit de rajouter le HTTP Host Header suivant dans IIS, pour les sites OperationsManager et MonitoringView :

image

Nom : X-UA-Compatible

Valeur: IE=edge

image

Ce problème est normalement réglé par le passage à SCOM 1807, mais si tel n’était pas le cas, ce contournement devrait vous permettre de rétablir un accès normal!

SCOM 18xx–Voir les alertes dans la console web

Encore un souci lié à la nouvelle console web, introduite dans SCOM 1801 : lors de la création d’une Notification pour envoyer des alertes par mail, il est d’usage d’intégrer dans le corps du mail l’URL permettant d’ouvrir la console web sur l’URL en question, en utilisant le raccourci généré par le wizard :

image

image

Seulement voilà, l’URL insérée ici est incorrecte, elle ne correspond pas à la nouvelle console Web et cette adresse renvoie donc à la page d’accueil de la console !

Ce bug a été corrigé lors du passage à SCOM 1807 :

image

On voit ici que le bon format d’URL à utiliser est en réalité $Target/Property[Type="Notification!Microsoft.SystemCenter.AlertNotificationSubscriptionServer"]/WebConsoleUrl$/#/monitoring/drilldown/alert/$UrlEncodeData/Context/DataItem/AlertId$ , ce qui correspond au nouveau dashboard “Alert Drilldown”, auquel on accède normalement à partir d’un dashboard Alerts “nouvelle génération” (plus d’explications sur les nouveaux dashboards et les relations entre eux ici : https://blogs.technet.microsoft.com/momteam/2018/02/12/new-scom-web-console-blog-series-post4-the-all-new-drilldown-experience/ ) :

image


Malheureusement, cela engendre un nouveau problème : si l’utilisateur qui reçoit ce mail mais n’a qu’un accès limité à SCOM (permission accordée uniquement à quelques dossiers/vues), il risque fort de se trouver face à cet écran :

image

Ou, dans le meilleur des cas, celui-ci qui n’est pas beaucoup plus utile :

image

La raison en est que ce nouveau dashboard Alert Drilldown contient une série de widgets qui nécessitent également que l’utilisateur aie le droit d’y accéder individuellement. Or, cette permission n’est accordée par l’assistant User Roles que lorsqu’on accorde la permission à un dashboard qui y fait appel… dont aucun n’existe par défaut. Et même s’il en existait, vous pourriez très bien ne pas vouloir les montrer à tout le monde!

Un contournement serait donc de créer un dashboard Alerts “nouvelle génération”, de le cibler sur un groupe vide créé également pour l’occasion et d’en déléguer l’accès à tous les User Roles susceptibles de devoir accéder au Drilldown d’une alerte :

image


image

L’utilisateur devrait maintenant avoir accès au dashboard Drilldown alert :

image


En espérant que Microsoft corrige cela dans la prochaine version…

PowerShell/SQL–Script générant un .csv contenant le résultat d’une requête

Introduction

Le script suivant permet :

  • de récupérer le résultat d’une requête SQL dans un fichier .csv,
  • de renseigner les actions réalisées dans l’EventViewer,
  • de retourner un code d’erreur (0 ou 1) suite à l’exécution du script.

Prérequis

  • Powershell
  • SQL

Présentation du script

Variables :

[string] $server = "localhost" : Serveur SQL
[string] $database = "base" : Base SQL
[string] $query = "SELCT * FROM Table" : Requête ou procédure stockée
[string] $extractFilePath = "C:\test.csv" : Emplacemnt de génération du fichier .csv
[string] $delimiter = ";" : Délimiteur à utiliser pour la génération du fichier .csv
[int] $skipline = 1 : Nombre de ligne à tronquer (utile si l’on souhaite retirer la ligne d’entête.

Script

 

# Variables d'entrée
param
    (
    [string] $server = "localhost", 
    [string] $database = "base",
    [string] $query = "SELECT * FROM Table",
    [string] $extractFilePath = "C:\test.csv",
    [string] $delimiter = ";",
    [int] $skipline = 1

    )

#MAIN
function main
{

#Variables calculées
$full_date = Get-Date;
$custom_date = $full_date.ToString("yyyyMMdd");
$DataSet = New-Object System.Data.DataSet;
$eventsource ="CsvFromSQL_Powershell";
$eventlogname = "Application";
[int] $errorcode = 1;


#Génération de la source d'evenements si inexistante
if ([System.Diagnostics.EventLog]::SourceExists($eventsource) -eq $false) 
    {
        #write-host "Creating event source $eventsource on event log $eventlogname"
        [System.Diagnostics.EventLog]::CreateEventSource($eventsource, $eventlogname)
        #write-host -foregroundcolor green "Event source $eventsource created"
    }
        else
    {
        #write-host -foregroundcolor yellow "Warning: Event source $eventsource already exists. Cannot create this source on Event log $eventlogname"
    }

<#
$logFileExists = Get-EventLog -list | Where-Object {$_.logdisplayname -eq $eventlogname} 
if (! $logFileExists) 
    {
        New-EventLog -LogName $eventlogname -Source $eventsource
    }
#>

#Execution des fonctions et récupération du code d'erreur
$errorcode = Test-SQLConn -_server $server -_database $database;
$errorcode = sql_to_dataset -_server $server -_database $database -_query $query -_dataset $DataSet;
$errorcode = dataset_to_csv -_dataset $DataSet -_extractFilePath $extractFilePath;
return $errorcode;
}

#Test connection SQL
Function Test-SQLConn ($_server, $_database)
{
    $errorcode=0;
    $connectionString = "Data Source=$_server;Integrated Security=true;Initial Catalog=$_database;Connect Timeout=3;";
    $sqlConn = new-object ("Data.SqlClient.SqlConnection") $connectionString;
    trap
        {
        Start-Sleep -s 1;
        Write-EventLog -LogName $eventlogname -Source $eventsource -EventID 1 -EntryType Error -Message "Cannot connect to server $_server or database $_database.";
        $errorcode=1;
        Write-Host $errorcode;
        exit
        }
    $sqlConn.Open()
    if ($sqlConn.State -eq 'Open')
        {
        $sqlConn.Close();
        Start-Sleep -s 1;
        Write-EventLog  -LogName $eventlogname -Source $eventsource -EventID 0 -EntryType Information -Message "Connected to server $_server on database $_database.";
        }
}

#Rempli un dataset à partir d'une requête SQL
function sql_to_dataset
    {
    param($_server, $_database, $_query, $_dataset);
    $connectionTemplate = "Data Source={0};Integrated Security=SSPI;Initial Catalog={1};";
    $connectionString = [string]::Format($connectionTemplate, $_server, $_database);
    $errorcode=0;
    try
        {
        $connection = New-Object System.Data.SqlClient.SqlConnection -ErrorAction stop;
        Start-Sleep -s 1;
        Write-EventLog -LogName $eventlogname -Source $eventsource -EventID 0 -EntryType Information -Message "The SQL connection has been created successfully.";
        }
    catch
        {
        Start-Sleep -s 1;
        Write-EventLog -LogName $eventlogname -Source $eventsource -EventID 1 -EntryType Error -Message "An error occurred when creating the SQL connection. Error Message : $_.Exception.Message";
        $errorcode=1;
        Write-Host $errorcode;
        exit;
        }

    $connection.ConnectionString = $connectionString;

    try
        {
        $command = New-Object System.Data.SqlClient.SqlCommand -ErrorAction stop;
        Start-Sleep -s 1;
        Write-EventLog -LogName $eventlogname -Source $eventsource -EventID 0 -EntryType Information -Message "The SQL command has been created successfully.";
        }
    catch
        {
        Start-Sleep -s 1;
        Write-EventLog -LogName $eventlogname -Source $eventsource -EventID 1 -EntryType Error -Message "An error occurred when creating the SQL command. Error Message : $_.Exception.Message";
        $errorcode=1;
        Write-Host $errorcode;
        exit;
        }
    
    $command.CommandText = $_query;
    $command.Connection = $connection;

    try
        {
        $SqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter -ErrorAction stop;
        Start-Sleep -s 1;
        Write-EventLog -LogName $eventlogname -Source $eventsource -EventID 0 -EntryType Information -Message "The SQL DataMapper has been created successfully.";
        }
    catch
        {
        Start-Sleep -s 1;
        Write-EventLog -LogName $eventlogname -Source $eventsource -EventID 1 -EntryType Error -Message "An error occurred when creating the SQL DataMapper. Error Message : $_.Exception.Message"; 
        $errorcode=1;
        Write-Host $errorcode;
        exit;
        }
    
    $SqlAdapter.SelectCommand = $command;

    try
        {
        $SqlAdapter.Fill($_dataSet) | Out-Null -ErrorAction stop;
        Start-Sleep -s 1;
        Write-EventLog -LogName $eventlogname -Source $eventsource -EventID 0 -EntryType Information -Message "The Dataset has been filled successfully.";
        }
    catch
        {
        Start-Sleep -s 1;
        Write-EventLog -LogName $eventlogname -Source $eventsource -EventID 1 -EntryType Error -Message "An error occurred when filling the Dataset. Error Message : $_.Exception.Message";
        $errorcode=1;
        Write-Host $errorcode;
        exit;
        }

    $connection.Close();
    return $errorcode;
    }

#Rempli un csv avec un dataset
function dataset_to_csv
{
param($_dataset, $_extractFilePath);
$errorcode=0;
    try
        {
        $_dataSet.Tables[0] | ConvertTo-Csv -NoTypeInformation -Delimiter $delimiter | select -Skip $skipline  | Set-Content $_extractFilePath -ErrorAction stop;
        Start-Sleep -s 1;
        Write-EventLog -LogName $eventlogname -Source $eventsource -EventID 0 -EntryType Information -Message "The CSV file has been created successfully.";
        }
    catch
        {
        Start-Sleep -s 1;
        Write-EventLog -LogName $eventlogname -Source $eventsource -EventID 1 -EntryType Error -Message "An error occurred when generating the CSV file. Error Message : $_.Exception.Message";
        $errorcode=1;
        Write-Host $errorcode;
        exit;
        }
        return $errorcode;
}

main



 

SCCM–Modifier depuis SQL le Maximum package processing threads.

Introdution

ATTENTION, l’action proposée par cet article ne fait pas l’objet d’une KB.

La valeur “Maximum package processing threads” permet de limiter le nombre de distribution concurrente de packages.

Cette valeur est bornée de 1 à 50, elle peut être modifiée depuis le console d’administration dans “Configure Site Components”

image

Problématique

Suite à une mauvaise manipulation, cette valeur a été modifiée à 0. La valeur 0 étant en dehors de [0..50], l’erreur suivante apparait :

clip_image001

Résolution

Le script suivant permet de récupérer la valeur actuelle :

Select * from SC_Component_Property cp join SC_Component c on c . ID = cp . ComponentID join SC_SiteDefinition sd on sd . SiteNumber = c . SiteNumber where cp . name = 'Thread Limit' and ComponentName = 'SMS_DISTRIBUTION_MANAGER' and sd . SiteCode = 'SITECODE'
 

Le script suivant permet de modifier la valeur à 3 :

Update CP Set Value3 = 3 from SC_Component_Property cp join SC_Component c on c . ID = cp . ComponentID join SC_SiteDefinition sd on sd . SiteNumber = c . SiteNumber where cp . name = 'Thread Limit' and ComponentName = 'SMS_DISTRIBUTION_MANAGER' and sd . SiteCode = 'SITECODE'
 

SCCM/WMI – Modifier depuis WMI le Maximum package processing threads.

Introdution

ATTENTION, l’action proposée par cet article ne fait pas l’objet d’une KB.

La valeur “Maximum package processing threads” permet de limiter le nombre de distribution concurrente de packages.

Cette valeur est bornée de 1 à 50, elle peut être modifiée depuis le console d’administration dans “Configure Site Components”

image_thumb5_thumb

Problématique

Suite à une mauvaise manipulation, cette valeur a été modifiée à 0. La valeur 0 étant en dehors de [0..50], l’erreur suivante apparait :

clip_image001_thumb1_thumb

Résolution

La valeur peut être modifié depuis SQL (cf. article précèdent), mais également depuis WMI :

Lancer l’outil WMI Tester :

image_thumb1[1]

Se connecter à “root\SMS\site_NOMDUSITE” puis cliquer sur “Enum Classes…” :

image_thumb4

Rechercher la classe “SMS_SCI_Component” puis double cliquer dessus :

image_thumb8

Cliquer sur “props” puis cliquer sur “Instances” :

image_thumb10

Choisir “SMS_Distribution_Manager” :

image_thumb12

Double cliquer sur “Props” :

image_thumb1

Cliquer sur “view embedded” :

image_thumb16

Vérifier les entrées suivantes jusqu’à trouver une valeur appelée “Thread Limit”

image_thumb18

image_thumb20

Modifier la valeur “Value” à la valeur souhaitée, dans cet exemple on passe de 0 (0x0) à 3 (0x0)  :

image_thumb22

image_thumb24

Fermer toutes les fenêtre en sauvegardant chacune.

SCOM - Script de Fermeture des alertes liées a des monitors en état Healthy

Ci-dessous une nouvelle version d'un script de fermeture des alertes liées a des monitors en état Healthy. En effet meme si le cas contraire est plus fréquent (alerte fermée alors que le monitor est encore en état Warning ou Critical), il se peut que l'on doivent fermer les alertes de monitors en état Healthy.

Le script affiche clairement la sortie des alertes a traiter et log cette sortie dans l'eventlog.

 

# SCRIPT TO CHECK INCONSISTENCY BETWEEN NOT CLOSED ALERTS AND HEALTHY MONITORS 


Param(
# Treat alerts that has been modified since less than $LastModifHours
$LastModifHours = 2
)

$ScriptName = "CloseAlertsFromHealthyMonitors.ps1"


# FUNCTIONS

# Check if a source with script name exist in operationsmanager eventlog to log some specific events 
         Function NewEventSource 
         { 
         if(!(Test-Path "HKLM:\SYSTEM\CurrentControlSet\services\eventlog\Operations Manager\$ScriptName")) 
         { 
         New-EventLog -LogName "Operations Manager" -Source $ScriptName 
         }
         } 


# END FUNCTIONS



#Log of script execution 
NewEventSource 
write-eventlog -logname "Operations Manager" -Source $ScriptName -EventID 1000 -Message "Execution of script $ScriptName (Value of LastModifHours is $LastModifHours hours)" -EntryType Information

# Import of Scom Module
Import-Module OperationsManager


# Get list of closed alerts with following criterias:
# - Not Closed
# - Generated by a monitor

$NotClosedAlerts = Get-SCOMAlert -ResolutionState (0..254)  | where { ($_.ismonitoralert -eq $true) -and $_.LastModified -gt (Get-Date).addhours(-$LastModifHours)}


# Variable to store the result of alert treatment.
# Header
$Result = "`n*********** CHECK INCONSISTENCY BETWEEN NOT CLOSED ALERTS AND HEALTHY MONITORS ***********"

$Result += "`n START `n"


for ($i=0;$i -le $NotClosedAlerts.GetUpperBound(0);$i=$i+1) 
{
# Display alert Nb
$NotClosedAlert = $NotClosedAlerts[$i]
$Result +=  "`n`nalert $i ------------------------------"



# Get IDs from Closed alert 
$mrid = $NotClosedAlert.monitoringruleid 
$mcid = $NotClosedAlert.monitoringclassid 
$moid = $NotClosedAlert.monitoringobjectid 



# Get corresponding class 
$monitoringclass = Get-SCOMClass -id $mcid

# Get the corresponding instance with following criterias:
# - HealthState equal Success 
$MyInstance = Get-SCOMMonitoringObject -Class $monitoringclass | where {$_.id -eq $moid -and $_.HealthState -eq "Success"} 

# If there is no instances no need to reset (exit the loop and treat the next closed alert)
If(!($MyInstance))
{
$Result += "No Instance found in Success state for the alert `"$($NotClosedAlert.Name)`" - NO NEED TO CLOSE ALERT"
}
Else
{
$Result += "`nThe following alert must be closed: `n"
$Result += "NAME: $($NotClosedAlert.Name)`n"
$Result += "ID: $($NotClosedAlert.Id)`n"
$Result += "COMPUTER OR OBJECT: $($NotClosedAlert.MonitoringObjectDisplayName)`n"
$Result += "COMPUTER OR OBJECT FULL PATH: $($NotClosedAlert.MonitoringObjectFullName)`n"
$Result += "COMPUTER OR OBJECT HEALTH STATE: $($NotClosedAlert.MonitoringObjectHealthState)`n"
$Result += "TIME RAISED: $($NotClosedAlert.TimeRaised)`n"
$Result += "TIME LAST MODIFIED: $($NotClosedAlert.LastModified)`n"



    try
    {
    Set-SCOMAlert -Alert $NotClosedAlert -ResolutionState 255
    $Result += "Closing of alert `"$($NotClosedAlert.Name)`"..."
    $Result += "-------------------------------------------------------`n`n`n"
    # Update of Alert History Text
    $NotClosedAlert.Update("Alert closed by $ScriptName script")
    }
    catch
    {
    $Result += "Error during the close of alert `"$($NotClosedAlert.Name)`" (ALERT ID: $($NotClosedAlert.Id))"
    }
}

}

$Result += "`n`n END `n"

# Check if $Result contains error of alert closing
if ($($Result | Out-String).Contains("Error during the close of alert"))
{
$Result += "At least one Error has been encountered during closing of some alerts"
NewEventSource 
write-eventlog -logname "Operations Manager" -Source $ScriptName -EventID 1001 -Message $Result -EntryType Warning
}
Else
{
$Result += "`n OK - Treatment of Alerts has ending without Error"
NewEventSource 
write-eventlog -logname "Operations Manager" -Source $ScriptName -EventID 1002 -Message $Result -EntryType Information

}

# Display Result
$Result

 

 

Zabbix API with Powershell – Example

L'API de Zabbix, basée sur le standard JSON-RPC 2.0 peux bien sur etre intérrogée aussi avec Powershell.

 Ci-dessous un script montrant le principe d'interrogation de l'API, en recuperant certaines infos.

 Vous devez renseigner un compte (<my_zabbix_account>) avec au minimum les droits de lecture sur Zabbix, et le nom ou l'ip du serveur Front Web (<zabbix_frontweb_server>).

 

### Query Zabbix Through native zabbix json api

    $credential = Get-Credential -Credential "my_zabbix_account"

$baseurl = 'https://<name_or_ip_of_front_server>/zabbix'
$params = @{
    body =  @{
        "jsonrpc"= "2.0"
        "method"= "user.login"
        "params"= @{
            "user"= $credential.UserName
            "password"= $credential.GetNetworkCredential().Password
        }
        "id"= 1
        "auth"= $null
    } | ConvertTo-Json
    uri = "$baseurl/api_jsonrpc.php"
    headers = @{"Content-Type" = "application/json"}
    method = "Post"
}

[System.Net.ServicePointManager]::SecurityProtocol = 'tls12'
[Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}

$result = Invoke-WebRequest @params -UseBasicParsing



$params.body = @{
    "jsonrpc"= "2.0"
    "method"= "host.get"
    "params"= @{
        output = "extend"
		selectFunctions = "extend"
		selectLastEvent = "extend"
		selectGroups = "extend"
		selectHosts = "extend"
    }
    auth = ($result.Content | ConvertFrom-Json).result
    id = 2
} | ConvertTo-Json

$result = Invoke-WebRequest @params -UseBasicParsing
$result = $result.Content | ConvertFrom-Json