How to Clean Stale Computers

 

Applies To: Windows Server Update Services

Use the following procedure to clean stale computers that have not contacted the server in a specific number of days and then either delete them or move them to the Stale Computers group. The example that follows shows a working command-line application that has command-line parameters enabling the user to control the number of days of inactivity before a computer is defined as "stale," the action to be taken (delete or move) for any computers found to be stale, and whether or not to prompt the user before starting the delete or move procedure.

To clean stale computers

  1. Add the Microsoft.UpdateServices.Administration assembly to your project.

  2. Insert a using statement for the Microsoft.UpdateServices.Administration namespace.

    using Microsoft.UpdateServices.Administration;  
    
  3. Determine the number of days that will define a computer as being stale. In the following code example, I’m using today’s date (DateTime.Now) and subtracting a hard-coded value of 14 days from it to yield a DateTime object that represents 14 days in the past from today. This value will be used later to filter, or scope, computers that have not contacted the server since that date.

    // Get DateTime object from today minus 14 days  
    DateTime lastValidContactDate = DateTime.Now.Subtract(new TimeSpan(14, 0, 0, 0));  
    
  4. Connect to the local Windows Server Update Services (WSUS) server.

    // Connect to the local updateServer  
    IUpdateServer updateServer = AdminProxy.GetUpdateServer();  
    
  5. Instantiate a ComputerTargetCollection object that will hold the computer objects representing the stale computers that must be deleted from or moved to the Stale Computers group. I’ll refer to this collection as the Move/Delete collection.

    // Will hold the list of computers that need to be deleted or moved  
    ComputerTargetCollection computersToMoveOrDelete = new ComputerTargetCollection();  
    
  6. Write a conditional code block that, if moving the stale computers, will do the following:

    1. Verify whether the Stale Computers group already exists. This is done by iterating through all the groups returned from the IUpdateServer.GetComputerTargetGroups method and manually comparing the group name (IComputerTargetGroup.Name) to the string "Stale Computers".

    2. If the Stale Computers group does not exist, create it via the IUpdateServer.CreateComputerTargetGroup method.

    3. Call the IComputerTargetGroup.GetComputerTargets method, which returns a ComputerTargetCollection collection of all computers in the Stale Computers group that will be used later for comparison purposes.

    // If not deleting, then find the stale computers group and its current members  
    IComputerTargetGroup staleGroup = null;  
    ComputerTargetCollection computersInStaleGroup = null;  
    const string StaleComputerGroupName = "Stale Computers";      
    
    // Verify if the "stale" group already exists  
    foreach(IComputerTargetGroup grp in updateServer.GetComputerTargetGroups())  
    {  
       if (grp.Name == StaleComputerGroupName)  
       {  
          staleGroup = grp;  
          break;  
       }  
    }  
    
    // If the Stale Computers group doesn't exist, then create it  
    if (staleGroup == null)   
    {  
       staleGroup = updateServer.CreateComputerTargetGroup(StaleComputerGroupName);  
    }  
    
    // Get the list of local computers that are already in the Stale Computers group  
    computersInStaleGroup = staleGroup.GetComputerTargets();  
    
  7. Define a filter scope – in the form of a ComputerTargetScope object - that will enable you to obtain from the WSUS server a collection of only computers that haven’t contacted the server in the desired number of days.

    // Specify a filter scope of LastSyncTime < lastValidContactDate  
    ComputerTargetScope computerScope = new ComputerTargetScope();  
    computerScope.ToLastSyncTime = lastValidContactDate;  
    computerScope.IncludeDownstreamComputerTargets = false;  
    
  8. After you have the scope collection, iterate over that collection to add the computers to the Move/Delete collection for later processing. However, if you’re planning only to move the computers to the Stale Computers group (rather than deleting them), first verify that the computer isn’t already in the Stale Computers group.

    // For each computer in scoped collection  
    foreach(IComputerTarget computer in updateServer.GetComputerTargets(computerScope))  
    {  
    // If moving, only put in the Move/Delete collection for later processing if the computer doesn’t already exist in the Stale Computers group  
    if (!computersInStaleGroup.Contains(computer))  
       computersToMoveOrDelete.Add(computer);  
    
    // If deleting, you can put each one in the Move/Delete collection for later processing  
    computersToMoveOrDelete.Add(computer);  
    }  
    
  9. At this point, you have the collection of stale computers you want to either move or delete. Simply iterate over the Move/Delete collection and delete or move the stale computers as desired.

    foreach (IComputerTarget computer in computersToMoveOrDelete)  
    {  
       // If deleting the stale computers  
       computer.Delete();  
    
       // If moving the stale computers  
       staleGroup.AddComputerTarget(computer);  
    }  
    

Example

The following is the complete code listing for a console application that has command-line parameters enabling the user to control the number of days of inactivity before a computer is defined as "stale," the action to be taken (delete or move) for any computers found to be stale, and whether or not to prompt the user before starting the delete or move procedure.

