Video Screencast Help

{CWoC} Patch Trending: Inactive Computer Trending Report

Created: 16 Sep 2013 • Updated: 16 Sep 2013
Language Translations
Ludovic Ferre's picture
+2 2 Votes
Login to vote

With the release of the {CWoC} Site Builder version 11 [1], comes the need for a new trending report, in order to record the count of inactive computers over time. This document contains everything you need to create the report and understand what it does, and why.

Content:

Summary:

The SQL code provided here (and in a Connect download that will be kept up to date [2]) creates 3 tables:

  • TREND_InactiveComputerCounts
  • TREND_InactiveComputer_Current
  • TREND_InactiveComputer_Previous

And a stored procedure:

  • spTrendInactiveComputers

The procedure can be simply called with:

exec spTrendInactiveComputers

in which case the tables will be populated if the last record was done more than 23 hours prior the query execution, or you can invoke the procedure using the force mode, that will cause data to be stored regardless of the previous execution time:

exec spTrendInactiveComputers @force = 1

This is useful if you have missed a schedule, or if you want to kick start the data gathering process.

So you can now trend inactive computers on your SMP by adding the above code in a report and scheduling it to run daily (either via a Task or Notification Policy.

Back to top

Background:

As you have probably have seen from my various "Patch trending" related posts (download, articles or blog entries) adding trending is a simple and important part of managing Patch Management Compliance for computer estate of varying size.

Quickly deploying patches to 30,000 computers world wide is not an easy task, especially when the target is to have 95% of the computers compliant at 95% of above (I call this the 95-95 rule), and when this ambitious target is not met we need to be able to explain why this is so.

So we need to know how many computers are inactive (off, or not reporting data) in over time, in order to factor those off-time in with the compliance data:  given we have new updates to install every 4 to 5 weeks, having computers off during that time is going to make it harder to meet the 95-95 goal.

Back to top

Selecting metrics:

Now that we have a target, we need to make sure we select the right metric to monitor the estate, and flag any problems that could be unrelated to normal time off.

I started with 2 key metrics: count of computers inactive for more than 7 days, and count of computers inactive for more than 17 days.

I have selected 7 days because it ensure we do not capture short time-off: for example with a 5 days value you would catch when someone turns off their computer on a Friday night and returns to work on the following Thursday. And it isn't too long, so we still get to see those period of inactivity.

The upper threshold is set to 17 days, so we catch any computers that have been inactive for more than a 2 weeks holiday. This give us a bracket (7 to 17 days) to calculate the count of computers out of office for 1 to 2 weeks.

I have also added to element that I found interesting: computers added or removed from the 7 days+ pool. This will make for the most interesting part of the implementation below :D.

So here is the summary of those metrics to gather:

Metric name Description
Managed computers Count of managed computers in the SMP database.
7 days Count of computers inactive for more than 7 days
17 days Count of computers inactive for more than 17 days
7 days ++ Count of computers added to the "7 days" count. These are computers that were not inactive in the previous record (t -1)
7 days -- Count of computers removed from the "7 days" count. These are computers that were inactive at (t -1) and that are not currently inactive i.e. they are back to Active!

Back to top

Implementation:

With some metrics selected making sure we get accurate data came next. So I started with the SQL query that brought in the questions about inactive computers: looking at the Evt_AeX_SWD_Execution and Evt_NS_Event_History tables I could see how many computers had reported data in the last 24 hours, or n days.

Joining in the Evt_NS_Client_Config_Request I had the same result set - so the computers requesting policies were also sending data - and the opposite pointed to computers being inactive from an agent standpoint.

Finally I checked the result against the data provided by running spGetComputersToPurge and it came clear that the data matched - so I decided to rely on this procedure content (I extracted the SQL) to list inactive computers for more than 7 and 17 days.

This is done with the following code:

set @inactive_1 = (
	select count(distinct(c.Guid))
	  from RM_ResourceComputer c
	 INNER JOIN
		(
		select [ResourceGuid]
		  from dbo.ResourceUpdateSummary
		 where InventoryClassGuid = '9E6F402A-6A45-4CBA-9299-C2323F73A506' 		
		 group by [ResourceGuid]
		having max([ModifiedDate]) < GETDATE() - 7
		 ) as dt 
		ON c.Guid = dt.ResourceGuid	
	 where c.IsManaged = 1
	)
set @inactive_2 = (
	select count(distinct(c.Guid))
	  from RM_ResourceComputer c
	 INNER JOIN
		(
		select [ResourceGuid]
		  from dbo.ResourceUpdateSummary
		 where InventoryClassGuid = '9E6F402A-6A45-4CBA-9299-C2323F73A506' 		
		 group by [ResourceGuid]
		having max([ModifiedDate]) < GETDATE() - 17
		 ) as dt 
		ON c.Guid = dt.ResourceGuid	
	 where c.IsManaged = 1
	)

But getting a count of inactive computers for 7 or 17 days is not sufficient to understand what is happening in the environment. If on a given day 200 computers are added to the inactive count and 195 are removed then our records will only show a delta of 5 computers. We'll have no way of calculating the churn rate - which is very important in relation to Patch Management.

Populating the "7 days ++" and "7-days --" metrics we need to compare the inactive computers dataset between the current and previous recording. As such we'll create 2 tables to store the current and previous computer guids:

if not exists (select 1 from sys.objects where type = 'u' and name = 'TREND_InactiveComputer_Current')
begin
	CREATE TABLE [TREND_InactiveComputer_Current] (guid uniqueidentifier not null, _exec_time datetime not null)
	CREATE UNIQUE CLUSTERED INDEX [IX_TREND_InactiveComputer_Current] ON [dbo].[TREND_InactiveComputer_Current] 
		(
			[Guid] ASC
	)
end

if not exists (select 1 from sys.objects where type = 'u' and name = 'TREND_InactiveComputer_Previous')
begin
	CREATE TABLE [TREND_InactiveComputer_Previous] (guid uniqueidentifier not null, _exec_time datetime not null)
	CREATE UNIQUE CLUSTERED INDEX [IX_TREND_InactiveComputer_Previous] ON [dbo].[TREND_InactiveComputer_Previous] 
		(
			[Guid] ASC
	)
end

We populate the 2 tables in this manner:

truncate table TREND_InactiveComputer_Previous
insert TREND_InactiveComputer_Previous (guid, _exec_time)
select * from TREND_InactiveComputer_Current

-- STAGE 2: Insert current data in the current table
truncate table TREND_InactiveComputer_Current
insert TREND_InactiveComputer_Current (guid, _exec_time)
select distinct(c.Guid) as 'Guid', getdate()
  from RM_ResourceComputer c
 INNER JOIN
	(
	select [ResourceGuid]
	  from dbo.ResourceUpdateSummary
	 where InventoryClassGuid = '9E6F402A-6A45-4CBA-9299-C2323F73A506' 		
	 group by [ResourceGuid]
	having max([ModifiedDate]) < GETDATE() - 7
	 ) as dt 
    ON c.Guid = dt.ResourceGuid	
 where c.IsManaged = 1

And then we calculate the added and removed computer counts:

declare @added as int, @removed as int
		 -- Added in c
			 set @added = (
					select count(*)
					  from TREND_InactiveComputer_Current c
					  full join TREND_InactiveComputer_Previous p
						on p.guid = c.guid
					 where p.guid is null
			)

			-- Removed in c
			 set @removed = (
					select count(*)
					  from TREND_InactiveComputer_Current c
					  full join TREND_InactiveComputer_Previous p
						on p.guid = c.guid
					 where c.guid is null
			)

We also need a trending table to store the daily statistics for later use. It is defined here:

if not exists (select 1 from sys.objects where type = 'u' and name = 'TREND_InactiveComputerCounts')
begin
	create table TREND_InactiveComputerCounts (
		[_exec_id] int not null,
		[timestamp] datetime not null,
		[Managed machines] int not null,
		[Inactive computers (7 days)] int not null,
		[New Inactive computers] int not null,
		[New Active computers] int not null,
		[Inactive computers (17 days)] int not null
	)
end

And we populate in this manner:

declare @execid as int
     set @execid = (select isnull(max(_exec_id), 0) from TREND_InactiveComputerCounts) + 1

insert TREND_InactiveComputerCounts (_exec_id, timestamp, [Managed machines], [inactive computers (7 days)], [New Inactive Computers], [New Active Computers], [Inactive Computers (17 days)])
values (@execid, getdate(), @managed, @inactive_1, @added, @removed, @inactive_2)

To wrap up the different task in a self contained process run the code in this manner:

if (current table is not empty) {
    truncate previous table
    insert current data into previous
    truncate current table
    insert query results into current
    count added
    count removed
    count managed machines
    count inactive 7-days
    count inactive 17-days
    insert data into trending table
} else {
    insert query results into current
}

Back to top

And here is the full code for the procedure spTrendInactiveComputers:

create procedure spTrendInactiveComputers
	@force as int = 0
as
if not exists (select 1 from sys.objects where type = 'u' and name = 'TREND_InactiveComputerCounts')
begin
	create table TREND_InactiveComputerCounts (
		[_exec_id] int not null,
		[timestamp] datetime not null,
		[Managed machines] int not null,
		[Inactive computers (7 days)] int not null,
		[New Inactive computers] int not null,
		[New Active computers] int not null,
		[Inactive computers (17 days)] int not null
	)
end

if not exists (select 1 from sys.objects where type = 'u' and name = 'TREND_InactiveComputer_Current')
begin
	CREATE TABLE [TREND_InactiveComputer_Current] (guid uniqueidentifier not null, _exec_time datetime not null)
	CREATE UNIQUE CLUSTERED INDEX [IX_TREND_InactiveComputer_Current] ON [dbo].[TREND_InactiveComputer_Current] 
		(
			[Guid] ASC
	)
end

if not exists (select 1 from sys.objects where type = 'u' and name = 'TREND_InactiveComputer_Previous')
begin
	CREATE TABLE [TREND_InactiveComputer_Previous] (guid uniqueidentifier not null, _exec_time datetime not null)
	CREATE UNIQUE CLUSTERED INDEX [IX_TREND_InactiveComputer_Previous] ON [dbo].[TREND_InactiveComputer_Previous] 
		(
			[Guid] ASC
	)
end

if ((select MAX(_exec_time) from TREND_InactiveComputer_Current where _exec_time >  dateadd(hour, -23, getdate())) is null) or (@force = 1)
begin
	-- STAGE 1: If we have current data, save it in the _previous table
	if (select count (*) from TREND_InactiveComputer_Current) > 0
		begin
			truncate table TREND_InactiveComputer_Previous
			insert TREND_InactiveComputer_Previous (guid, _exec_time)
			select * from TREND_InactiveComputer_Current

		-- STAGE 2: Insert current data in the current table
		truncate table TREND_InactiveComputer_Current
		insert TREND_InactiveComputer_Current (guid, _exec_time)
		select distinct(c.Guid) as 'Guid', getdate()
		  from RM_ResourceComputer c
		 INNER JOIN
			(
			select [ResourceGuid]
			  from dbo.ResourceUpdateSummary
			 where InventoryClassGuid = '9E6F402A-6A45-4CBA-9299-C2323F73A506' 		
			 group by [ResourceGuid]
			having max([ModifiedDate]) < GETDATE() - 7
			 ) as dt 
			ON c.Guid = dt.ResourceGuid	
		 where c.IsManaged = 1

		 --STAGE 3: Extract the add/drop counts and insert data in the trending table
		 declare @added as int, @removed as int
		 -- Added in c
			 set @added = (
					select count(*)
					  from TREND_InactiveComputer_Current c
					  full join TREND_InactiveComputer_Previous p
						on p.guid = c.guid
					 where p.guid is null
			)

			-- Removed in c
			 set @removed = (
					select count(*)
					  from TREND_InactiveComputer_Current c
					  full join TREND_InactiveComputer_Previous p
						on p.guid = c.guid
					 where c.guid is null
			)

		declare @managed as int, @inactive_1 as int, @inactive_2 as int
		set @managed = (select count(distinct(Guid)) from RM_ResourceComputer where IsManaged = 1)
		set @inactive_1 = (
			select count(distinct(c.Guid))
			  from RM_ResourceComputer c
			 INNER JOIN
				(
				select [ResourceGuid]
				  from dbo.ResourceUpdateSummary
				 where InventoryClassGuid = '9E6F402A-6A45-4CBA-9299-C2323F73A506' 		
				 group by [ResourceGuid]
				having max([ModifiedDate]) < GETDATE() - 7
				 ) as dt 
				ON c.Guid = dt.ResourceGuid	
			 where c.IsManaged = 1
		)
		set @inactive_2 = (
			select count(distinct(c.Guid))
			  from RM_ResourceComputer c
			 INNER JOIN
				(
				select [ResourceGuid]
				  from dbo.ResourceUpdateSummary
				 where InventoryClassGuid = '9E6F402A-6A45-4CBA-9299-C2323F73A506' 		
				 group by [ResourceGuid]
				having max([ModifiedDate]) < GETDATE() - 17
				 ) as dt 
				ON c.Guid = dt.ResourceGuid	
			 where c.IsManaged = 1
		)
		declare @execid as int
			set @execid = (select isnull(max(_exec_id), 0) from TREND_InactiveComputerCounts) + 1

		insert TREND_InactiveComputerCounts (_exec_id, timestamp, [Managed machines], [inactive computers (7 days)], [New Inactive Computers], [New Active Computers], [Inactive Computers (17 days)])
		values (@execid, getdate(), @managed, @inactive_1, @added, @removed, @inactive_2)
	end
	else
	begin
		truncate table TREND_InactiveComputer_Current
		insert TREND_InactiveComputer_Current (guid, _exec_time)
		select distinct(c.Guid) as 'Guid', getdate()
		  from RM_ResourceComputer c
		 INNER JOIN
			(
			select [ResourceGuid]
			  from dbo.ResourceUpdateSummary
			 where InventoryClassGuid = '9E6F402A-6A45-4CBA-9299-C2323F73A506' 		
			 group by [ResourceGuid]
			having max([ModifiedDate]) < GETDATE() - 7
			 ) as dt 
			ON c.Guid = dt.ResourceGuid	
		 where c.IsManaged = 1
	end
end

select * from TREND_InactiveComputerCounts order by _exec_id desc

Back to top

References

[1] {CWoc} Patch Trending SiteBuilder

[2] {CWoC} Patch trending stored procedures

Back to top