View Advanced System Information on a Remote Computer

March 23, 2017

ניתן להשיג את הקוד המלא כאן

 

 

 

השבוע ביקרתי אצל לקוח, שהתלונן שבסביבת הSharepoint המשתמשים מרגישים איטיות.

הסביבה שעליה יושב השרת(ים)  הינה סביבת SQL Cluster וגרסת SQL Server 2008 Ent.

הסביבה הינה וירטואלית. – VMware. כלומר, מספר מכונות יושבות על אותה הפלטפורמה- ESX (השרת הפיסי aka הברזלים)

בפועל VM יכול לשרת על אותו ESX(הברזל עצמו) או אחר באותה פלטפורמה – תלוי הגדרות.

בחזרה ללקוח, 2 המכונות שישבו תחת SQL Cluster.

Node1 – ישב על ESX שמכיל CPU ב 2.0Ghz

Node2 – ישב על ESX שמכיל CPU ב 3.0Ghz

Node2 היה אקטיבי במשך תקופה ארוכה, באחת מפעולות התחזוקה הוחלף הNode הפעיל לNode1. אז החלו להגיע תלונות רבות על איטיות.

לאחר זמן מה גילינו את מה שקרה.

החזרנו את הNode הפעיל לNode2 וביצענו מיגרציה(VMotion) לNode1 לESX בעל פלטפורמה זהה.

המקרה העלה חשש אצל הלקוח, שיכול להיות פוטנציאל בעייתי בעוד שרתים בארגון שמכילים SQL.

הבקשה של הלקוח הייתה למצוא דרך שאילתת T-SQL האם הCPU של הNodes זהים במקרים הבאים –

  1. SQL Server Failover Cluster

  2. Mirror

  3. Always on.

הסביבה אצל הלקוח מכילה שרתי SQL מ2005 ועד 2012 ומגוון גרסאות מערכות הפעלה Windows Server 2003 ומעלה.

קרוב לוודאי שמשימה כזאת לא הכי נכונה לביצוע מתוך SQL, אבל אהבתי את האתגר.

כמה פתרונות אפשריים עלו לי בראש:

  1. דרך SQL עצמו בעזרת  – Linked Server או SQLCMD – אך במקרה של SQL Cluster הNode המשני אינו זמין.

  2. PowerShell – אולי זאת הדרך הנכונה, אבל נתקלתי בבעיה של מספר פלטפורמות. לגרסאות הwindows   השונות קיים גרסת PS שונה וכך גם המטודות שונות זו מזו.

לדוגמא, הסקריפט שכתבתי יעבוד ברוב המכונות העדכניות. אך, מכונות בעלות מערכת הפעלה windows server 2003 ומטה, הקוד לא ייתמך – בגלל גרסאות שונות של PowerShell .

 

DECLARE @cmd VARCHAR(4000);

DECLARE @RemoteServer VARCHAR(4000) ;

DECLARE @output TABLE (line varchar(4000));

SELECT @RemoteServer = NodeName

FROM   sys.dm_os_cluster_nodes

WHERE  NodeName != @@servername

if @RemoteServer is null

SELECT @RemoteServer = replica_server_name

from   sys.availability_replicas

where  replica_server_name != @@servername

 

--set @RemoteServer 'print-srv'

