Perte de connectivité réseau sur machines virtuelles

J'ai été confronté à un problème de manière aléatoire sur quelques machines virtuelles d'un cluster vSphere. Ce problème se traduisait par une perte de connectivité réseau : les machines virtuelles étaient incapables de joindre leur gateway et de communiquer avec d'autres machines du même VLAN. Une manière temporaire de rétablir la connectivité réseau était de réaliser un vMotion sur la machine virtuelle afin de la déplacer sur un autre hôte.

De nombreuses manipulations ont été tentées sur ces machines pour tenter de garder une connectivité durable : changement de type de carte réseau, création de nouvelle machine avec rattachement des disques virtuels, changement d'adresse MAC, etc. mais sans succès.

Les 2 hôtes vSphere formant le cluster ont chacun 4 interfaces physiques : 2 pour les machines virtuelles, une pour le management network et la dernière pour le vMotion. Sur ces deux dernières interfaces, seul un VLAN était déclaré côté switch physique.

En regardant du côté des vSwitch positionnés sur chaque hôte, il apparaissait qu'en fait les VLAN déclarés sur vSphere étaient poussés sur toutes les interfaces physiques (vmnic) du serveur. Cette configuration pouvait donc faire qu'une machine située sur le VLAN "PROD_CLIENT" tentait de communiquer sur les 2 interfaces qui n'avaient pas ce VLAN de déclaré côté switch physique.

Explications : considérons un serveur avec 4 interfaces (vmnic0 à vmnic3) branchées sur les ports 1, 2, 3 et 4 d'un switch. Les ports 1 et 2 (branchés sur vmnic0 et vmnic1) donnent accès au VLAN "ADMIN" tandis que les ports 3 et 4 (branchés sur vmnic2 et vmnic3) donnent accès aux VLAN "PROD_CLIENT", "PROD_INTERNE", "RE7_CLI", "RE7_INT". Côté vSphere, ces VLAN sont visibles et peuplés ; seulement ils doivent être configurés sur les bonnes interfaces. Si le VLAN "PROD_CLIENT" était accessible sur vmnic2 et vmnic3 puisqu'il est disponible sur les ports 3 et 4 du switch, il ne l'était pas sur les ports 1 (vmnic0) ni 2 (vmnic1). Or, ce VLAN était configuré sur ESXi pour utiliser les 4 interfaces physiques.

Sur cette capture, le VLAN est configuré pour être accessible sur tous les vmnic ; cependant côté switch physique cela n'est pas le cas puisque le VLAN est configuré sur les seuls ports 3 et 4 (vmnic2 et vmnic3).