/*----------------------------------------------------------------------  
This file is part of the Microsoft Windows Server Update Services  
API Code Samples.  
  
DISCLAIMER OF WARRANTY: THIS CODE AND INFORMATION ARE PROVIDED "AS-IS."    
YOU BEAR THE RISK OF USING IT.  MICROSOFT GIVES NO EXPRESS WARRANTIES,   
GUARANTEES OR CONDITIONS.  YOU MAY HAVE ADDITIONAL CONSUMER RIGHTS   
UNDER YOUR LOCAL LAWS WHICH THIS AGREEMENT CANNOT CHANGE.    
TO THE EXTENT PERMITTED UNDER YOUR LOCAL LAWS, MICROSOFT EXCLUDES   
THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR   
PURPOSE AND NON-INFRINGEMENT.  
----------------------------------------------------------------------*/  
using System;  
using System.Collections.Generic;  
using System.Linq;  
using System.Text;  
using Microsoft.UpdateServices.Administration;  
  
/*  
 * Purpose: This sample illustrates how to use the WSUS Class Library to   
 * remove computers from the WSUS updateServer that have not contacted  
 * the updateServer in a specified number of days.  
 * Syntax: CleanStaleComputers /d {1-365} [/a {Move | Delete}] [/p {Yes | No}]  
 * /d - [Days] Number of days the client has been stale: from 1-365  
 * /a - [Action] Delete from the WSUS updateServer or Move to the Stale Computers group. [Defaults to "Move"]  
 * /p - [Prompt] Prompts before moving/deleting computers. [Defaults to "Yes"]  
 */  
  
namespace WsusSamples  
{  
  class CleanStaleComputers  
  {  
    static void Main(string[] args)  
    {  
      CleanStaleComputers program = new CleanStaleComputers();  
      program.Run(args);  
    }  
  
    const string StaleComputerGroupName = "Stale Computers";      
  
    int days;  
    bool prompt;  
    public enum Action  
    {  
      Move,  
      Delete  
    };  
    Action action;  
  
    void Run(string[] args)  
    {  
      InitUI();  
      if (ParseParameters(args))  
      {  
        CleanUp();  
      }  
    }  
  
    void InitUI()  
    {  
      // Clear console and output what we're doing  
      Console.Clear();  
    }  
  
    bool ParseParameters(string[] args)  
    {  
      // We need at least the /d or /? param  
      if (args.Length < 2)  
      {  
        return PrintSyntax();  
      }  
  
      // Check for syntax help request  
      if (Array.IndexOf(args, "/?") != -1)  
      {  
        return PrintSyntax();  
      }  
  
      // Days (/d) parameter MUST be passed  
      int idx;  
      if ((idx = Array.IndexOf(args, "/d")) != -1)  
      {  
        // backup backupFolder/folder specified  
        this.days = Convert.ToInt32(args[idx + 1]);  
        if (this.days < 1 || this.days > 365) return PrintSyntax();  
      }  
      else  
      {  
        return PrintSyntax();  
      }  
  
      // Get the action - /a - param. If not supplied, we default to MOVE  
      this.action = Action.Move;  
      if ((idx = Array.IndexOf(args, "/a")) != -1)  
      {  
        // Save the user-specified comment for later use when backing up the GPOs  
        string temp = args[idx + 1];  
        if (0 == temp.ToUpper().CompareTo("MOVE"))   
          this.action = Action.Move;  
        else if (0 == temp.ToUpper().CompareTo("DELETE"))   
          this.action = Action.Delete;  
        else return PrintSyntax();  
      }  
  
      // Get the prompt - /a - param. If not supplied, we default to NO  
      this.prompt = true;  
      if ((idx = Array.IndexOf(args, "/p")) != -1)  
      {  
        // Save the user-specified comment for later use when backing up the GPOs  
        string temp = args[idx + 1];  
        if (0 == temp.ToUpper().CompareTo("YES"))  
          this.prompt = true;  
        else if (0 == temp.ToUpper().CompareTo("NO"))  
          this.prompt = false;  
        else return PrintSyntax();  
      }  
  
      return true; // successful parsing  
    }  
  
    bool PrintSyntax()  
    {  
      Console.WriteLine("Syntax: CleanStaleComputers /d {1-365} [/a {Move | Delete}] [/p {Yes | No}]");  
      Console.WriteLine("/d - [Days] Number of days the client has been stale: from 1-365");  
      Console.WriteLine("/a - [Action] Delete from the WSUS updateServer or Move to the Stale Computers group. [Defaults to Move]");  
      Console.WriteLine("/p - [Prompt] Prompts before moving/deleting computers. [Defaults to Yes]");  
  
      return false; // if we got here the command line parsing failed  
    }  
  
