mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-01-14 01:25:40 -05:00
Add AQL Assessment service (#946)
[SQL Assessment API](https://docs.microsoft.com/en-us/sql/sql-assessment-api/sql-assessment-api-overview) provides a mechanism to evaluate the configuration of SQL Server for best practices. SQL Assessment API gives a list of recommended actions to improve SQL Server performance or security. The SQL Assessment service is used by the expected SQL Assessment feature of Azure Data Studio. SqlAssessmentService forwards JSONRPC calls to SQL Assessment engine and wraps results as a response. `assessment/getAssessmentItems` returns a set of checks applicable to a given target. `assessment/invoke` returns a set of recommendations for improving SQL Server instance or database configurations. `assessment/generateScript` returns a T-SQL script for storing an assessment result set to a SQL data table.
This commit is contained in:
@@ -652,6 +652,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection
|
||||
public string OsVersion;
|
||||
|
||||
public string MachineName;
|
||||
public string ServerName;
|
||||
|
||||
public Dictionary<string, object> Options { get; set; }
|
||||
}
|
||||
@@ -666,6 +667,14 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection
|
||||
public int Port;
|
||||
}
|
||||
|
||||
public class ServerHostInfo
|
||||
{
|
||||
public string Platform;
|
||||
public string Distribution;
|
||||
public string Release;
|
||||
public string ServicePackLevel;
|
||||
}
|
||||
|
||||
public static bool TryGetServerVersion(string connectionString, out ServerInfo serverInfo, string azureAccountToken)
|
||||
{
|
||||
serverInfo = null;
|
||||
@@ -697,6 +706,37 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection
|
||||
azureAccountToken: azureAccountToken);
|
||||
|
||||
return serverInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the server host information from sys.dm_os_host_info view
|
||||
/// </summary>
|
||||
/// <param name="connection">The connection</param>
|
||||
public static ServerHostInfo GetServerHostInfo(IDbConnection connection)
|
||||
{
|
||||
// SQL Server 2016 and below does not provide sys.dm_os_host_info
|
||||
if (!Version.TryParse(ReadServerVersion(connection), out var hostVersion) || hostVersion.Major <= 13)
|
||||
{
|
||||
return new ServerHostInfo
|
||||
{
|
||||
Platform = "Windows"
|
||||
};
|
||||
}
|
||||
|
||||
var hostInfo = new ServerHostInfo();
|
||||
ExecuteReader(
|
||||
connection,
|
||||
SqlConnectionHelperScripts.GetHostInfo,
|
||||
reader =>
|
||||
{
|
||||
reader.Read();
|
||||
hostInfo.Platform = reader[0].ToString();
|
||||
hostInfo.Distribution = reader[1].ToString();
|
||||
hostInfo.Release = reader[2].ToString();
|
||||
hostInfo.ServicePackLevel = reader[3].ToString();
|
||||
});
|
||||
|
||||
return hostInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -729,11 +769,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection
|
||||
serverInfo.ServerLevel = reader[2].ToString();
|
||||
serverInfo.ServerEdition = reader[3].ToString();
|
||||
serverInfo.MachineName = reader[4].ToString();
|
||||
serverInfo.ServerName = reader[5].ToString();
|
||||
|
||||
if (reader.FieldCount > 5)
|
||||
if (reader.FieldCount > 6)
|
||||
{
|
||||
// Detect the presence of SXI
|
||||
serverInfo.IsSelectiveXmlIndexMetadataPresent = reader.GetInt32(5) == 1;
|
||||
serverInfo.IsSelectiveXmlIndexMetadataPresent = reader.GetInt32(6) == 1;
|
||||
}
|
||||
|
||||
// The 'ProductVersion' server property is of the form ##.#[#].####.#,
|
||||
|
||||
@@ -7,8 +7,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection
|
||||
{
|
||||
static class SqlConnectionHelperScripts
|
||||
{
|
||||
public const string EngineEdition = "SELECT SERVERPROPERTY('EngineEdition'), SERVERPROPERTY('productversion'), SERVERPROPERTY ('productlevel'), SERVERPROPERTY ('edition'), SERVERPROPERTY ('MachineName'), (SELECT CASE WHEN EXISTS (SELECT TOP 1 1 from [sys].[all_columns] WITH (NOLOCK) WHERE name = N'xml_index_type' AND OBJECT_ID(N'sys.xml_indexes') = object_id) THEN 1 ELSE 0 END AS SXI_PRESENT)";
|
||||
public const string EngineEditionWithLock = "SELECT SERVERPROPERTY('EngineEdition'), SERVERPROPERTY('productversion'), SERVERPROPERTY ('productlevel'), SERVERPROPERTY ('edition'), SERVERPROPERTY ('MachineName'), (SELECT CASE WHEN EXISTS (SELECT TOP 1 1 from [sys].[all_columns] WHERE name = N'xml_index_type' AND OBJECT_ID(N'sys.xml_indexes') = object_id) THEN 1 ELSE 0 END AS SXI_PRESENT)";
|
||||
public const string EngineEdition = "SELECT SERVERPROPERTY('EngineEdition'), SERVERPROPERTY('productversion'), SERVERPROPERTY ('productlevel'), SERVERPROPERTY ('edition'), SERVERPROPERTY ('MachineName'), SERVERPROPERTY ('ServerName'), (SELECT CASE WHEN EXISTS (SELECT TOP 1 1 from [sys].[all_columns] WITH (NOLOCK) WHERE name = N'xml_index_type' AND OBJECT_ID(N'sys.xml_indexes') = object_id) THEN 1 ELSE 0 END AS SXI_PRESENT)";
|
||||
public const string EngineEditionWithLock = "SELECT SERVERPROPERTY('EngineEdition'), SERVERPROPERTY('productversion'), SERVERPROPERTY ('productlevel'), SERVERPROPERTY ('edition'), SERVERPROPERTY ('MachineName'), SERVERPROPERTY ('ServerName'), (SELECT CASE WHEN EXISTS (SELECT TOP 1 1 from [sys].[all_columns] WHERE name = N'xml_index_type' AND OBJECT_ID(N'sys.xml_indexes') = object_id) THEN 1 ELSE 0 END AS SXI_PRESENT)";
|
||||
|
||||
public const string CheckDatabaseReadonly = @"EXEC sp_dboption '{0}', 'read only'";
|
||||
|
||||
@@ -48,5 +48,6 @@ SELECT @filepath AS FilePath
|
||||
|
||||
public const string GetOsVersion = @"SELECT OSVersion = RIGHT(@@version, LEN(@@version)- 3 -charindex (' on ', LOWER(@@version)))";
|
||||
public const string GetClusterEndpoints = @"SELECT [name], [description], [endpoint], [protocol_desc] FROM .[sys].[dm_cluster_endpoints];";
|
||||
public const string GetHostInfo = @"SELECT [host_platform], [host_distribution], [host_release], [host_service_pack_level], [host_sku], [os_language_version] FROM sys.dm_os_host_info";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ using Microsoft.SqlTools.ServiceLayer.QueryExecution;
|
||||
using Microsoft.SqlTools.ServiceLayer.SchemaCompare;
|
||||
using Microsoft.SqlTools.ServiceLayer.Scripting;
|
||||
using Microsoft.SqlTools.ServiceLayer.Security;
|
||||
using Microsoft.SqlTools.ServiceLayer.SqlAssessment;
|
||||
using Microsoft.SqlTools.ServiceLayer.SqlContext;
|
||||
using Microsoft.SqlTools.ServiceLayer.Workspace;
|
||||
|
||||
@@ -129,6 +130,9 @@ namespace Microsoft.SqlTools.ServiceLayer
|
||||
|
||||
ExternalLanguageService.Instance.InitializeService(serviceHost);
|
||||
serviceProvider.RegisterSingleService(ExternalLanguageService.Instance);
|
||||
|
||||
SqlAssessmentService.Instance.InitializeService(serviceHost);
|
||||
serviceProvider.RegisterSingleService(SqlAssessmentService.Instance);
|
||||
|
||||
InitializeHostedServices(serviceProvider, serviceHost);
|
||||
serviceHost.ServiceProvider = serviceProvider;
|
||||
|
||||
@@ -2981,6 +2981,30 @@ namespace Microsoft.SqlTools.ServiceLayer
|
||||
}
|
||||
}
|
||||
|
||||
public static string SqlAssessmentGenerateScriptTaskName
|
||||
{
|
||||
get
|
||||
{
|
||||
return Keys.GetString(Keys.SqlAssessmentGenerateScriptTaskName);
|
||||
}
|
||||
}
|
||||
|
||||
public static string SqlAssessmentQueryInvalidOwnerUri
|
||||
{
|
||||
get
|
||||
{
|
||||
return Keys.GetString(Keys.SqlAssessmentQueryInvalidOwnerUri);
|
||||
}
|
||||
}
|
||||
|
||||
public static string SqlAssessmentConnectingError
|
||||
{
|
||||
get
|
||||
{
|
||||
return Keys.GetString(Keys.SqlAssessmentConnectingError);
|
||||
}
|
||||
}
|
||||
|
||||
public static string ConnectionServiceListDbErrorNotConnected(string uri)
|
||||
{
|
||||
return Keys.GetString(Keys.ConnectionServiceListDbErrorNotConnected, uri);
|
||||
@@ -3156,6 +3180,11 @@ namespace Microsoft.SqlTools.ServiceLayer
|
||||
return Keys.GetString(Keys.ScheduleNameAlreadyExists, scheduleName);
|
||||
}
|
||||
|
||||
public static string SqlAssessmentUnsuppoertedEdition(int editionCode)
|
||||
{
|
||||
return Keys.GetString(Keys.SqlAssessmentUnsuppoertedEdition, editionCode);
|
||||
}
|
||||
|
||||
[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
public class Keys
|
||||
{
|
||||
@@ -4376,6 +4405,18 @@ namespace Microsoft.SqlTools.ServiceLayer
|
||||
public const string SchemaCompareSessionNotFound = "SchemaCompareSessionNotFound";
|
||||
|
||||
|
||||
public const string SqlAssessmentGenerateScriptTaskName = "SqlAssessmentGenerateScriptTaskName";
|
||||
|
||||
|
||||
public const string SqlAssessmentQueryInvalidOwnerUri = "SqlAssessmentQueryInvalidOwnerUri";
|
||||
|
||||
|
||||
public const string SqlAssessmentConnectingError = "SqlAssessmentConnectingError";
|
||||
|
||||
|
||||
public const string SqlAssessmentUnsuppoertedEdition = "SqlAssessmentUnsuppoertedEdition";
|
||||
|
||||
|
||||
private Keys()
|
||||
{ }
|
||||
|
||||
|
||||
@@ -1768,4 +1768,21 @@
|
||||
<value>Could not find the schema compare session to cancel</value>
|
||||
<comment></comment>
|
||||
</data>
|
||||
<data name="SqlAssessmentGenerateScriptTaskName" xml:space="preserve">
|
||||
<value>Generate SQL Assessment script</value>
|
||||
<comment></comment>
|
||||
</data>
|
||||
<data name="SqlAssessmentQueryInvalidOwnerUri" xml:space="preserve">
|
||||
<value>Not connected to a server</value>
|
||||
<comment></comment>
|
||||
</data>
|
||||
<data name="SqlAssessmentConnectingError" xml:space="preserve">
|
||||
<value>Cannot connect to the server</value>
|
||||
<comment></comment>
|
||||
</data>
|
||||
<data name="SqlAssessmentUnsuppoertedEdition" xml:space="preserve">
|
||||
<value>Unsupported engine edition {0}</value>
|
||||
<comment>.
|
||||
Parameters: 0 - editionCode (int) </comment>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -816,4 +816,12 @@ ExtractInvalidVersion = Invalid version '{0}' passed. Version must be in the for
|
||||
PublishChangesTaskName = Apply schema compare changes
|
||||
SchemaCompareExcludeIncludeNodeNotFound = Failed to find the specified change in the model
|
||||
OpenScmpConnectionBasedModelParsingError = Error encountered while trying to parse connection information for endpoint '{0}' with error message '{1}'
|
||||
SchemaCompareSessionNotFound = Could not find the schema compare session to cancel
|
||||
SchemaCompareSessionNotFound = Could not find the schema compare session to cancel
|
||||
|
||||
############################################################################
|
||||
# SQL Assessment
|
||||
|
||||
SqlAssessmentGenerateScriptTaskName = Generate SQL Assessment script
|
||||
SqlAssessmentQueryInvalidOwnerUri = Not connected to a server
|
||||
SqlAssessmentConnectingError = Cannot connect to the server
|
||||
SqlAssessmentUnsuppoertedEdition(int editionCode) = Unsupported engine edition {0}
|
||||
@@ -2056,6 +2056,32 @@
|
||||
<target state="new">Encountered unsupported token {0}</target>
|
||||
<note></note>
|
||||
</trans-unit>
|
||||
<trans-unit id="SqlAssessmentOperationExecuteCalledTwice">
|
||||
<source>A SQL Assessment operation's Execute method should not be called more than once</source>
|
||||
<target state="new">A SQL Assessment operation's Execute method should not be called more than once</target>
|
||||
<note></note>
|
||||
</trans-unit>
|
||||
<trans-unit id="SqlAssessmentGenerateScriptTaskName">
|
||||
<source>Generate SQL Assessment script</source>
|
||||
<target state="new">Generate SQL Assessment script</target>
|
||||
<note></note>
|
||||
</trans-unit>
|
||||
<trans-unit id="SqlAssessmentQueryInvalidOwnerUri">
|
||||
<source>Not connected to a server</source>
|
||||
<target state="new">Not connected to a server</target>
|
||||
<note></note>
|
||||
</trans-unit>
|
||||
<trans-unit id="SqlAssessmentConnectingError">
|
||||
<source>Cannot connect to the server</source>
|
||||
<target state="new">Cannot connect to the server</target>
|
||||
<note></note>
|
||||
</trans-unit>
|
||||
<trans-unit id="SqlAssessmentUnsuppoertedEdition">
|
||||
<source>Unsupported engine edition {0}</source>
|
||||
<target state="new">Unsupported engine edition {0}</target>
|
||||
<note>.
|
||||
Parameters: 0 - editionCode (int) </note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -0,0 +1,129 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Microsoft.SqlServer.Management.Assessment;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters for executing a query from a provided string
|
||||
/// </summary>
|
||||
public class AssessmentParams
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the owner uri to get connection from
|
||||
/// </summary>
|
||||
public string OwnerUri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the target type
|
||||
/// </summary>
|
||||
public SqlObjectType TargetType { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes an item returned by SQL Assessment RPC methods
|
||||
/// </summary>
|
||||
public class AssessmentItemInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets assessment ruleset version.
|
||||
/// </summary>
|
||||
public string RulesetVersion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets assessment ruleset name
|
||||
/// </summary>
|
||||
public string RulesetName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets assessed target's type.
|
||||
/// Supported values: 1 - server, 2 - database.
|
||||
/// </summary>
|
||||
public SqlObjectType TargetType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the assessed object's name.
|
||||
/// </summary>
|
||||
public string TargetName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets check's ID.
|
||||
/// </summary>
|
||||
public string CheckId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets tags assigned to this item.
|
||||
/// </summary>
|
||||
public string[] Tags { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a display name for this item.
|
||||
/// </summary>
|
||||
public string DisplayName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a brief description of the item's purpose.
|
||||
/// </summary>
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a <see cref="string"/> containing
|
||||
/// an link to a page providing detailed explanation
|
||||
/// of the best practice.
|
||||
/// </summary>
|
||||
public string HelpLink { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a <see cref="string"/> indicating
|
||||
/// severity level assigned to this items.
|
||||
/// Values are: "Information", "Warning", "Critical".
|
||||
/// </summary>
|
||||
public string Level { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic SQL Assessment Result
|
||||
/// </summary>
|
||||
/// <typeparam name="T">
|
||||
/// Result item's type derived from <see cref="AssessmentItemInfo"/>
|
||||
/// </typeparam>
|
||||
public class AssessmentResultData<T>
|
||||
where T : AssessmentItemInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the collection of assessment results.
|
||||
/// </summary>
|
||||
public List<T> Items { get; } = new List<T>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets SQL Assessment API version.
|
||||
/// </summary>
|
||||
public string ApiVersion { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic SQL Assessment Result
|
||||
/// </summary>
|
||||
/// <typeparam name="T">
|
||||
/// Result item's type derived from <see cref="AssessmentItemInfo"/>
|
||||
/// </typeparam>
|
||||
public class AssessmentResult<T> : AssessmentResultData<T>
|
||||
where T : AssessmentItemInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating
|
||||
/// if assessment operation was successful.
|
||||
/// </summary>
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an status message for the operation.
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
|
||||
using Microsoft.SqlTools.ServiceLayer.TaskServices;
|
||||
using Microsoft.SqlTools.ServiceLayer.Utility;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters for executing a query from a provided string
|
||||
/// </summary>
|
||||
public class GenerateScriptParams
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a list of assessment result items
|
||||
/// to be written to a table
|
||||
/// </summary>
|
||||
public List<AssessmentResultItem> Items { get; set; }
|
||||
|
||||
public TaskExecutionMode TaskExecutionMode { get; set; }
|
||||
|
||||
public string TargetServerName { get; set; }
|
||||
|
||||
public string TargetDatabaseName { get; set; }
|
||||
}
|
||||
|
||||
public class GenerateScriptResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating
|
||||
/// if assessment operation was successful
|
||||
/// </summary>
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an status message for the operation
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets script text
|
||||
/// </summary>
|
||||
public string Script { get; set; }
|
||||
}
|
||||
|
||||
public class GenerateScriptRequest
|
||||
{
|
||||
public static readonly
|
||||
RequestType<GenerateScriptParams, ResultStatus> Type =
|
||||
RequestType<GenerateScriptParams, ResultStatus>.Create("assessment/generateScript");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters for executing a query from a provided string
|
||||
/// </summary>
|
||||
public class GetAssessmentItemsParams : AssessmentParams
|
||||
{
|
||||
// a placeholder for future specialization
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes a check used to assess SQL Server objects.
|
||||
/// </summary>
|
||||
public class CheckInfo : AssessmentItemInfo
|
||||
{
|
||||
// a placeholder for future specialization
|
||||
}
|
||||
|
||||
|
||||
public class GetAssessmentItemsRequest
|
||||
{
|
||||
public static readonly RequestType<GetAssessmentItemsParams, AssessmentResult<CheckInfo>> Type =
|
||||
RequestType<GetAssessmentItemsParams, AssessmentResult<CheckInfo>>.Create(
|
||||
"assessment/getAssessmentItems");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System;
|
||||
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
|
||||
using Microsoft.SqlTools.ServiceLayer.Utility;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters for executing a query from a provided string
|
||||
/// </summary>
|
||||
public class InvokeParams : AssessmentParams
|
||||
{
|
||||
// a placeholder for future specialization
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SQL Assessment result item kind.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// SQL Assessment run is a set of checks. Every check
|
||||
/// may return a result item. Normally it is a note containing
|
||||
/// recommendations on improving target's configuration.
|
||||
/// But some checks may fail to obtain data due to access
|
||||
/// restrictions or data integrity. In this case
|
||||
/// the check produces an error or a warning.
|
||||
/// </remarks>
|
||||
public enum AssessmentResultItemKind
|
||||
{
|
||||
/// <summary>
|
||||
/// SQL Assessment item contains recommendation
|
||||
/// </summary>
|
||||
Note = 0,
|
||||
|
||||
/// <summary>
|
||||
/// SQL Assessment item contains a warning on
|
||||
/// limited assessment capabilities
|
||||
/// </summary>
|
||||
Warning = 1,
|
||||
|
||||
/// <summary>
|
||||
/// SQL Assessment item contain a description of
|
||||
/// error occured in the course of assessment run
|
||||
/// </summary>
|
||||
Error = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes an assessment result item
|
||||
/// containing a recommendation based on best practices.
|
||||
/// </summary>
|
||||
public class AssessmentResultItem : AssessmentItemInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a message to the user
|
||||
/// containing the recommendation.
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets result type:
|
||||
/// 0 - real result, 1 - warning, 2 - error.
|
||||
/// </summary>
|
||||
public AssessmentResultItemKind Kind { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets date and time
|
||||
/// when the item had been acquired.
|
||||
/// </summary>
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
}
|
||||
|
||||
public class InvokeRequest
|
||||
{
|
||||
public static readonly
|
||||
RequestType<InvokeParams, AssessmentResult<AssessmentResultItem>> Type =
|
||||
RequestType<InvokeParams, AssessmentResult<AssessmentResultItem>>.Create("assessment/invoke");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Microsoft.SqlTools.ServiceLayer.Management;
|
||||
using Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts;
|
||||
using Microsoft.SqlTools.ServiceLayer.TaskServices;
|
||||
using Microsoft.SqlTools.Utility;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.SqlAssessment
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a script storing SQL Assessment results to a table.
|
||||
/// </summary>
|
||||
internal sealed class GenerateScriptOperation : ITaskOperation, IDisposable
|
||||
{
|
||||
private readonly CancellationTokenSource cancellation = new CancellationTokenSource();
|
||||
|
||||
private bool disposed = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the unique id associated with this instance.
|
||||
/// </summary>
|
||||
public string OperationId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameters containing assessment results
|
||||
/// to be stored in a data table.
|
||||
/// </summary>
|
||||
public GenerateScriptParams Parameters { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the error message text
|
||||
/// if an error occurred during task execution
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sql task that's executing the operation
|
||||
/// </summary>
|
||||
public SqlTask SqlTask { get; set; }
|
||||
|
||||
public GenerateScriptOperation(GenerateScriptParams parameters)
|
||||
{
|
||||
Validate.IsNotNull(nameof(parameters), parameters);
|
||||
Parameters = parameters;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute a task
|
||||
/// </summary>
|
||||
/// <param name="mode">Task execution mode (e.g. script or execute)</param>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// The method has been called twice in parallel for the same instance.
|
||||
/// </exception>
|
||||
public void Execute(TaskExecutionMode mode)
|
||||
{
|
||||
try
|
||||
{
|
||||
var scriptText = GenerateScript(Parameters, cancellation.Token);
|
||||
if (scriptText != null)
|
||||
{
|
||||
SqlTask?.AddScript(SqlTaskStatus.Succeeded, scriptText);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ErrorMessage = e.Message;
|
||||
Logger.Write(TraceEventType.Error, string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"SQL Assessment: generate script operation failed with exception {0}",
|
||||
e.Message));
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
cancellation.Cancel();
|
||||
}
|
||||
|
||||
#region Helpers
|
||||
|
||||
internal static string GenerateScript(GenerateScriptParams generateScriptParams,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
const string scriptPrologue =
|
||||
@"IF (NOT EXISTS(SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'dbo' AND TABLE_NAME = 'AssessmentResult'))
|
||||
BEGIN
|
||||
CREATE TABLE [dbo].[AssessmentResult](
|
||||
[CheckName] [nvarchar](max) NOT NULL,
|
||||
[CheckId] [nvarchar](max) NOT NULL,
|
||||
[RulesetName] [nvarchar](max) NOT NULL,
|
||||
[RulesetVersion] [nvarchar](max) NOT NULL,
|
||||
[Severity] [nvarchar](max) NOT NULL,
|
||||
[Message] [nvarchar](max) NOT NULL,
|
||||
[TargetPath] [nvarchar](max) NOT NULL,
|
||||
[TargetType] [nvarchar](max) NOT NULL,
|
||||
[HelpLink] [nvarchar](max) NOT NULL,
|
||||
[Timestamp] [datetimeoffset](7) NOT NULL
|
||||
)
|
||||
END
|
||||
GO
|
||||
INSERT INTO [dbo].[AssessmentResult] ([CheckName],[CheckId],[RulesetName],[RulesetVersion],[Severity],[Message],[TargetPath],[TargetType],[HelpLink],[Timestamp])
|
||||
VALUES";
|
||||
|
||||
var sb = new StringBuilder();
|
||||
if (generateScriptParams.Items != null)
|
||||
{
|
||||
sb.Append(scriptPrologue);
|
||||
foreach (var item in generateScriptParams.Items)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (item.Kind == AssessmentResultItemKind.Note)
|
||||
{
|
||||
sb.Append(
|
||||
$"\r\n('{CUtils.EscapeStringSQuote(item.DisplayName)}','{CUtils.EscapeStringSQuote(item.CheckId)}','{CUtils.EscapeStringSQuote(item.RulesetName)}','{item.RulesetVersion}','{item.Level}','{CUtils.EscapeStringSQuote(item.Message)}','{CUtils.EscapeStringSQuote(item.TargetName)}','{item.TargetType}','{CUtils.EscapeStringSQuote(item.HelpLink)}','{item.Timestamp:yyyy-MM-dd hh:mm:ss.fff zzz}'),");
|
||||
}
|
||||
}
|
||||
|
||||
sb.Length -= 1;
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!disposed)
|
||||
{
|
||||
Cancel();
|
||||
cancellation.Dispose();
|
||||
disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,503 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.SqlServer.Management.Assessment;
|
||||
using Microsoft.SqlServer.Management.Assessment.Configuration;
|
||||
using Microsoft.SqlTools.Hosting.Protocol;
|
||||
using Microsoft.SqlTools.ServiceLayer.Connection;
|
||||
using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
|
||||
using Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting;
|
||||
using Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts;
|
||||
using Microsoft.SqlTools.ServiceLayer.SqlContext;
|
||||
using Microsoft.SqlTools.ServiceLayer.TaskServices;
|
||||
using Microsoft.SqlTools.ServiceLayer.Utility;
|
||||
using Microsoft.SqlTools.ServiceLayer.Workspace;
|
||||
using Microsoft.SqlTools.Utility;
|
||||
|
||||
using AssessmentResultItem = Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts.AssessmentResultItem;
|
||||
using ConnectionType = Microsoft.SqlTools.ServiceLayer.Connection.ConnectionType;
|
||||
using InvokeParams = Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts.InvokeParams;
|
||||
using InvokeRequest = Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts.InvokeRequest;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.SqlAssessment
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for running SQL Assessment.
|
||||
/// </summary>
|
||||
public sealed class SqlAssessmentService : IDisposable
|
||||
{
|
||||
private const string ApiVersion = "1.0";
|
||||
|
||||
#region Singleton Instance Implementation
|
||||
|
||||
private static readonly Lazy<SqlAssessmentService> LazyInstance
|
||||
= new Lazy<SqlAssessmentService>(() => new SqlAssessmentService());
|
||||
|
||||
internal SqlAssessmentService(
|
||||
ConnectionService connService,
|
||||
WorkspaceService<SqlToolsSettings> workspaceService)
|
||||
{
|
||||
ConnectionService = connService;
|
||||
WorkspaceService = workspaceService;
|
||||
}
|
||||
|
||||
private SqlAssessmentService()
|
||||
{
|
||||
ConnectionService = ConnectionService.Instance;
|
||||
WorkspaceService = WorkspaceService<SqlToolsSettings>.Instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Singleton instance of the query execution service
|
||||
/// </summary>
|
||||
public static SqlAssessmentService Instance => LazyInstance.Value;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="Engine"/> used to run assessment operations.
|
||||
/// </summary>
|
||||
private Engine Engine { get; } = new Engine();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the instance of the connection service,
|
||||
/// used to get the connection info for a given owner URI.
|
||||
/// </summary>
|
||||
private ConnectionService ConnectionService { get; }
|
||||
|
||||
private WorkspaceService<SqlToolsSettings> WorkspaceService { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Holds a map from the <see cref="Guid"/>
|
||||
/// to a <see cref="Task"/> that is being ran.
|
||||
/// </summary>
|
||||
private readonly Lazy<ConcurrentDictionary<string, Task>> activeRequests =
|
||||
new Lazy<ConcurrentDictionary<string, Task>>(() => new ConcurrentDictionary<string, Task>());
|
||||
|
||||
/// <summary>
|
||||
/// Gets a map from the <see cref="Guid"/>
|
||||
/// to a <see cref="Task"/> that is being ran.
|
||||
/// </summary>
|
||||
internal ConcurrentDictionary<string, Task> ActiveRequests => activeRequests.Value;
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the service with the service host,
|
||||
/// registers request handlers and shutdown event handler.
|
||||
/// </summary>
|
||||
/// <param name="serviceHost">The service host instance to register with</param>
|
||||
public void InitializeService(ServiceHost serviceHost)
|
||||
{
|
||||
// Register handlers for requests
|
||||
serviceHost.SetRequestHandler(InvokeRequest.Type, HandleInvokeRequest);
|
||||
serviceHost.SetRequestHandler(GetAssessmentItemsRequest.Type, HandleGetAssessmentItemsRequest);
|
||||
serviceHost.SetRequestHandler(GenerateScriptRequest.Type, HandleGenerateScriptRequest);
|
||||
|
||||
// Register handler for shutdown event
|
||||
serviceHost.RegisterShutdownTask((shutdownParams, requestContext) =>
|
||||
{
|
||||
Dispose();
|
||||
return Task.FromResult(0);
|
||||
});
|
||||
}
|
||||
|
||||
#region Request Handlers
|
||||
|
||||
internal Task HandleGetAssessmentItemsRequest(
|
||||
GetAssessmentItemsParams itemsParams,
|
||||
RequestContext<AssessmentResult<CheckInfo>> requestContext)
|
||||
{
|
||||
return this.HandleAssessmentRequest(requestContext, itemsParams, this.GetAssessmentItems);
|
||||
}
|
||||
|
||||
internal Task HandleInvokeRequest(
|
||||
InvokeParams invokeParams,
|
||||
RequestContext<AssessmentResult<AssessmentResultItem>> requestContext)
|
||||
{
|
||||
return this.HandleAssessmentRequest(requestContext, invokeParams, this.InvokeSqlAssessment);
|
||||
}
|
||||
|
||||
internal async Task HandleGenerateScriptRequest(
|
||||
GenerateScriptParams parameters,
|
||||
RequestContext<ResultStatus> requestContext)
|
||||
{
|
||||
GenerateScriptOperation operation = null;
|
||||
try
|
||||
{
|
||||
operation = new GenerateScriptOperation(parameters);
|
||||
TaskMetadata metadata = new TaskMetadata
|
||||
{
|
||||
TaskOperation = operation,
|
||||
TaskExecutionMode = parameters.TaskExecutionMode,
|
||||
ServerName = parameters.TargetServerName,
|
||||
DatabaseName = parameters.TargetDatabaseName,
|
||||
Name = SR.SqlAssessmentGenerateScriptTaskName
|
||||
};
|
||||
|
||||
var _ = SqlTaskManager.Instance.CreateAndRun<SqlTask>(metadata);
|
||||
|
||||
await requestContext.SendResult(new ResultStatus
|
||||
{
|
||||
Success = true,
|
||||
ErrorMessage = operation.ErrorMessage
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Write(TraceEventType.Error, "SQL Assessment: failed to generate a script. Error: " + e);
|
||||
await requestContext.SendResult(new ResultStatus()
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = operation == null ? e.Message : operation.ErrorMessage,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helpers
|
||||
|
||||
private async Task HandleAssessmentRequest<TResult>(
|
||||
RequestContext<AssessmentResult<TResult>> requestContext,
|
||||
AssessmentParams requestParams,
|
||||
Func<SqlObjectLocator, Task<List<TResult>>> assessmentFunction)
|
||||
where TResult : AssessmentItemInfo
|
||||
{
|
||||
try
|
||||
{
|
||||
string randomUri = Guid.NewGuid().ToString();
|
||||
|
||||
// get connection
|
||||
if (!ConnectionService.TryFindConnection(requestParams.OwnerUri, out var connInfo))
|
||||
{
|
||||
await requestContext.SendError(SR.SqlAssessmentQueryInvalidOwnerUri);
|
||||
return;
|
||||
}
|
||||
|
||||
ConnectParams connectParams = new ConnectParams
|
||||
{
|
||||
OwnerUri = randomUri,
|
||||
Connection = connInfo.ConnectionDetails,
|
||||
Type = ConnectionType.Default
|
||||
};
|
||||
|
||||
if(!connInfo.TryGetConnection(ConnectionType.Default, out var connection))
|
||||
{
|
||||
await requestContext.SendError(SR.SqlAssessmentConnectingError);
|
||||
}
|
||||
|
||||
var workTask = CallAssessmentEngine<TResult>(
|
||||
requestParams,
|
||||
connectParams,
|
||||
randomUri,
|
||||
assessmentFunction)
|
||||
.ContinueWith(async tsk =>
|
||||
{
|
||||
await requestContext.SendResult(tsk.Result);
|
||||
});
|
||||
|
||||
ActiveRequests.TryAdd(randomUri, workTask);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ex is StackOverflowException || ex is OutOfMemoryException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
await requestContext.SendError(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This function obtains a live connection, then calls
|
||||
/// an assessment operation specified by <paramref name="assessmentFunc"/>
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">
|
||||
/// SQL Assessment result item type.
|
||||
/// </typeparam>
|
||||
/// <param name="requestParams">
|
||||
/// Request parameters passed from the host.
|
||||
/// </param>
|
||||
/// <param name="connectParams">
|
||||
/// Connection parameters used to identify and access the target.
|
||||
/// </param>
|
||||
/// <param name="taskUri">
|
||||
/// An URI identifying the request task to enable concurrent execution.
|
||||
/// </param>
|
||||
/// <param name="assessmentFunc">
|
||||
/// A function performing assessment operation for given target.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Returns <see cref="AssessmentResult{TResult}"/> for given target.
|
||||
/// </returns>
|
||||
internal async Task<AssessmentResult<TResult>> CallAssessmentEngine<TResult>(
|
||||
AssessmentParams requestParams,
|
||||
ConnectParams connectParams,
|
||||
string taskUri,
|
||||
Func<SqlObjectLocator, Task<List<TResult>>> assessmentFunc)
|
||||
where TResult : AssessmentItemInfo
|
||||
|
||||
{
|
||||
var result = new AssessmentResult<TResult>
|
||||
{
|
||||
ApiVersion = ApiVersion
|
||||
};
|
||||
|
||||
await ConnectionService.Connect(connectParams);
|
||||
|
||||
var connection = await ConnectionService.Instance.GetOrOpenConnection(taskUri, ConnectionType.Query);
|
||||
|
||||
try
|
||||
{
|
||||
var serverInfo = ReliableConnectionHelper.GetServerVersion(connection);
|
||||
var hostInfo = ReliableConnectionHelper.GetServerHostInfo(connection);
|
||||
|
||||
var server = new SqlObjectLocator
|
||||
{
|
||||
Connection = connection,
|
||||
EngineEdition = GetEngineEdition(serverInfo.EngineEditionId),
|
||||
Name = serverInfo.ServerName,
|
||||
ServerName = serverInfo.ServerName,
|
||||
Type = SqlObjectType.Server,
|
||||
Urn = serverInfo.ServerName,
|
||||
Version = Version.Parse(serverInfo.ServerVersion),
|
||||
Platform = hostInfo.Platform
|
||||
};
|
||||
|
||||
switch (requestParams.TargetType)
|
||||
{
|
||||
case SqlObjectType.Server:
|
||||
Logger.Write(
|
||||
TraceEventType.Verbose,
|
||||
$"SQL Assessment: running an operation on a server, platform:{server.Platform}, edition:{server.EngineEdition.ToString()}, version:{server.Version}");
|
||||
|
||||
result.Items.AddRange(await assessmentFunc(server));
|
||||
|
||||
Logger.Write(
|
||||
TraceEventType.Verbose,
|
||||
$"SQL Assessment: finished an operation on a server, platform:{server.Platform}, edition:{server.EngineEdition.ToString()}, version:{server.Version}");
|
||||
break;
|
||||
case SqlObjectType.Database:
|
||||
var db = GetDatabaseLocator(server, connection.Database);
|
||||
Logger.Write(
|
||||
TraceEventType.Verbose,
|
||||
$"SQL Assessment: running an operation on a database, platform:{server.Platform}, edition:{server.EngineEdition.ToString()}, version:{server.Version}");
|
||||
|
||||
result.Items.AddRange(await assessmentFunc(db));
|
||||
|
||||
Logger.Write(
|
||||
TraceEventType.Verbose,
|
||||
$"SQL Assessment: finished an operation on a database, platform:{server.Platform}, edition:{server.EngineEdition.ToString()}, version:{server.Version}");
|
||||
break;
|
||||
}
|
||||
|
||||
result.Success = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ActiveRequests.TryRemove(taskUri, out _);
|
||||
ConnectionService.Disconnect(new DisconnectParams { OwnerUri = taskUri, Type = null });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes SQL Assessment and formats results.
|
||||
/// </summary>
|
||||
/// <param name="target">
|
||||
/// A sequence of target servers or databases to be assessed.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Returns a <see cref="List{AssessmentResultItem}"/>
|
||||
/// containing assessment results.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// Internal for testing
|
||||
/// </remarks>
|
||||
internal async Task<List<AssessmentResultItem>> InvokeSqlAssessment(SqlObjectLocator target)
|
||||
{
|
||||
var resultsList = await Engine.GetAssessmentResultsList(target);
|
||||
Logger.Write(TraceEventType.Verbose, $"SQL Assessment: got {resultsList.Count} results.");
|
||||
|
||||
return resultsList.Select(TranslateAssessmentResult).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of checks for given target servers or databases.
|
||||
/// </summary>
|
||||
/// <param name="target">
|
||||
/// A sequence of target servers or databases.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Returns an <see cref="IEnumerable{SqlObjectLocator}"/>
|
||||
/// containing checks available for given <paramref name="target"/>.
|
||||
/// </returns>
|
||||
internal Task<List<CheckInfo>> GetAssessmentItems(SqlObjectLocator target)
|
||||
{
|
||||
var result = new List<CheckInfo>();
|
||||
|
||||
var resultsList = Engine.GetChecks(target).ToList();
|
||||
Logger.Write(TraceEventType.Verbose, $"SQL Assessment: got {resultsList.Count} items.");
|
||||
|
||||
foreach (var r in resultsList)
|
||||
{
|
||||
var item = new CheckInfo()
|
||||
{
|
||||
CheckId = r.Id,
|
||||
Description = r.Description,
|
||||
DisplayName = r.DisplayName,
|
||||
HelpLink = r.HelpLink,
|
||||
Level = r.Level.ToString(),
|
||||
TargetName = $"{target.ServerName}/{target.Name}",
|
||||
Tags = r.Tags.ToArray(),
|
||||
TargetType = target.Type,
|
||||
RulesetName = Engine.Configuration.DefaultRuleset.Name,
|
||||
RulesetVersion = Engine.Configuration.DefaultRuleset.Version.ToString()
|
||||
};
|
||||
|
||||
result.Add(item);
|
||||
}
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
private AssessmentResultItem TranslateAssessmentResult(IAssessmentResult r)
|
||||
{
|
||||
var item = new AssessmentResultItem
|
||||
{
|
||||
CheckId = r.Check.Id,
|
||||
Description = r.Check.Description,
|
||||
DisplayName = r.Check.DisplayName,
|
||||
HelpLink = r.Check.HelpLink,
|
||||
Level = r.Check.Level.ToString(),
|
||||
Message = r.Message,
|
||||
TargetName = r.TargetPath,
|
||||
Tags = r.Check.Tags.ToArray(),
|
||||
TargetType = r.TargetType,
|
||||
RulesetVersion = Engine.Configuration.DefaultRuleset.Version.ToString(),
|
||||
RulesetName = Engine.Configuration.DefaultRuleset.Name,
|
||||
Timestamp = r.Timestamp
|
||||
};
|
||||
|
||||
if (r is IAssessmentNote)
|
||||
{
|
||||
item.Kind = AssessmentResultItemKind.Note;
|
||||
}
|
||||
else if (r is IAssessmentWarning)
|
||||
{
|
||||
item.Kind = AssessmentResultItemKind.Warning;
|
||||
}
|
||||
else if (r is IAssessmentError)
|
||||
{
|
||||
item.Kind = AssessmentResultItemKind.Error;
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a <see cref="SqlObjectLocator"/> for specified database.
|
||||
/// </summary>
|
||||
/// <param name="server">Target server locator.</param>
|
||||
/// <param name="databaseName">Target database name.</param>
|
||||
/// <returns>Returns a locator for target database.</returns>
|
||||
private static SqlObjectLocator GetDatabaseLocator(SqlObjectLocator server, string databaseName)
|
||||
{
|
||||
return new SqlObjectLocator
|
||||
{
|
||||
Connection = server.Connection,
|
||||
EngineEdition = server.EngineEdition,
|
||||
Name = databaseName,
|
||||
Platform = server.Platform,
|
||||
ServerName = server.Name,
|
||||
Type = SqlObjectType.Database,
|
||||
Urn = $"{server.Name}/{databaseName}",
|
||||
Version = server.Version
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts numeric <paramref name="representation"/> of engine edition
|
||||
/// returned by SERVERPROPERTY('EngineEdition').
|
||||
/// </summary>
|
||||
/// <param name="representation">
|
||||
/// A number returned by SERVERPROPERTY('EngineEdition').
|
||||
/// </param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Engine edition is not supported.</exception>
|
||||
/// <returns>
|
||||
/// Returns a <see cref="SqlEngineEdition"/>
|
||||
/// corresponding to the <paramref name="representation"/>.
|
||||
/// </returns>
|
||||
private static SqlEngineEdition GetEngineEdition(int representation)
|
||||
{
|
||||
switch (representation)
|
||||
{
|
||||
case 1: return SqlEngineEdition.PersonalOrDesktopEngine;
|
||||
case 2: return SqlEngineEdition.Standard;
|
||||
case 3: return SqlEngineEdition.Enterprise;
|
||||
case 4: return SqlEngineEdition.Express;
|
||||
case 5: return SqlEngineEdition.AzureDatabase;
|
||||
case 6: return SqlEngineEdition.DataWarehouse;
|
||||
case 7: return SqlEngineEdition.StretchDatabase;
|
||||
case 8: return SqlEngineEdition.ManagedInstance;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(representation),
|
||||
SR.SqlAssessmentUnsuppoertedEdition(representation));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable Implementation
|
||||
|
||||
private bool disposed;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
foreach (var request in ActiveRequests)
|
||||
{
|
||||
request.Value.Dispose();
|
||||
}
|
||||
|
||||
ActiveRequests.Clear();
|
||||
}
|
||||
|
||||
disposed = true;
|
||||
}
|
||||
|
||||
~SqlAssessmentService()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.SqlServer.Management.Assessment;
|
||||
using Microsoft.SqlTools.ServiceLayer.Connection;
|
||||
using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
|
||||
using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility;
|
||||
using Microsoft.SqlTools.ServiceLayer.SqlAssessment;
|
||||
using Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts;
|
||||
using Microsoft.SqlTools.ServiceLayer.Test.Common;
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
using Assert = Xunit.Assert;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.SqlAssessment
|
||||
{
|
||||
public class SqlAssessmentServiceTests
|
||||
{
|
||||
private delegate Task<List<TResult>> AssessmentMethod<TResult>(SqlObjectLocator locator);
|
||||
|
||||
private static readonly string[] AllowedSeverityLevels = { "Information", "Warning", "Critical" };
|
||||
|
||||
[Fact]
|
||||
public async void GetAssessmentItemsServerTest()
|
||||
{
|
||||
var response = await CallAssessment<CheckInfo>(
|
||||
nameof(SqlAssessmentService.GetAssessmentItems),
|
||||
SqlObjectType.Server);
|
||||
|
||||
Assert.All(
|
||||
response.Items,
|
||||
i =>
|
||||
{
|
||||
AssertInfoPresent(i);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void InvokeSqlAssessmentServerTest()
|
||||
{
|
||||
var response = await CallAssessment<AssessmentResultItem>(
|
||||
nameof(SqlAssessmentService.InvokeSqlAssessment),
|
||||
SqlObjectType.Server);
|
||||
|
||||
|
||||
Assert.All(
|
||||
response.Items,
|
||||
i =>
|
||||
{
|
||||
Assert.NotNull(i.Message);
|
||||
Assert.NotEmpty(i.Message);
|
||||
|
||||
if (i.Kind == 0)
|
||||
{
|
||||
AssertInfoPresent(i);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void GetAssessmentItemsDatabaseTest()
|
||||
{
|
||||
const string DatabaseName = "tempdb";
|
||||
var response = await CallAssessment<CheckInfo>(
|
||||
nameof(SqlAssessmentService.GetAssessmentItems),
|
||||
SqlObjectType.Database,
|
||||
DatabaseName);
|
||||
|
||||
Assert.All(
|
||||
response.Items,
|
||||
i =>
|
||||
{
|
||||
StringAssert.EndsWith("/" + DatabaseName, i.TargetName);
|
||||
AssertInfoPresent(i);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void InvokeSqlAssessmentIDatabaseTest()
|
||||
{
|
||||
const string DatabaseName = "tempdb";
|
||||
var response = await CallAssessment<AssessmentResultItem>(
|
||||
nameof(SqlAssessmentService.InvokeSqlAssessment),
|
||||
SqlObjectType.Database,
|
||||
DatabaseName);
|
||||
|
||||
Assert.All(
|
||||
response.Items,
|
||||
i =>
|
||||
{
|
||||
StringAssert.EndsWith("/" + DatabaseName, i.TargetName);
|
||||
Assert.NotNull(i.Message);
|
||||
Assert.NotEmpty(i.Message);
|
||||
|
||||
if (i.Kind == 0)
|
||||
{
|
||||
AssertInfoPresent(i);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task<AssessmentResult<TResult>> CallAssessment<TResult>(
|
||||
string methodName,
|
||||
SqlObjectType sqlObjectType,
|
||||
string databaseName = "master")
|
||||
where TResult : AssessmentItemInfo
|
||||
{
|
||||
var liveConnection = LiveConnectionHelper.InitLiveConnectionInfo(databaseName);
|
||||
var connInfo = liveConnection.ConnectionInfo;
|
||||
|
||||
AssessmentResult<TResult> response;
|
||||
|
||||
using (var service = new SqlAssessmentService(
|
||||
TestServiceProvider.Instance.ConnectionService,
|
||||
TestServiceProvider.Instance.WorkspaceService))
|
||||
{
|
||||
string randomUri = Guid.NewGuid().ToString();
|
||||
AssessmentParams requestParams =
|
||||
new AssessmentParams { OwnerUri = randomUri, TargetType = sqlObjectType };
|
||||
ConnectParams connectParams = new ConnectParams
|
||||
{
|
||||
OwnerUri = requestParams.OwnerUri,
|
||||
Connection = connInfo.ConnectionDetails,
|
||||
Type = ConnectionType.Default
|
||||
};
|
||||
|
||||
var methodInfo = typeof(SqlAssessmentService).GetMethod(
|
||||
methodName,
|
||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
|
||||
Assert.NotNull(methodInfo);
|
||||
|
||||
var func = (AssessmentMethod<TResult>)Delegate.CreateDelegate(
|
||||
typeof(AssessmentMethod<TResult>),
|
||||
service,
|
||||
methodInfo);
|
||||
|
||||
response = await service.CallAssessmentEngine<TResult>(
|
||||
requestParams,
|
||||
connectParams,
|
||||
randomUri,
|
||||
t => func(t));
|
||||
}
|
||||
|
||||
Assert.NotNull(response);
|
||||
if (response.Success)
|
||||
{
|
||||
Assert.All(
|
||||
response.Items,
|
||||
i =>
|
||||
{
|
||||
Assert.Equal(sqlObjectType, i.TargetType);
|
||||
Assert.Contains(i.Level, AllowedSeverityLevels);
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private void AssertInfoPresent(AssessmentItemInfo item)
|
||||
{
|
||||
Assert.NotNull(item.CheckId);
|
||||
Assert.NotEmpty(item.CheckId);
|
||||
Assert.NotNull(item.DisplayName);
|
||||
Assert.NotEmpty(item.DisplayName);
|
||||
Assert.NotNull(item.Description);
|
||||
Assert.NotEmpty(item.Description);
|
||||
Assert.NotNull(item.Tags);
|
||||
Assert.All(item.Tags, t =>
|
||||
{
|
||||
Assert.NotNull(t);
|
||||
Assert.NotEmpty(t);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SqlServer.Management.Assessment;
|
||||
using Microsoft.SqlTools.ServiceLayer.SqlAssessment;
|
||||
using Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts;
|
||||
using Microsoft.SqlTools.ServiceLayer.TaskServices;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Xunit;
|
||||
using Assert = Xunit.Assert;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.UnitTests.SqlAssessment
|
||||
{
|
||||
public class GenerateScriptOperationTests
|
||||
{
|
||||
private static readonly GenerateScriptParams SampleParams = new GenerateScriptParams
|
||||
{
|
||||
Items = new List<AssessmentResultItem>
|
||||
{
|
||||
new AssessmentResultItem
|
||||
{
|
||||
CheckId = "C1",
|
||||
Description = "Desc1",
|
||||
DisplayName = "DN1",
|
||||
HelpLink = "HL1",
|
||||
Kind = AssessmentResultItemKind.Note,
|
||||
Level = "Information",
|
||||
Message = "Msg'1",
|
||||
TargetName = "proj[*]_dev",
|
||||
TargetType = SqlObjectType.Server,
|
||||
Timestamp = new DateTimeOffset(2001, 5, 25, 13, 42, 00, TimeSpan.Zero),
|
||||
RulesetName = "Microsoft Ruleset",
|
||||
RulesetVersion = "1.3"
|
||||
},
|
||||
new AssessmentResultItem
|
||||
{
|
||||
CheckId = "C-2",
|
||||
Description = "Desc2",
|
||||
DisplayName = "D N2",
|
||||
HelpLink = "http://HL2",
|
||||
Kind = AssessmentResultItemKind.Warning,
|
||||
Level = "Warning",
|
||||
Message = "Msg'1",
|
||||
TargetName = "proj[*]_devW",
|
||||
TargetType = SqlObjectType.Database,
|
||||
Timestamp = new DateTimeOffset(2001, 5, 25, 13, 42, 00, TimeSpan.FromHours(3)),
|
||||
RulesetName = "Microsoft Ruleset",
|
||||
RulesetVersion = "1.3"
|
||||
},
|
||||
new AssessmentResultItem
|
||||
{
|
||||
CheckId = "C'3",
|
||||
Description = "Des'c3",
|
||||
DisplayName = "D'N1",
|
||||
HelpLink = "HL'1",
|
||||
Kind = AssessmentResultItemKind.Error,
|
||||
Level = "Critical",
|
||||
Message = "Msg'1",
|
||||
TargetName = "proj[*]_devE",
|
||||
TargetType = SqlObjectType.Server,
|
||||
Timestamp = new DateTimeOffset(2001, 5, 25, 13, 42, 00, TimeSpan.FromMinutes(-90)),
|
||||
RulesetName = "Microsoft Ruleset",
|
||||
RulesetVersion = "1.3"
|
||||
},
|
||||
new AssessmentResultItem
|
||||
{
|
||||
CheckId = "C-2",
|
||||
Description = "Desc2",
|
||||
DisplayName = "D N2",
|
||||
HelpLink = "http://HL2",
|
||||
Kind = AssessmentResultItemKind.Note,
|
||||
Level = "Warning",
|
||||
Message = "Msg'1",
|
||||
TargetName = "proj[*]_dev",
|
||||
TargetType = SqlObjectType.Database,
|
||||
Timestamp = new DateTimeOffset(2001, 5, 25, 13, 42, 00, TimeSpan.FromHours(3)),
|
||||
RulesetName = "Microsoft Ruleset",
|
||||
RulesetVersion = "1.3"
|
||||
},
|
||||
new AssessmentResultItem
|
||||
{
|
||||
CheckId = "C'3",
|
||||
Description = "Des'c3",
|
||||
DisplayName = "D'N1",
|
||||
HelpLink = "HL'1",
|
||||
Kind = AssessmentResultItemKind.Note,
|
||||
Level = "Critical",
|
||||
Message = "Msg'1",
|
||||
TargetName = "proj[*]_dev",
|
||||
TargetType = SqlObjectType.Server,
|
||||
Timestamp = new DateTimeOffset(2001, 5, 25, 13, 42, 00, TimeSpan.FromMinutes(-90)),
|
||||
RulesetName = "Microsoft Ruleset",
|
||||
RulesetVersion = "1.3"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private const string SampleScript =
|
||||
@"IF (NOT EXISTS(SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'dbo' AND TABLE_NAME = 'AssessmentResult'))
|
||||
BEGIN
|
||||
CREATE TABLE [dbo].[AssessmentResult](
|
||||
[CheckName] [nvarchar](max) NOT NULL,
|
||||
[CheckId] [nvarchar](max) NOT NULL,
|
||||
[RulesetName] [nvarchar](max) NOT NULL,
|
||||
[RulesetVersion] [nvarchar](max) NOT NULL,
|
||||
[Severity] [nvarchar](max) NOT NULL,
|
||||
[Message] [nvarchar](max) NOT NULL,
|
||||
[TargetPath] [nvarchar](max) NOT NULL,
|
||||
[TargetType] [nvarchar](max) NOT NULL,
|
||||
[HelpLink] [nvarchar](max) NOT NULL,
|
||||
[Timestamp] [datetimeoffset](7) NOT NULL
|
||||
)
|
||||
END
|
||||
GO
|
||||
INSERT INTO [dbo].[AssessmentResult] ([CheckName],[CheckId],[RulesetName],[RulesetVersion],[Severity],[Message],[TargetPath],[TargetType],[HelpLink],[Timestamp])
|
||||
VALUES
|
||||
('DN1','C1','Microsoft Ruleset','1.3','Information','Msg''1','proj[*]_dev','Server','HL1','2001-05-25 01:42:00.000 +00:00'),
|
||||
('D N2','C-2','Microsoft Ruleset','1.3','Warning','Msg''1','proj[*]_dev','Database','http://HL2','2001-05-25 01:42:00.000 +03:00'),
|
||||
('D''N1','C''3','Microsoft Ruleset','1.3','Critical','Msg''1','proj[*]_dev','Server','HL''1','2001-05-25 01:42:00.000 -01:30')";
|
||||
|
||||
[Fact]
|
||||
public void GenerateScriptTest()
|
||||
{
|
||||
var scriptText = GenerateScriptOperation.GenerateScript(SampleParams, CancellationToken.None);
|
||||
Assert.Equal(SampleScript, scriptText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExecuteTest()
|
||||
{
|
||||
var subject = new GenerateScriptOperation(SampleParams);
|
||||
var taskMetadata = new TaskMetadata();
|
||||
using (var sqlTask = new SqlTask(taskMetadata, DummyOpFunction, DummyOpFunction))
|
||||
{
|
||||
subject.SqlTask = sqlTask;
|
||||
sqlTask.ScriptAdded += ValidateScriptAdded;
|
||||
subject.Execute(TaskExecutionMode.Script);
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateScriptAdded(object sender, TaskEventArgs<TaskScript> e)
|
||||
{
|
||||
Assert.Equal(SqlTaskStatus.Succeeded, e.TaskData.Status);
|
||||
Assert.Equal(SampleScript, e.TaskData.Script);
|
||||
}
|
||||
|
||||
private static Task<TaskResult> DummyOpFunction(SqlTask _)
|
||||
{
|
||||
return Task.FromResult(new TaskResult() {TaskStatus = SqlTaskStatus.Succeeded});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user