Étant donné que ce VLAN utilisait des interfaces sur lesquelles il était injoignable côté switch physique, lorsque vSphere faisait passer le trafic réseau de la VM sur ces interfaces les machines devenaient de ce fait inaccessibles et incapables de joindre leur passerelle. Un redémarrage complet de la machine virtuelle (et non pas uniquement l'OS invité) pouvait résoudre le souci temporairement puisque vSphere réinscrit la carte réseau virtuelle sur une des interfaces sur lesquelles le VLAN en question était accessible (dans notre exemple, vmnic2 ou vmnic3). La même action de réinscription se faisait dans le cadre d'un déplacement d'hôte, puisque la VM passait sur un autre hôte physique, avec d'autres cartes physiques ; cependant le dysfonctionnement persistait puisque l'autre hôte du cluster était configuré de la même manière.

Afin de résoudre le dysfonctionnement, il a donc fallu déplacer les vmnic0 et vmnic1 dans les adaptateurs inutilisés pour les VLAN "PROD_CLIENT", "PROD_INTERNE", "RE7_CLI" et "RE7_INT". Ainsi, le trafic des VM configurées sur ces réseaux passent désormais uniquement via vmnic2 et vmnic3 et le problème est résolu.

Sur cette capture, les VLAN sont bien configurés pour n'utiliser que les vmnic qui sont interfacés sur les ports du switch physique donnant accès aux VLAN en question.

VMware et Powershell : liste des datastores en surcharge

J'avais déjà développé un script envoyant un mail si un datastore tombait sous un seuil défini dans le script d'espace libre. Aujourd'hui, voici une variante de ce script puisque celui-ci ne va détecter une espace disque trop faible mais une sur-allocation de celui-ci. En effet, avec les disques à provisionnement dynamique, il est possible de se retrouver en surcharge en créant de nouveaux disques virtuels positionnés sur ce même datastore sur lequel l'espace disque aura été jugé suffisant, en omettant de prendre en compte la croissance des disques déjà présents. Une capture d'écran peut permettre de comprendre le phénomène.

Cette capture d'écran montre bien les différences entre l'espace libre affiché et celui que l'on peut calculer en déduisant l'espace provisionné de la capacité maximale.

Sur cette capture, le sixième datastore affiche 320,49 Go de libre sur 749,75 Go : sur le papier, il est possible de créer une nouvelle VM puisque nous sommes en dessous du seuil d'occupation que l'on ne souhaite pas dépasser (généralement 80%). Seulement, 652 Go sur les 749 sont provisionnés. Cela signifie que si les disques virtuels viennent à atteindre leur taille maximale, nous serons en dessous des 20% d'espace libre. Pire, si la taille provisionnée est supérieure à la capacité, les disques virtuels seraient incapables de grossir et les VM pourraient planter.

Cela dit, il n'est pas forcément nécessaire de s'en tenir simplement à l'espace alloué et à la capacité maximale du datastore. Avec des machines bien dimensionnées et des données de capacity-planning, il est tout à fait possible d'exploiter chaque méga-octet du datastore sans perdre systématiquement au minimum 20% de l'espace de stockage.

Ce script liste donc tous les datastores qui ont un espace provisionné supérieur au seuil défini par rapport à la capacité maximale. Par défaut, ce seuil est de 80% (calculé par 1 - 0,20 dans le script). Il prend le nom du cluster en paramètre -Cluster. Dans une fenêtre PowerCli, on appellera le script ainsi si l'on souhaite analyser le cluster nommé PROD_PARIS :

.\vmw_ds-allocated-size.ps1 -Cluster "PROD_PARIS"
Param([string]$Cluster)
 $threshold = 0.20
 $dslist = Get-Datastore -Location $Cluster
 foreach($ds in $dslist){
     $allocsize = 0
     $hdlist = Get-HardDisk -Datastore $ds
     foreach($hd in $hdlist){
         $allocsize += $hd.CapacityGB
     }
     if($allocsize -gt $ds.CapacityGB*(1-$threshold)) { echo $ds.Name }
 }

L'utilisation du pipe en Powershell

Loin de moi l'idée de vouloir dispenser des cours de Powershell, mais il y a certains concepts qui peuvent être déroutants lorsque l'on débute Powershell où que l'on a appris "sur le tas". Parmi ces concepts, celui du pipe (ou pipeline), que l'on retrouve dans d'autres environnements comme Bash.

Ce caractère | est très utile en Powershell car il permet de passer plusieurs instructions en une seule commande, permettant au code de gagner en lisibilité et bien souvent de limiter le temps de traitement et l'occupation en mémoire.

Prenons un exemple simple : je veux afficher une liste des adresses mail pour les utilisateurs de mon AD.

Sans utiliser de pipe, je vais devoir faire un code comme ceci :

$users = Get-AdUser -filter * -Properties mail
 foreach($user in $users) { echo $user.mail }

Le moteur Powershell va donc dans un premier temps effectuer une requête dans l'AD, stocker les résultats dans une variable, puis ensuite parser cette variable pour afficher les adresses mail. En utilisant un pipe, tout va tenir sur une ligne et je n'aurai pas à occuper de la mémoire en mobilisant une variable :

Get-AdUser -filter * -Properties mail | select mail

Le pipe, comme son nom l'indique, permet d'agir comme un tuyau. Dans cet exemple, pour chaque utilisateur que Get-ADUser renvoie, on affiche son adresse mail. Le moteur Powershell va donc traiter de multiples instructions en un seul appel.

Powershell offre nativement des possibilités de filtre ou de traitement avec le pipe. Dans cet exemple, je veux afficher les noms de fichiers commençant par la lettre W dans C:\Windows. Sans utiliser de pipe, je devrais exécuter un script comme ceci :

$items = Get-Item -Path C:\Windows\*
 foreach($item in $items){
     if($item.Name -like "w*") { echo $item }
 }

En utilisant un pipeline, je peux tout passer en une seule ligne, grâce à Where-Object :

Get-Item C:\windows\* | where-object { $_.Name -like "w*" }

Ici, Powershell va filtrer dès l'exécution de Get-Item au lieu de stocker dans une variable et de la parser ensuite. Get-Item retourne un objet par élément présent dans le dossier. Le pipeline where-object insère une condition pour le retour de la commande Get-Item ; dans cet exemple la condition est que le nom de l'objet retourné commence par la lettre W. PS va donc vérifier chaque objet au fur et à mesure qu'il est retourné par Get-Item et ne le considérer que si il remplit la condition posée dans Where-Object. Dans un pipeline, l'objet actuellement traité est désigné par la variable $_ ; on peut donc accéder aux propriétés, qui ici sont entre autres Mode, LastWriteTime, Length et Name.

Je souhaite désormais afficher les informations de plusieurs comptes AD en même temps à partir de leur nom de compte. Je vais donc déclarer ma liste d'utilisateurs :

$users = @("mscott","dschrute","jhalpert")

Je peux procéder de cette manière dans un script :

foreach($user in $users){
     Get-AdUser $user
}

Mais cela peut tenir sur une simple ligne en utilisant le pipeline ForEach-Object :

$users | foreach-object { Get-AdUser $_ }

Comme pour Where-Object, PS va parcourir le contenu de la variable $users et exécuter la commande incluse pour chaque objet qui sera retourné par $users.

Un autre exemple pratique pour Foreach-Object : ce script va lister les contrôleurs de domaine et exécuter un ping sur chacun d'entre eux.

$domctl = Get-ADDomainController -filter *
 foreach ($dc in $domctl){
     ping $dc.Name
 }

Ce code peut se résumer en une simple instruction :

Get-ADDomainController -Filter * | foreach-object { ping $_.Name }

Il est possible d'utiliser de multiples pipelines en une seule instruction. En reprenant l'exemple avec mes trois utilisateurs, je veux afficher ceux dont l'identifiant comme par la lettre M.

foreach($user in $users){
     if($user -like "m*") { Get-AdUser $user }
 }

Je peux éviter cette double boucle en utilisant deux pipelines comme ceci :

$users | where-object { $_ -like "m*" } | foreach-object { Get-AdUser $_ }

Dans un premier temps, PS va traiter la condition du Where-Object, et si celle-ci est validée, alors l'objet va passer dans le pipe du Foreach-Object. Tout objet qui ne commence pas par la lettre M ne passe pas dans le deuxième pipeline.

D'autres usages plus courants du pipe comprennent les commandes Format-List ( | fl ) ou Format-Table ( | ft ). Le premier permet d'afficher toutes les données retournées par une commande sous forme de liste. Des captures d'écran valent mieux qu'un long discours :

Get-Item C:\Windows\*
Get-Item C:\Windows\* | fl

Format-List nous permet d'obtenir bien plus d'informations que dans le retour de base de la commande Get-Item. Format-Table, quant à lui, permet de sélectionner ce que l'on souhaite afficher, sous forme de tableau.

Get-Item C:\Windows\* | ft Name,CreationTime,LastWriteTime

Les pipelines remplissent toujours le même rôle : exécuter une instruction supplémentaire pour chaque objet retourné par l'instruction précédant le pipe.

Quelques snippets pour Exchange Management Shell

Travaillant de plus en plus sur Exchange ces derniers temps, j'essaye de maximiser mon utilisation courante du Exchange Management Shell. Je vais partager quelques snippets permettant de récupérer des informations qu'il est plus difficile voire impossible de récupérer via la console graphique. Ces snippets peuvent être de bonnes bases pour ensuite peaufiner une recherche, ou simplement avoir la syntaxe pour obtenir une autre information, ou encore pour être intégrés dans un script. A noter que j'ai utilisé ce code sur un Exchange on Premise, il est possible que certaines commandes soient à adapter pour un usage sur Exchange Online.


Liste des mailbox n'ayant pas OWA activé

Get-CasMailbox | Where-Object { $_.OWAEnabled -eq $false }

Liste des mailbox n'ayant pas ActiveSync activé

Get-CasMailbox | Where-Object { $_.ActiveSyncEnabled -eq $false }

La commande Get-CasMailbox permet de retourner d'autres informations, il suffit donc de modifier la propriété que l'on utilise comme critère pour obtenir un nouveau filtre.


Récupération de la taille des mailbox

Get-Mailbox | Get-MailboxStatistics | sort TotalItemSize | ft DisplayName, TotalItemSize, Database

Liste des mailbox n'étant pas sous la règle du quota par défaut de la database

Get-Mailbox | Where-Object {$_.UseDatabaseQuotaDefaults -eq $false} | sort ProhibitSendQuota | ft DisplayName, Database, ProhibitSendQuota

Vérifier les permissions d'un utilisateur sur les mailbox

Get-Mailbox | Get-MailboxPermission | Where-Object {$_.User -like "DOMAINE\enguerrand"} | ft AccessRights, IsInherited, Deny -GroupBy identity

Pour ce snippet, il suffit d'ajouter un paramètre à l'instruction Get-Mailbox pour cibler une boîte particulière.


Lister toutes les mailbox par type

Get-Mailbox | sort RecipientTypeDetails | ft DisplayName, Alias, PrimarySmtpAddress -GroupBy RecipientTypeDetails

Visualiser le trafic sur une mailbox

Get-TransportServer | Get-MessageTrackingLog -Start "01/01/1990 02:00:00" -End "02/01/1990 03:00:00" -Recipients enguerrand@domaine.local

Il est possible de remplacer le paramètre -Recipients par -Sender si l'on souhaite avoir un autre point de vue. Les deux paramètres sont cumulables, permettant de centrer la recherche sur un émetteur et un destinataire. Le paramètre -EventID pouvant prendre les valeurs SEND ou RECEIVE par exemple offre la possibilité de n'afficher uniquement les mails effectivement reçus ou envoyés.

Lister les mails reçus sur une mailbox

Get-TransportServer | Get-MessageTrackingLog -Start "01/01/1990 02:00:00" -End "02/01/1990 03:00:00" -Recipients enguerrand@domaine.local -EventID Receive

Vérifier si une mailbox a des éléments supprimés récupérables

Get-Mailbox enguerrand | Get-MailboxFolderStatistics | Where-Object { $_.Name -like "Recoverable Items" -and $_.ItemsInFolder -gt 0 }

Lister les mailbox attachées à des utilisateurs présents dans une OU

Get-Mailbox | Where-Object {$_.DistinguishedName -like "*OU=Sales*"} | select alias