    void CleanUp()  
    {  
      try  
      {  
        // Figure out the last valid contact date.   
        DateTime lastValidContactDate = DateTime.Now.Subtract(new TimeSpan(this.days, 0, 0, 0));  
  
        // Connect to the local updateServer  
        IUpdateServer updateServer = AdminProxy.GetUpdateServer();  
  
        // Will hold the list of computers that need to be deleted or moved  
        ComputerTargetCollection computersToMoveOrDelete = new ComputerTargetCollection();  
  
        // If not deleting, then find the stale computers group and its current members  
        IComputerTargetGroup staleGroup = null;  
        ComputerTargetCollection computersInStaleGroup = null;  
        if (this.action == Action.Move)  
        {  
          // Verify if the "stale" group already exists  
          foreach(IComputerTargetGroup grp in updateServer.GetComputerTargetGroups())  
          {  
            if (grp.Name == StaleComputerGroupName)  
            {  
              staleGroup = grp;  
              break;  
            }  
          }  
  
          // If the Stale Computers group doesn't exist, then create it  
          if (staleGroup == null)   
          {  
            staleGroup = updateServer.CreateComputerTargetGroup(StaleComputerGroupName);  
          }  
  
          // Get the list of local computers that are already in the Stale Computers group  
          computersInStaleGroup = staleGroup.GetComputerTargets();  
        }  
  
        // Specify a filter scope of LastSyncTime < lastValidContactDate  
        ComputerTargetScope computerScope = new ComputerTargetScope();  
        computerScope.ToLastSyncTime = lastValidContactDate;  
        computerScope.IncludeDownstreamComputerTargets = false;  
  
        // For each computer in scoped collection  
        foreach(IComputerTarget computer in updateServer.GetComputerTargets(computerScope))  
        {  
          // Verify that this computer isn't already in the stale computers group  
          // if it is AND we're not going to delete, then don't include it.   
  
          // If moving...  
          if (this.action == Action.Move)  
          {  
            // ... and not already in Stale Group, add to move/delete collection  
            if (!computersInStaleGroup.Contains(computer))  
            {  
              computersToMoveOrDelete.Add(computer);  
            }  
          }  
          else // Else, if deleting add to move/delete collection  
          {  
            computersToMoveOrDelete.Add(computer);  
          }  
        }  
  
        // If there are no stale machines, report that and return  
        if (computersToMoveOrDelete.Count == 0)  
        {  
          Console.WriteLine("There are no computers that have not contacted the updateServer in the last {0} days{1}",  
                            this.days,  
                            (this.action == Action.Move ? " that are not already in the Stale Computers group." : "."));  
          return;  
        }  
  
        // Prompt the user if they asked us to  
        if (this.prompt)  
        {  
          PrintPrompt(false, computersToMoveOrDelete);  
  
          // Print the names of the computers that will be moved or deleted  
          foreach(IComputerTarget computer in computersToMoveOrDelete)  
          {  
            Console.WriteLine("\t{0}", computer.FullDomainName);  
          }  
          Console.Write("Continue? [Y]es, [N]o: ");  
  
          // Loop asking for input until we get a yes or a no.   
          while (true)  
          {  
            switch (Console.ReadLine().ToUpper())  
            {  
              case "Y" : return;  
              case "N" : return;  
              default:   
              {  
                PrintPrompt(true, computersToMoveOrDelete);  
                break;  
              }  
            }  
          }  
        }  
  
        // If we got here, the user must have declined prompts or replied Y to the prompt to continue  
  
        // Either delete the computers...  
        if (this.action == Action.Delete)  
        {  
          Console.WriteLine("Deleting {0} computers.", computersToMoveOrDelete.Count);  
  
          foreach (IComputerTarget computer in computersToMoveOrDelete)  
          {  
            Console.WriteLine("Deleting computer: {0}", computer.FullDomainName);  
            // computer.Delete();  
          }  
        }  
        else // ... or move them  
        {  
          Console.WriteLine("Moving {0} computers.", computersToMoveOrDelete.Count);  
  
          foreach (IComputerTarget computer in computersToMoveOrDelete)  
          {  
            Console.WriteLine("Moving computer: {0}", computer.FullDomainName);  
            // staleGroup.AddComputerTarget(computer);  
          }  
        }  
      }  
      catch(Exception ex)  
      {  
        Console.WriteLine("An error occurred while trying to clean up stale computers. Details: {0}", ex.Message);  
        return;  
      }  
    }  
  
    void PrintPrompt(bool error, ComputerTargetCollection computersToMoveOrDelete)  
    {  
      Console.WriteLine();  
      if (error)  
      {  
        Console.WriteLine("Invalid response. Please type Y or N followed by the [Enter] key");  
      }  
  
      Console.WriteLine("You are about to {0} the following {1} computers:",   
                        (this.action == Action.Move ? "move" : "delete"),   
                        computersToMoveOrDelete.Count);  
    }  
  }  
}