SET @cmd = 'powershell.exe -noprofile -command "$servername = ''' + @RemoteServer + '''; invoke-command -computer $servername -scriptblock {[array]$wmiinfo = Get-WmiObject Win32_Processor; $cpu = ($wmiinfo[0].name);  $cores = ( $wmiinfo | Select SocketDesignation | Measure-Object ).count;  $NumberOfLogicalProcessors = ( $wmiinfo[0].NumberOfLogicalProcessors); $obj = New-Object Object; $obj | Add-Member Noteproperty Servername -value $servername; $obj | Add-Member Noteproperty CPU -value $cpu; $obj | Add-Member Noteproperty Cores -value $cores; $obj | Add-Member Noteproperty NumberOfLogicalProcessors -value $NumberOfLogicalProcessors; Write-Host ($obj | Format-List | Out-String);}"';

 

INSERT @output

EXEC xp_cmdshell @cmd;

IF exists(select top 1 1 from @output WHERE     line like '%is not recognized as an internal or external command%')

begin

       delete from @output;

       SET @cmd = '%SYSTEMROOT%\System32\WindowsPowerShell\v1.0\powershell.exe -noprofile -command "$servername = ''' + @RemoteServer + '''; invoke-command -computer $servername -scriptblock {[array]$wmiinfo = Get-WmiObject Win32_Processor; $cpu = ($wmiinfo[0].name);  $cores = ( $wmiinfo | Select SocketDesignation | Measure-Object ).count;  $NumberOfLogicalProcessors = ( $wmiinfo[0].NumberOfLogicalProcessors); $obj = New-Object Object; $obj | Add-Member Noteproperty Servername -value $servername; $obj | Add-Member Noteproperty CPU -value $cpu; $obj | Add-Member Noteproperty Cores -value $cores; $obj | Add-Member Noteproperty NumberOfLogicalProcessors -value $NumberOfLogicalProcessors; Write-Host ($obj | Format-List | Out-String);}"';

 

       INSERT @output

       EXEC xp_cmdshell @cmd;

 

end

 

SELECT LEFT(line,CHARINDEX(' ',line)),RIGHT(line,CASE CHARINDEX(':',REVERSE(line)) WHEN 0 THEN 0 ELSE CHARINDEX(':',REVERSE(line))-1 END)

FROM   @output

WHERE  line IS NOT NULL;

 

  1. Clr – מכיוון שהשרתים מפלטפורמה 2005 – 2012, מראש עלי להכין 2 סוגי מסדי נתונים שיתמכו ב2 CLR, האחד לגרסאת .Net 2.0 לתתמוך לשרתים עד 2008R2 ו.Net 3.5 לשרתים 2012 ומעלה.

  2. ניראה לי הרפתקאה שלא כדי להיכנס בשלב זה.

  3. xp_cmdshell

 

פתרון יחסית קל לשימוש נין ליישם דרך הcmd

 

מצאתי את הכלי הבא -

 

כאשר אני זורק את הקבצים בשרת המתשאל(המקור) ורושם את הפקודה הבאה –

אני אוכל לדעת מה סוג הCPU של השרת אותו תשאלתי –

@" Usaage: RunRemote <\\server> Process <remote.exe> [arg1 arg2 ...]"

RunRemote \\xxx Process cmd /C wmic cpu get name

 

 אך, הרעיון קצת יותר מורכב,

הלקוח לא מעוניין בתלות הקבצים וזיכרון המפעיל לזכור לשים את הקבצים בכל שרת.

יש מסד נתונים ניהולי והדרך היותר עדיפה היא, לשמור את הקבצים בטבלה ובהרצת פרוצדורה לזרוק אותם לנתיב מסוים – האם ניסיתם לבצע את זה פעם?

סקריפט לטעינת הקבצים-

CREATE TABLE [dbo].[Files](

           [FileID] [int] IDENTITY(1,1) NOT NULL,

[FileContentType] [varchar](50) NOT NULL,

[FileName] [varchar](150) NOT NULL,

           [Content] [varbinary](max) NOT NULL

) ON [PRIMARY]

 

 

insert [dbo].[Files]

 Select 'dll', 'CodeProject.Remoting.Common.dll',(SELECT * FROM OPENROWSET(BULK N'D:\tmp\Release_files\CodeProject.Remoting.Common.dll', SINGLE_BLOB) AS document) document

go

insert [dbo].[Files]

 Select 'dll', 'ICSharpCode.SharpZipLib.dll',(SELECT * FROM OPENROWSET(BULK N'D:\tmp\Release_files\ICSharpCode.SharpZipLib.dll', SINGLE_BLOB) AS document) document

go

insert [dbo].[Files]

 Select 'exe', 'Process.exe',(SELECT * FROM OPENROWSET(BULK N'D:\tmp\Release_files\Process.exe', SINGLE_BLOB) AS document) document

go

insert [dbo].[Files]

 Select 'exe', 'RunRemote.exe',(SELECT * FROM OPENROWSET(BULK N'D:\tmp\Release_files\RunRemote.exe', SINGLE_BLOB) AS document) document

go

insert [dbo].[Files]

 Select 'exe', 'ServiceOnRemoteMachine.exe',(SELECT * FROM OPENROWSET(BULK N'D:\tmp\Release_files\ServiceOnRemoteMachine.exe', SINGLE_BLOB) AS document) document

go

טעינת קבצים לטבלה עברה בהצלחה.

סקריפט לשליפת הקבצים מתוך הטבלה –

דרך א - BCP

DECLARE @SQLcommand varchar(8000)

set @SQLcommand = 'bcp "SELECT Content FROM [xxx].[dbo].[Files]" queryout "D:\CodeProject.Remoting.Common.dll" -T -n -S "SQLNODE1"'

אך, התוצאה לא משביעה רצון

 

דרך ב – Ole Automation Procedures

sp_configure 'show advanced options', 1

GO

RECONFIGURE;

GO

sp_configure 'Ole Automation Procedures', 1

GO

RECONFIGURE;

GO

DECLARE @SQLIMG VARCHAR(MAX) ,

              @DOC_PATH VARCHAR(50) = 'D:\' ,

              @TIMESTAMP VARCHAR(MAX) ,

              @ObjectToken INT ,

              @FILENAME VARCHAR(50);

SET @TIMESTAMP = @DOC_PATH + 'CodeProject.Remoting.Common.dll';

SELECT  @FILENAME = DocData

FROM    [dbo].[Files];

    

 

EXEC sp_OACreate 'ADODB.Stream', @ObjectToken OUTPUT

EXEC sp_OASetProperty @ObjectToken, 'Type', 1

EXEC sp_OAMethod @ObjectToken, 'Open'

EXEC sp_OAMethod @ObjectToken, 'Write', NULL, @FILENAME --new variable here

EXEC sp_OAMethod @ObjectToken, 'SaveToFile', NULL, @TIMESTAMP, 2

EXEC sp_OAMethod @ObjectToken, 'Close'

EXEC sp_OADestroy @ObjectToken

דרך ג - CLR

פה, נעזרתי בclr שניתן להשיג מכאן

 

 

כזכור לנו מהפסקה מעלה, תחילה לא רציתי להשתמש בclr בגלל קימפול ב2 סוגי .Net. 

אך, יחסית פה המקרה הוא דיי פשוט.

ניתן לקחת את הקוד שמסופק מהמאמר, ולהכניס לקובץ טקסט בעל השם SqlFileProcessor.cs ומפה זה כבר דיי פשוט לקמפל לאיזה גרסה של .Net בדרך הבאה– בעזרת CMD

על מנת לקמפל את הקוד ל.Net 2.0 -

C:\Windows\Microsoft.NET\Framework\v2.0.50727\csc.exe /target:library /out:C:\temp\SqlFileProcessor.dll C:\temp\SqlFileProcessor.cs

על מנת לקמפל את הקוד ל.Net 3.5 -

C:\Windows\Microsoft.NET\Framework\v3.5\csc.exe /target:library /out:C:\temp\SqlFileProcessor.dll C:\temp\SqlFileProcessor.cs

מכאן הטעינה יחסית זהה –

 

USE <DB_Name>;

GO

ALTER DATABASE <DB_Name> SET TRUSTWORTHY ON;

GO

EXEC sp_changedbowner 'sa'

go

EXEC sp_configure 'clr enabled',1 ;

GO

RECONFIGURE

GO

CREATE ASSEMBLY SqlFileProcessor

 from 'C:\SqlFileProcessor.dll'

 WITH PERMISSION_SET =EXTERNAL_ACCESS;

GO

CREATE Function [Utility].[ufn_clr_GetBytesFromFile]( @FileName NVarchar(128)) RETURNs varbinary(max)

 AS EXTERNAL NAME SqlFileProcessor.FileProcessor.GetBytesFromFile;

GO

CREATE Function [Utility].[ufn_clr_SaveFileToDisk]( @sFileName Nvarchar(128), @Blob varbinary(max))Returns int

 AS EXTERNAL NAME SqlFileProcessor.FileProcessor.SaveFileToDisk

GO

 

הקוד לטובת התשאול המלא יראה כך –

 

 

DECLARE @RemoteServer VARCHAR(4000) ;

DECLARE @output TABLE (line varchar(4000));

DECLARE @RemoteProp TABLE([Server] sysname NOT NULL,Property VARCHAR(4000),[Value] varchar(4000));

DECLARE @cmdShell VARCHAR(4000);

DECLARE @PathcmdFile NVARCHAR(2048);

IF OBJECT_ID('tempdb..#Nodes') IS NOT NULL

       DROP TABLE #Nodes;

       CREATE TABLE #Nodes(NodeName sysname,[Type] sysname);

IF OBJECT_ID('tempdb..#RemoteServer') IS NOT NULL

       DROP TABLE #RemoteServer;

       CREATE TABLE #RemoteServer([Server] sysname NOT NULL,Property VARCHAR(4000),[Value] varchar(4000));

      

       INSERT #Nodes

       SELECT NodeName,'SQL Cluster' [Type]

       FROM   sys.dm_os_cluster_nodes;

 

       if SERVERPROPERTY('IsHadrEnabled') = 1

              EXEC('INSERT    #Nodes

SELECT member_name,''AlwaysOn''

FROM   sys.dm_hadr_cluster_members');

 

DECLARE @NoOutput TABLE (ID INT);

DECLARE @files TABLE (ID int IDENTITY, FileName varchar(100))

DECLARE cuNode CURSOR LOCAL FAST_FORWARD READ_ONLY FOR

SELECT NodeName

FROM   #Nodes

OPEN cuNode

 

FETCH NEXT FROM cuNode INTO @RemoteServer

 

WHILE @@FETCH_STATUS = 0

BEGIN

       DELETE FROM @output;

       DELETE FROM @RemoteProp;

    --By powershell

       PRINT'By powershell'

       SET @cmdShell = 'powershell.exe -noprofile -command "$servername = ''' + @RemoteServer + '''; invoke-command -computer $servername -scriptblock {[array]$wmiinfo = Get-WmiObject Win32_Processor; $cpu = ($wmiinfo[0].name);  $cores = ( $wmiinfo | Select SocketDesignation | Measure-Object ).count;  $NumberOfLogicalProcessors = ( $wmiinfo[0].NumberOfLogicalProcessors); $obj = New-Object Object; $obj | Add-Member Noteproperty CPU -value $cpu; $obj | Add-Member Noteproperty Cores -value $cores; $obj | Add-Member Noteproperty NumberOfLogicalProcessors -value $NumberOfLogicalProcessors; Write-Host ($obj | Format-List | Out-String);}"';

 

       INSERT @output

       EXEC master.sys.xp_cmdshell @cmdShell;

       IF exists(select top 1 1 from @output WHERE line like '%is not recognized as an internal or external command%')

       begin

                 delete from @output;

                 SET @cmdShell = '%SYSTEMROOT%\System32\WindowsPowerShell\v1.0\powershell.exe -noprofile -command "$servername = ''' + @RemoteServer + '''; invoke-command -computer $servername -scriptblock {[array]$wmiinfo = Get-WmiObject Win32_Processor; $cpu = ($wmiinfo[0].name);  $cores = ( $wmiinfo | Select SocketDesignation | Measure-Object ).count;  $NumberOfLogicalProcessors = ( $wmiinfo[0].NumberOfLogicalProcessors); $obj = New-Object Object; $obj | Add-Member Noteproperty CPU -value $cpu; $obj | Add-Member Noteproperty Cores -value $cores; $obj | Add-Member Noteproperty NumberOfLogicalProcessors -value $NumberOfLogicalProcessors; Write-Host ($obj | Format-List | Out-String);}"';

 

                 INSERT @output

                 EXEC master.sys.xp_cmdshell @cmdShell;

 

       END

    IF NOT EXISTS(SELECT TOP 1 1 FROM @output WHERE line = 'The system cannot find the path specified.')

       INSERT @RemoteProp

       SELECT @RemoteServer [Server],

                     LEFT(line,CHARINDEX(' ',line))[Property],

                     RIGHT(line,CASE CHARINDEX(':',REVERSE(line)) WHEN 0 THEN 0 ELSE CHARINDEX(':',REVERSE(line))-1 END)[Value]

       FROM   @output

       WHERE  line IS NOT NULL ;

 

 

       --By cmdshell + clr

       IF NOT EXISTS(SELECT TOP 1 1 FROM @RemoteProp)

       BEGIN

              PRINT 'By cmdshell + clr';

              DELETE FROM @output;

              SET @PathcmdFile = N'C:\tmp\RemoteFilesCmd\';

              SET @cmdShell = 'dir "' + @PathcmdFile + '" /b';

              EXEC master.dbo.xp_create_subdir @PathcmdFile;

              INSERT INTO @files EXECUTE master.sys.xp_cmdshell @cmdShell;

 

              INSERT @NoOutput

              SELECT [Utility].[ufn_clr_SaveFileToDisk] (@PathcmdFile + PF.FileName ,Content)

              FROM   dbo.Files PF

                           LEFT JOIN @files DF ON DF.FileName = PF.[FileName]

              WHERE  DF.ID IS NULL;

 

              SET @cmdShell = @PathcmdFile + 'RunRemote \\' + @RemoteServer + ' Process cmd /C wmic cpu get name'

              DELETE FROM @output;

              INSERT @output

              EXEC master.sys.xp_cmdshell @cmdShell;

             

              INSERT @RemoteProp

              SELECT TOP 1 @RemoteServer [Server],'CPU' [Property],REPLACE(line,'Name                                      ','') [Value]

              FROM   @output                   

              WHERE  line NOT IN ('

','Return value = 0','Completed: cmd /C wmic cpu get name','Name')

              UNION ALL

              SELECT TOP 1 @RemoteServer [Server],'Cores' [Property],CONVERT(VARCHAR(4000),COUNT(1)) [Value]

              FROM   @output                   

              WHERE  line NOT IN ('

','Return value = 0','Completed: cmd /C wmic cpu get name','Name');

 

       END

      

       IF EXISTS(SELECT TOP 1 1 FROM @RemoteProp)

       BEGIN

              INSERT #RemoteServer

              SELECT Server ,

                Property ,

                Value

              FROM   @RemoteProp

       END

       DELETE FROM @RemoteProp;

    FETCH NEXT FROM cuNode INTO @RemoteServer;

END

 

CLOSE cuNode;

DEALLOCATE cuNode;

 

 

 

SELECT *

FROM   #RemoteServer

 

 

DROP TABLE #Nodes,#RemoteServer;

 

 

ניתן להשיג את הקוד המלא כאן

 

Please reload

Featured Posts

I'm busy working on my blog posts. Watch this space!

Please reload

Recent Posts

October 31, 2017

October 29, 2017

Please reload

Archive
Please reload

Search By Tags