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.
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.
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! |
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 }
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
References
[1]{CWoc} Patch Trending SiteBuilder