Adding server metadata to SQL Migration Assessment API (#1222)

* WIP

* Adding server metdata to assessment call

* Fixed spacing

* Sorted imports
Added copyright header
Changes some class names
Fixed description

* Fixed assessment result and removing unused imports

* Adding assessment result
This commit is contained in:
Aasim Khan
2021-07-19 12:38:10 -07:00
committed by GitHub
parent c731744298
commit f25dd5d2a4
3 changed files with 259 additions and 196 deletions

View File

@@ -3,8 +3,8 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information. // 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.Hosting.Protocol.Contracts;
using Microsoft.SqlServer.Migration.Assessment.Common.Contracts.Models;
namespace Microsoft.SqlTools.ServiceLayer.Migration.Contracts namespace Microsoft.SqlTools.ServiceLayer.Migration.Contracts
{ {
@@ -16,20 +16,25 @@ namespace Microsoft.SqlTools.ServiceLayer.Migration.Contracts
public class MigrationAssessmentResult public class MigrationAssessmentResult
{ {
/// <summary> /// <summary>
/// Gets the collection of assessment results. /// Errors that happen while running the assessment
/// </summary> /// </summary>
public List<MigrationAssessmentInfo> Items { get; } = new List<MigrationAssessmentInfo>(); public ErrorModel[] Errors { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating /// Result of the assessment
/// if assessment operation was successful.
/// </summary> /// </summary>
public bool Success { get; set; } public ServerAssessmentProperties AssessmentResult { get; set; }
/// <summary> /// <summary>
/// Gets or sets an status message for the operation. /// Start time of the assessment
/// </summary> /// </summary>
public string ErrorMessage { get; set; } public string StartTime { get; set; }
/// <summary>
/// End time of the assessment
/// </summary>
public string EndedTime { get; set; }
/// <summary>
/// Contains the raw assessment response
/// </summary>
public ISqlMigrationAssessmentModel RawAssessmentResult { get; set; }
} }
/// <summary> /// <summary>

View File

@@ -0,0 +1,133 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlServer.Migration.Assessment.Common.Contracts.TargetAssessment.Models;
namespace Microsoft.SqlTools.ServiceLayer.Migration.Contracts
{
public class ServerAssessmentProperties
{
/// <summary>
/// Name of the server
/// </summary>
public string Name { get; set; }
/// <summary>
/// Cpu cores for the server host
/// </summary>
public long CpuCoreCount { get; set; }
/// <summary>
/// Server host physical memory size
/// </summary>
public double PhysicalServerMemory { get; set; }
/// <summary>
/// Host operating system of the SQL server
/// </summary>
public string ServerHostPlatform { get; set; }
/// <summary>
/// Version of the SQL server
/// </summary>
public string ServerVersion { get; set; }
/// <summary>
/// SQL server engine edition
/// </summary>
public string ServerEngineEdition { get; set; }
/// <summary>
/// SQL server edition
/// </summary>
public string ServerEdition { get; set; }
/// <summary>
/// We use this flag to indicate if the SQL server is part of the failover cluster
/// </summary>
public bool IsClustered { get; set; }
/// <summary>
/// Returns the total number of dbs assessed
/// </summary>
public long NumberOfUserDatabases { get; set; }
/// <summary>
/// Returns the assessment status
/// </summary>
public int SqlAssessmentStatus { get; set; }
/// <summary>
/// Count of Dbs assessed
/// </summary>
public long AssessedDatabaseCount{get; set;}
/// <summary>
/// Give assessed server stats for SQL MI compatibility
/// </summary>
public IServerTargetReadiness SQLManagedInstanceTargetReadiness { get; set; }
/// <summary>
/// Server assessment results
/// </summary>
public MigrationAssessmentInfo[] Items { get; set; }
/// <summary>
/// Server assessment errors
/// </summary>
public ErrorModel[] Errors { get; set; }
/// <summary>
/// List of databases that are assessed
/// </summary>
public DatabaseAssessmentProperties[] Databases { get; set; }
}
public class DatabaseAssessmentProperties
{
/// <summary>
/// Name of the database
/// </summary>
public string Name { get; set; }
/// <summary>
/// Compatibility level of the database
/// </summary>
public string CompatibilityLevel { get; set; }
/// <summary>
/// Size of the database
/// </summary>
public double DatabaseSize { get; set; }
/// <summary>
/// Flag that indicates if the database is replicated
/// </summary>
public bool IsReplicationEnabled { get; set; }
/// <summary>
/// Time taken for assessing the database
/// </summary>
public double AssessmentTimeInMilliseconds { get; set; }
/// <summary>
/// Database Assessment Results
/// </summary>
public MigrationAssessmentInfo[] Items { get; set; }
/// <summary>
/// Database assessment errors
/// </summary>
public ErrorModel[] Errors {get; set;}
/// <summary>
/// Flags that indicate if the database is ready for migration
/// </summary>
public IDatabaseTargetReadiness SQLManagedInstanceTargetReadiness { get; set; }
}
public class ErrorModel
{
/// <summary>
/// Id of the assessment error
/// </summary>
public string ErrorId { get; set; }
/// <summary>
/// Error message
/// </summary>
public string Message { get; set; }
/// <summary>
/// Summary of the Error
/// </summary>
public string ErrorSummary { get; set; }
/// <summary>
/// Possible causes for the error
/// </summary>
public string PossibleCauses { get; set; }
/// <summary>
/// Possible mitigation for the error
/// </summary>
public string Guidance { get; set; }
}
}

View File

@@ -5,27 +5,23 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.SqlServer.Management.Assessment; using Microsoft.SqlServer.DataCollection.Common;
using Microsoft.SqlServer.Management.Assessment.Checks; using Microsoft.SqlServer.Management.Assessment.Checks;
using Microsoft.SqlServer.Management.Assessment;
using Microsoft.SqlServer.Migration.Assessment.Common.Contracts.Models; using Microsoft.SqlServer.Migration.Assessment.Common.Contracts.Models;
using Microsoft.SqlServer.Migration.Assessment.Common.Engine; using Microsoft.SqlServer.Migration.Assessment.Common.Engine;
using Microsoft.SqlTools.Hosting.Protocol; using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
using Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection; using Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection;
using Microsoft.SqlTools.Utility; using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.Hosting; using Microsoft.SqlTools.ServiceLayer.Hosting;
using Microsoft.SqlTools.ServiceLayer.Migration.Contracts; using Microsoft.SqlTools.ServiceLayer.Migration.Contracts;
using Microsoft.SqlTools.ServiceLayer.SqlAssessment; using Microsoft.SqlTools.ServiceLayer.SqlAssessment;
using Microsoft.Win32.SafeHandles; using Microsoft.SqlTools.Utility;
using Microsoft.SqlServer.DataCollection.Common;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Security.Principal;
using System.IO;
namespace Microsoft.SqlTools.ServiceLayer.Migration namespace Microsoft.SqlTools.ServiceLayer.Migration
{ {
/// <summary> /// <summary>
@@ -95,8 +91,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Migration
{ {
this.ServiceHost = serviceHost; this.ServiceHost = serviceHost;
this.ServiceHost.SetRequestHandler(MigrationAssessmentsRequest.Type, HandleMigrationAssessmentsRequest); this.ServiceHost.SetRequestHandler(MigrationAssessmentsRequest.Type, HandleMigrationAssessmentsRequest);
this.ServiceHost.SetRequestHandler(ValidateWindowsAccountRequest.Type, HandleValidateWindowsAccountRequest);
this.ServiceHost.SetRequestHandler(ValidateNetworkFileShareRequest.Type, HandleValidateNetworkFileShareRequest);
} }
/// <summary> /// <summary>
@@ -143,10 +137,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Migration
var db = SqlAssessmentService.GetDatabaseLocator(server, connection.Database); var db = SqlAssessmentService.GetDatabaseLocator(server, connection.Database);
var connectionString = ConnectionService.BuildConnectionString(connInfo.ConnectionDetails); var connectionString = ConnectionService.BuildConnectionString(connInfo.ConnectionDetails);
var results = await GetAssessmentItems(server, connectionString);
var result = new MigrationAssessmentResult(); var results = await GetAssessmentItems(connectionString);
result.Items.AddRange(results); await requestContext.SendResult(results);
await requestContext.SendResult(result);
} }
catch (Exception e) catch (Exception e)
{ {
@@ -186,67 +179,124 @@ namespace Microsoft.SqlTools.ServiceLayer.Migration
} }
} }
internal async Task<List<MigrationAssessmentInfo>> GetAssessmentItems(SqlObjectLocator target, string connectionString) internal async Task<MigrationAssessmentResult> GetAssessmentItems(string connectionString)
{ {
SqlAssessmentConfiguration.EnableLocalLogging = true; SqlAssessmentConfiguration.EnableLocalLogging = true;
SqlAssessmentConfiguration.EnableReportCreation = true; SqlAssessmentConfiguration.EnableReportCreation = true;
SqlAssessmentConfiguration.AssessmentReportAndLogsRootFolderPath = Path.GetDirectoryName(Logger.LogFileFullPath); SqlAssessmentConfiguration.AssessmentReportAndLogsRootFolderPath = Path.GetDirectoryName(Logger.LogFileFullPath);
DmaEngine engine = new DmaEngine(connectionString); DmaEngine engine = new DmaEngine(connectionString);
var assessmentResults = await engine.GetTargetAssessmentResultsList(); var assessmentResults = await engine.GetTargetAssessmentResultsList();
Dictionary<string, ISqlMigrationAssessmentResult> assessmentResultLookup = new Dictionary<string, ISqlMigrationAssessmentResult>();
var result = new List<MigrationAssessmentInfo>(); foreach (ISqlMigrationAssessmentResult r in assessmentResults)
foreach (var r in assessmentResults)
{ {
var migrationResult = r as ISqlMigrationAssessmentResult; assessmentResultLookup.Add(CreateAssessmentResultKey(r as ISqlMigrationAssessmentResult), r as ISqlMigrationAssessmentResult);
if (migrationResult == null) }
{ ISqlMigrationAssessmentModel contextualizedAssessmentResult = await engine.GetTargetAssessmentResultsList(System.Threading.CancellationToken.None);
continue; return new MigrationAssessmentResult()
} {
AssessmentResult = ParseServerAssessmentInfo(contextualizedAssessmentResult.Servers[0], assessmentResultLookup),
Errors = ParseAssessmentError(contextualizedAssessmentResult.Errors),
StartTime = contextualizedAssessmentResult.StartedOn.ToString(),
EndedTime = contextualizedAssessmentResult.EndedOn.ToString(),
RawAssessmentResult = contextualizedAssessmentResult
};
}
var targetName = !string.IsNullOrWhiteSpace(migrationResult.DatabaseName) internal ServerAssessmentProperties ParseServerAssessmentInfo(IServerAssessmentInfo server, Dictionary<string, ISqlMigrationAssessmentResult> assessmentResultLookup)
? $"{target.ServerName}:{migrationResult.DatabaseName}" {
: target.Name; return new ServerAssessmentProperties()
var ruleId = migrationResult.FeatureId.ToString(); {
CpuCoreCount = server.Properties.ServerCoreCount,
PhysicalServerMemory = server.Properties.MaxServerMemoryInUse,
ServerHostPlatform = server.Properties.ServerHostPlatform,
ServerVersion = server.Properties.ServerVersion,
ServerEngineEdition = server.Properties.ServerEngineEdition,
ServerEdition = server.Properties.ServerEdition,
IsClustered = server.Properties.IsClustered,
NumberOfUserDatabases = server.Properties.NumberOfUserDatabases,
SqlAssessmentStatus = (int)server.Status,
AssessedDatabaseCount = server.Properties.NumberOfUserDatabases,
SQLManagedInstanceTargetReadiness = server.TargetReadinesses[Microsoft.SqlServer.DataCollection.Common.Contracts.Advisor.TargetType.AzureSqlManagedInstance],
Errors = ParseAssessmentError(server.Errors),
Items = ParseAssessmentResult(server.ServerAssessments, assessmentResultLookup),
Databases = ParseDatabaseAssessmentInfo(server.Databases, assessmentResultLookup),
Name = server.Properties.ServerName
};
}
var item = new MigrationAssessmentInfo() internal DatabaseAssessmentProperties[] ParseDatabaseAssessmentInfo(IList<IDatabaseAssessmentInfo> databases, Dictionary<string, ISqlMigrationAssessmentResult> assessmentResultLookup)
{
return databases.Select(d =>
{
return new DatabaseAssessmentProperties()
{ {
CheckId = r.Check.Id, Name = d.Properties.Name,
Description = r.Check.Description, CompatibilityLevel = d.Properties.CompatibilityLevel.ToString(),
DisplayName = r.Check.DisplayName, DatabaseSize = d.Properties.SizeMB,
HelpLink = r.Check.HelpLink, IsReplicationEnabled = d.Properties.IsReplicationEnabled,
Level = r.Check.Level.ToString(), AssessmentTimeInMilliseconds = d.Properties.TSqlScriptAnalysisTimeElapse.TotalMilliseconds,
TargetName = targetName, Errors = ParseAssessmentError(d.Errors),
DatabaseName = migrationResult.DatabaseName, Items = ParseAssessmentResult(d.DatabaseAssessments, assessmentResultLookup),
ServerName = migrationResult.ServerName, SQLManagedInstanceTargetReadiness = d.TargetReadinesses[Microsoft.SqlServer.DataCollection.Common.Contracts.Advisor.TargetType.AzureSqlManagedInstance]
Tags = r.Check.Tags.ToArray(), };
TargetType = target.Type, }).ToArray();
}
internal ErrorModel[] ParseAssessmentError(IList<Microsoft.SqlServer.DataCollection.Common.Contracts.ErrorHandling.IErrorModel> errors)
{
return errors.Select(e =>
{
return new ErrorModel()
{
ErrorId = e.ErrorID.ToString(),
Message = e.Message,
ErrorSummary = e.ErrorSummary,
PossibleCauses = e.PossibleCauses,
Guidance = e.Guidance,
};
}).ToArray();
}
internal MigrationAssessmentInfo[] ParseAssessmentResult(IList<ISqlMigrationAssessmentResult> assessmentResults, Dictionary<string, ISqlMigrationAssessmentResult> assessmentResultLookup)
{
return assessmentResults.Select(r =>
{
var check = assessmentResultLookup[CreateAssessmentResultKey(r)].Check;
return new MigrationAssessmentInfo()
{
CheckId = check.Id,
Description = check.Description,
DisplayName = check.DisplayName,
HelpLink = check.HelpLink,
Level = check.Level.ToString(),
TargetName = r.AppliesToMigrationTargetPlatform.ToString(),
DatabaseName = r.DatabaseName,
ServerName = r.ServerName,
Tags = check.Tags.ToArray(),
RulesetName = Engine.Configuration.DefaultRuleset.Name, RulesetName = Engine.Configuration.DefaultRuleset.Name,
RulesetVersion = Engine.Configuration.DefaultRuleset.Version.ToString(), RulesetVersion = Engine.Configuration.DefaultRuleset.Version.ToString(),
RuleId = ruleId, RuleId = r.FeatureId.ToString(),
Message = r.Message, Message = r.Message,
AppliesToMigrationTargetPlatform = migrationResult.AppliesToMigrationTargetPlatform.ToString(), AppliesToMigrationTargetPlatform = r.AppliesToMigrationTargetPlatform.ToString(),
IssueCategory = "Category_Unknown" IssueCategory = r.IssueCategory.ToString(),
ImpactedObjects = ParseImpactedObjects(r.ImpactedObjects)
}; };
}).ToArray();
if (migrationResult.ImpactedObjects != null) }
internal ImpactedObjectInfo[] ParseImpactedObjects(IList<Microsoft.SqlServer.DataCollection.Common.Contracts.Advisor.Models.IImpactedObject> impactedObjects)
{
return impactedObjects.Select(i =>
{
return new ImpactedObjectInfo()
{ {
ImpactedObjectInfo[] impactedObjects = new ImpactedObjectInfo[migrationResult.ImpactedObjects.Count]; Name = i.Name,
for (int i = 0; i < migrationResult.ImpactedObjects.Count; ++i) ImpactDetail = i.ImpactDetail,
{ ObjectType = i.ObjectType
var impactedObject = new ImpactedObjectInfo() };
{ }).ToArray();
Name = migrationResult.ImpactedObjects[i].Name, }
ImpactDetail = migrationResult.ImpactedObjects[i].ImpactDetail,
ObjectType = migrationResult.ImpactedObjects[i].ObjectType
};
impactedObjects[i] = impactedObject;
}
item.ImpactedObjects = impactedObjects;
}
result.Add(item); internal string CreateAssessmentResultKey(ISqlMigrationAssessmentResult assessment)
} {
return result; return assessment.ServerName+assessment.DatabaseName+assessment.FeatureId.ToString()+assessment.IssueCategory.ToString()+assessment.Message + assessment.TargetType.ToString() + assessment.AppliesToMigrationTargetPlatform.ToString();
} }
/// <summary> /// <summary>
@@ -259,130 +309,5 @@ namespace Microsoft.SqlTools.ServiceLayer.Migration
disposed = true; disposed = true;
} }
} }
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool LogonUser(String lpszUsername, String lpszDomain, String lpszPassword,
int dwLogonType, int dwLogonProvider, out SafeAccessTokenHandle phToken);
internal async Task HandleValidateWindowsAccountRequest(
ValidateWindowsAccountRequestParams parameters,
RequestContext<bool> requestContext)
{
if (!ValidateWindowsDomainUsername(parameters.Username))
{
await requestContext.SendError("Invalid user name format. Example: Domain\\username");
return;
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
int separator = parameters.Username.IndexOf("\\");
string domainName = parameters.Username.Substring(0, separator);
string userName = parameters.Username.Substring(separator + 1, parameters.Username.Length - separator - 1);
const int LOGON32_PROVIDER_DEFAULT = 0;
const int LOGON32_LOGON_INTERACTIVE = 2;
SafeAccessTokenHandle safeAccessTokenHandle;
bool returnValue = LogonUser(userName, domainName, parameters.Password,
LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT,
out safeAccessTokenHandle);
if (!returnValue)
{
int ret = Marshal.GetLastWin32Error();
string errorMessage = new Win32Exception(Marshal.GetLastWin32Error()).Message;
await requestContext.SendError(errorMessage);
}
else
{
await requestContext.SendResult(true);
}
}
else
{
await requestContext.SendResult(true);
}
}
internal async Task HandleValidateNetworkFileShareRequest(
ValidateNetworkFileShareRequestParams parameters,
RequestContext<bool> requestContext)
{
if (!ValidateWindowsDomainUsername(parameters.Username))
{
await requestContext.SendError("Invalid user name format. Example: Domain\\username");
return;
}
if (!ValidateUNCPath(parameters.Path))
{
await requestContext.SendError("Invalid network share path. Example: \\\\Servername.domainname.com\\Backupfolder");
return;
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
int separator = parameters.Username.IndexOf("\\");
string domainName = parameters.Username.Substring(0, separator);
string userName = parameters.Username.Substring(separator + 1, parameters.Username.Length - separator - 1);
const int LOGON32_PROVIDER_WINNT50 = 3;
const int LOGON32_LOGON_NEW_CREDENTIALS = 9;
SafeAccessTokenHandle safeAccessTokenHandle;
bool returnValue = LogonUser(
userName,
domainName,
parameters.Password,
LOGON32_LOGON_NEW_CREDENTIALS,
LOGON32_PROVIDER_WINNT50,
out safeAccessTokenHandle);
if (!returnValue)
{
int ret = Marshal.GetLastWin32Error();
string errorMessage = new Win32Exception(Marshal.GetLastWin32Error()).Message;
await requestContext.SendError(errorMessage);
return;
}
await WindowsIdentity.RunImpersonated(
safeAccessTokenHandle,
// User action
async () =>
{
if(!Directory.Exists(parameters.Path)){
await requestContext.SendError("Cannot connect to file share");
} else {
await requestContext.SendResult(true);
}
}
);
}
else
{
await requestContext.SendResult(true);
}
}
/// <summary>
/// Check if the username is in 'domain\username' format.
/// </summary>
/// <returns></returns>
internal bool ValidateWindowsDomainUsername(string username)
{
var domainUserRegex = new Regex(@"^(?<domain>[A-Za-z0-9\._-]*)\\(?<username>[A-Za-z0-9\._-]*)$");
return domainUserRegex.IsMatch(username);
}
/// <summary>
/// Checks if the file path is in UNC format '\\Servername.domainname.com\Backupfolder'
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
internal bool ValidateUNCPath(string path)
{
return new Uri(path).IsUnc;
}
} }
} }