diff --git a/src/Microsoft.SqlTools.ServiceLayer/Migration/Contracts/MigrationAssessmentsRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/Migration/Contracts/MigrationAssessmentsRequest.cs index fd46743b..6b9befd7 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Migration/Contracts/MigrationAssessmentsRequest.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Migration/Contracts/MigrationAssessmentsRequest.cs @@ -3,8 +3,8 @@ // 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.SqlServer.Migration.Assessment.Common.Contracts.Models; namespace Microsoft.SqlTools.ServiceLayer.Migration.Contracts { @@ -16,20 +16,25 @@ namespace Microsoft.SqlTools.ServiceLayer.Migration.Contracts public class MigrationAssessmentResult { /// - /// Gets the collection of assessment results. + /// Errors that happen while running the assessment /// - public List Items { get; } = new List(); - + public ErrorModel[] Errors { get; set; } /// - /// Gets or sets a value indicating - /// if assessment operation was successful. + /// Result of the assessment /// - public bool Success { get; set; } - + public ServerAssessmentProperties AssessmentResult { get; set; } /// - /// Gets or sets an status message for the operation. + /// Start time of the assessment /// - public string ErrorMessage { get; set; } + public string StartTime { get; set; } + /// + /// End time of the assessment + /// + public string EndedTime { get; set; } + /// + /// Contains the raw assessment response + /// + public ISqlMigrationAssessmentModel RawAssessmentResult { get; set; } } /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/Migration/Contracts/MigrationObjectProperties.cs b/src/Microsoft.SqlTools.ServiceLayer/Migration/Contracts/MigrationObjectProperties.cs new file mode 100644 index 00000000..3ade4c56 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Migration/Contracts/MigrationObjectProperties.cs @@ -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 + { + /// + /// Name of the server + /// + public string Name { get; set; } + /// + /// Cpu cores for the server host + /// + public long CpuCoreCount { get; set; } + /// + /// Server host physical memory size + /// + public double PhysicalServerMemory { get; set; } + /// + /// Host operating system of the SQL server + /// + public string ServerHostPlatform { get; set; } + /// + /// Version of the SQL server + /// + public string ServerVersion { get; set; } + /// + /// SQL server engine edition + /// + public string ServerEngineEdition { get; set; } + /// + /// SQL server edition + /// + public string ServerEdition { get; set; } + /// + /// We use this flag to indicate if the SQL server is part of the failover cluster + /// + public bool IsClustered { get; set; } + /// + /// Returns the total number of dbs assessed + /// + public long NumberOfUserDatabases { get; set; } + /// + /// Returns the assessment status + /// + public int SqlAssessmentStatus { get; set; } + /// + /// Count of Dbs assessed + /// + public long AssessedDatabaseCount{get; set;} + /// + /// Give assessed server stats for SQL MI compatibility + /// + public IServerTargetReadiness SQLManagedInstanceTargetReadiness { get; set; } + /// + /// Server assessment results + /// + public MigrationAssessmentInfo[] Items { get; set; } + /// + /// Server assessment errors + /// + public ErrorModel[] Errors { get; set; } + /// + /// List of databases that are assessed + /// + public DatabaseAssessmentProperties[] Databases { get; set; } + } + + public class DatabaseAssessmentProperties + { + /// + /// Name of the database + /// + public string Name { get; set; } + /// + /// Compatibility level of the database + /// + public string CompatibilityLevel { get; set; } + /// + /// Size of the database + /// + public double DatabaseSize { get; set; } + /// + /// Flag that indicates if the database is replicated + /// + public bool IsReplicationEnabled { get; set; } + /// + /// Time taken for assessing the database + /// + public double AssessmentTimeInMilliseconds { get; set; } + /// + /// Database Assessment Results + /// + public MigrationAssessmentInfo[] Items { get; set; } + /// + /// Database assessment errors + /// + public ErrorModel[] Errors {get; set;} + /// + /// Flags that indicate if the database is ready for migration + /// + public IDatabaseTargetReadiness SQLManagedInstanceTargetReadiness { get; set; } + } + + public class ErrorModel + { + /// + /// Id of the assessment error + /// + public string ErrorId { get; set; } + /// + /// Error message + /// + public string Message { get; set; } + /// + /// Summary of the Error + /// + public string ErrorSummary { get; set; } + /// + /// Possible causes for the error + /// + public string PossibleCauses { get; set; } + /// + /// Possible mitigation for the error + /// + public string Guidance { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/Migration/MigrationService.cs b/src/Microsoft.SqlTools.ServiceLayer/Migration/MigrationService.cs index 0f1ec874..6afc85d3 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Migration/MigrationService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Migration/MigrationService.cs @@ -5,27 +5,23 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; 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; using Microsoft.SqlServer.Migration.Assessment.Common.Contracts.Models; using Microsoft.SqlServer.Migration.Assessment.Common.Engine; 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.Utility; +using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Hosting; using Microsoft.SqlTools.ServiceLayer.Migration.Contracts; using Microsoft.SqlTools.ServiceLayer.SqlAssessment; -using Microsoft.Win32.SafeHandles; -using Microsoft.SqlServer.DataCollection.Common; -using System.ComponentModel; -using System.Runtime.InteropServices; -using System.Text.RegularExpressions; -using System.Security.Principal; -using System.IO; +using Microsoft.SqlTools.Utility; + namespace Microsoft.SqlTools.ServiceLayer.Migration { /// @@ -95,8 +91,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Migration { this.ServiceHost = serviceHost; this.ServiceHost.SetRequestHandler(MigrationAssessmentsRequest.Type, HandleMigrationAssessmentsRequest); - this.ServiceHost.SetRequestHandler(ValidateWindowsAccountRequest.Type, HandleValidateWindowsAccountRequest); - this.ServiceHost.SetRequestHandler(ValidateNetworkFileShareRequest.Type, HandleValidateNetworkFileShareRequest); } /// @@ -143,10 +137,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Migration var db = SqlAssessmentService.GetDatabaseLocator(server, connection.Database); var connectionString = ConnectionService.BuildConnectionString(connInfo.ConnectionDetails); - var results = await GetAssessmentItems(server, connectionString); - var result = new MigrationAssessmentResult(); - result.Items.AddRange(results); - await requestContext.SendResult(result); + + var results = await GetAssessmentItems(connectionString); + await requestContext.SendResult(results); } catch (Exception e) { @@ -186,67 +179,124 @@ namespace Microsoft.SqlTools.ServiceLayer.Migration } } - internal async Task> GetAssessmentItems(SqlObjectLocator target, string connectionString) + internal async Task GetAssessmentItems(string connectionString) { SqlAssessmentConfiguration.EnableLocalLogging = true; SqlAssessmentConfiguration.EnableReportCreation = true; SqlAssessmentConfiguration.AssessmentReportAndLogsRootFolderPath = Path.GetDirectoryName(Logger.LogFileFullPath); DmaEngine engine = new DmaEngine(connectionString); var assessmentResults = await engine.GetTargetAssessmentResultsList(); - - var result = new List(); - foreach (var r in assessmentResults) + Dictionary assessmentResultLookup = new Dictionary(); + foreach (ISqlMigrationAssessmentResult r in assessmentResults) { - var migrationResult = r as ISqlMigrationAssessmentResult; - if (migrationResult == null) - { - continue; - } + assessmentResultLookup.Add(CreateAssessmentResultKey(r as ISqlMigrationAssessmentResult), r as ISqlMigrationAssessmentResult); + } + ISqlMigrationAssessmentModel contextualizedAssessmentResult = await engine.GetTargetAssessmentResultsList(System.Threading.CancellationToken.None); + 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) - ? $"{target.ServerName}:{migrationResult.DatabaseName}" - : target.Name; - var ruleId = migrationResult.FeatureId.ToString(); + internal ServerAssessmentProperties ParseServerAssessmentInfo(IServerAssessmentInfo server, Dictionary assessmentResultLookup) + { + return new ServerAssessmentProperties() + { + 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 databases, Dictionary assessmentResultLookup) + { + return databases.Select(d => + { + return new DatabaseAssessmentProperties() { - CheckId = r.Check.Id, - Description = r.Check.Description, - DisplayName = r.Check.DisplayName, - HelpLink = r.Check.HelpLink, - Level = r.Check.Level.ToString(), - TargetName = targetName, - DatabaseName = migrationResult.DatabaseName, - ServerName = migrationResult.ServerName, - Tags = r.Check.Tags.ToArray(), - TargetType = target.Type, + Name = d.Properties.Name, + CompatibilityLevel = d.Properties.CompatibilityLevel.ToString(), + DatabaseSize = d.Properties.SizeMB, + IsReplicationEnabled = d.Properties.IsReplicationEnabled, + AssessmentTimeInMilliseconds = d.Properties.TSqlScriptAnalysisTimeElapse.TotalMilliseconds, + Errors = ParseAssessmentError(d.Errors), + Items = ParseAssessmentResult(d.DatabaseAssessments, assessmentResultLookup), + SQLManagedInstanceTargetReadiness = d.TargetReadinesses[Microsoft.SqlServer.DataCollection.Common.Contracts.Advisor.TargetType.AzureSqlManagedInstance] + }; + }).ToArray(); + } + internal ErrorModel[] ParseAssessmentError(IList 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 assessmentResults, Dictionary 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, RulesetVersion = Engine.Configuration.DefaultRuleset.Version.ToString(), - RuleId = ruleId, + RuleId = r.FeatureId.ToString(), Message = r.Message, - AppliesToMigrationTargetPlatform = migrationResult.AppliesToMigrationTargetPlatform.ToString(), - IssueCategory = "Category_Unknown" + AppliesToMigrationTargetPlatform = r.AppliesToMigrationTargetPlatform.ToString(), + IssueCategory = r.IssueCategory.ToString(), + ImpactedObjects = ParseImpactedObjects(r.ImpactedObjects) }; - - if (migrationResult.ImpactedObjects != null) + }).ToArray(); + } + internal ImpactedObjectInfo[] ParseImpactedObjects(IList impactedObjects) + { + return impactedObjects.Select(i => + { + return new ImpactedObjectInfo() { - ImpactedObjectInfo[] impactedObjects = new ImpactedObjectInfo[migrationResult.ImpactedObjects.Count]; - for (int i = 0; i < migrationResult.ImpactedObjects.Count; ++i) - { - var impactedObject = new ImpactedObjectInfo() - { - Name = migrationResult.ImpactedObjects[i].Name, - ImpactDetail = migrationResult.ImpactedObjects[i].ImpactDetail, - ObjectType = migrationResult.ImpactedObjects[i].ObjectType - }; - impactedObjects[i] = impactedObject; - } - item.ImpactedObjects = impactedObjects; - } + Name = i.Name, + ImpactDetail = i.ImpactDetail, + ObjectType = i.ObjectType + }; + }).ToArray(); + } - result.Add(item); - } - return result; + internal string CreateAssessmentResultKey(ISqlMigrationAssessmentResult assessment) + { + return assessment.ServerName+assessment.DatabaseName+assessment.FeatureId.ToString()+assessment.IssueCategory.ToString()+assessment.Message + assessment.TargetType.ToString() + assessment.AppliesToMigrationTargetPlatform.ToString(); } /// @@ -259,130 +309,5 @@ namespace Microsoft.SqlTools.ServiceLayer.Migration 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 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 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); - } - } - - /// - /// Check if the username is in 'domain\username' format. - /// - /// - internal bool ValidateWindowsDomainUsername(string username) - { - var domainUserRegex = new Regex(@"^(?[A-Za-z0-9\._-]*)\\(?[A-Za-z0-9\._-]*)$"); - return domainUserRegex.IsMatch(username); - } - - - /// - /// Checks if the file path is in UNC format '\\Servername.domainname.com\Backupfolder' - /// - /// - /// - internal bool ValidateUNCPath(string path) - { - return new Uri(path).IsUnc; - } } }