diff --git a/src/Microsoft.SqlTools.ServiceLayer/Admin/AdminService.cs b/src/Microsoft.SqlTools.ServiceLayer/Admin/AdminService.cs index 316cad97..56a2bc78 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Admin/AdminService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Admin/AdminService.cs @@ -190,12 +190,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin } /// - /// Create database task helper + /// Create a data container object /// /// connection info /// flag indicating whether to create taskhelper for existing database or not - /// - internal static DatabaseTaskHelper CreateDatabaseTaskHelper(ConnectionInfo connInfo, bool databaseExists = false) + internal static CDataContainer CreateDataContainer(ConnectionInfo connInfo, bool databaseExists = false) { XmlDocument xmlDoc = CreateDataContainerDocument(connInfo, databaseExists); CDataContainer dataContainer; @@ -232,6 +231,18 @@ namespace Microsoft.SqlTools.ServiceLayer.Admin xmlDoc.InnerXml); } + return dataContainer; + } + + /// + /// Create database task helper + /// + /// connection info + /// flag indicating whether to create taskhelper for existing database or not + /// + internal static DatabaseTaskHelper CreateDatabaseTaskHelper(ConnectionInfo connInfo, bool databaseExists = false) + { + var dataContainer = CreateDataContainer(connInfo, databaseExists); var taskHelper = new DatabaseTaskHelper(dataContainer); return taskHelper; } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Admin/Common/IManagedConnection.cs b/src/Microsoft.SqlTools.ServiceLayer/Admin/Common/IManagedConnection.cs index fcb9b5f4..f078637b 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Admin/Common/IManagedConnection.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Admin/Common/IManagedConnection.cs @@ -1,4 +1,9 @@ -using System; +// +// 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; using Microsoft.SqlServer.Management.Common; using Microsoft.SqlServer.Management.Diagnostics; diff --git a/src/Microsoft.SqlTools.ServiceLayer/Admin/Common/Utils.cs b/src/Microsoft.SqlTools.ServiceLayer/Admin/Common/Utils.cs index 104fd42c..c53c2eeb 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Admin/Common/Utils.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Admin/Common/Utils.cs @@ -1,6 +1,7 @@ -//------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. All rights reserved. -//------------------------------------------------------------ +// +// 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.Text; diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/AgentService.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/AgentService.cs index 0f6c545b..d94698b0 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Agent/AgentService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/AgentService.cs @@ -13,6 +13,7 @@ using Microsoft.SqlServer.Management.Common; using Microsoft.SqlServer.Management.Smo; using Microsoft.SqlServer.Management.Smo.Agent; using Microsoft.SqlTools.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.Admin; using Microsoft.SqlTools.ServiceLayer.Agent.Contracts; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Hosting; @@ -21,10 +22,17 @@ using Microsoft.SqlTools.Utility; namespace Microsoft.SqlTools.ServiceLayer.Agent { /// - /// Main class for Profiler Service functionality + /// Main class for Agent Service functionality /// - public sealed class AgentService + public class AgentService { + internal enum AgentConfigAction + { + Create, + Update, + Drop + } + private Dictionary jobs = null; private ConnectionService connectionService = null; private static readonly Lazy instance = new Lazy(() => new AgentService()); @@ -64,7 +72,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Agent } } - /// /// Service host object for sending/receiving requests/events. /// Internal for testing purposes. @@ -84,47 +91,70 @@ namespace Microsoft.SqlTools.ServiceLayer.Agent this.ServiceHost.SetRequestHandler(AgentJobsRequest.Type, HandleAgentJobsRequest); this.ServiceHost.SetRequestHandler(AgentJobHistoryRequest.Type, HandleJobHistoryRequest); this.ServiceHost.SetRequestHandler(AgentJobActionRequest.Type, HandleJobActionRequest); + + // Alerts request handlers + this.ServiceHost.SetRequestHandler(AgentAlertsRequest.Type, HandleAgentAlertsRequest); + this.ServiceHost.SetRequestHandler(CreateAgentAlertRequest.Type, HandleCreateAgentAlertRequest); + this.ServiceHost.SetRequestHandler(UpdateAgentAlertRequest.Type, HandleUpdateAgentAlertRequest); + this.ServiceHost.SetRequestHandler(DeleteAgentAlertRequest.Type, HandleDeleteAgentAlertRequest); + + // Operators request handlers + this.ServiceHost.SetRequestHandler(AgentOperatorsRequest.Type, HandleAgentOperatorsRequest); + this.ServiceHost.SetRequestHandler(CreateAgentOperatorRequest.Type, HandleCreateAgentOperatorRequest); + this.ServiceHost.SetRequestHandler(UpdateAgentOperatorRequest.Type, HandleUpdateAgentOperatorRequest); + this.ServiceHost.SetRequestHandler(DeleteAgentOperatorRequest.Type, HandleDeleteAgentOperatorRequest); + + // Proxy Accounts request handlers + this.ServiceHost.SetRequestHandler(AgentProxiesRequest.Type, HandleAgentProxiesRequest); + this.ServiceHost.SetRequestHandler(CreateAgentProxyRequest.Type, HandleCreateAgentProxyRequest); + this.ServiceHost.SetRequestHandler(UpdateAgentProxyRequest.Type, HandleUpdateAgentProxyRequest); + this.ServiceHost.SetRequestHandler(DeleteAgentProxyRequest.Type, HandleDeleteAgentProxyRequest); } + + #region "Jobs Handlers" /// /// Handle request to get Agent job activities /// internal async Task HandleAgentJobsRequest(AgentJobsParams parameters, RequestContext requestContext) { - try + await Task.Run(async () => { - var result = new AgentJobsResult(); - ConnectionInfo connInfo; - ConnectionServiceInstance.TryFindConnection( - parameters.OwnerUri, - out connInfo); - - if (connInfo != null) + try { - var sqlConnection = ConnectionService.OpenSqlConnection(connInfo); - var serverConnection = new ServerConnection(sqlConnection); - var fetcher = new JobFetcher(serverConnection); - var filter = new JobActivityFilter(); - this.jobs = fetcher.FetchJobs(filter); - var agentJobs = new List(); - if (this.jobs != null) + var result = new AgentJobsResult(); + ConnectionInfo connInfo; + ConnectionServiceInstance.TryFindConnection( + parameters.OwnerUri, + out connInfo); + + if (connInfo != null) { - - foreach (var job in this.jobs.Values) + var sqlConnection = ConnectionService.OpenSqlConnection(connInfo); + var serverConnection = new ServerConnection(sqlConnection); + var fetcher = new JobFetcher(serverConnection); + var filter = new JobActivityFilter(); + this.jobs = fetcher.FetchJobs(filter); + var agentJobs = new List(); + if (this.jobs != null) { - agentJobs.Add(JobUtilities.ConvertToAgentJobInfo(job)); + + foreach (var job in this.jobs.Values) + { + agentJobs.Add(JobUtilities.ConvertToAgentJobInfo(job)); + } } + result.Succeeded = true; + result.Jobs = agentJobs.ToArray(); + sqlConnection.Close(); } - result.Succeeded = true; - result.Jobs = agentJobs.ToArray(); - sqlConnection.Close(); + await requestContext.SendResult(result); } - await requestContext.SendResult(result); - } - catch (Exception e) - { - await requestContext.SendError(e); - } + catch (Exception e) + { + await requestContext.SendError(e); + } + }); } /// @@ -132,44 +162,47 @@ namespace Microsoft.SqlTools.ServiceLayer.Agent /// internal async Task HandleJobHistoryRequest(AgentJobHistoryParams parameters, RequestContext requestContext) { - try + await Task.Run(async () => { - var result = new AgentJobHistoryResult(); - ConnectionInfo connInfo; - ConnectionServiceInstance.TryFindConnection( - parameters.OwnerUri, - out connInfo); - if (connInfo != null) + try { - Tuple tuple = CreateSqlConnection(connInfo, parameters.JobId); - SqlConnectionInfo sqlConnInfo = tuple.Item1; - DataTable dt = tuple.Item2; - ServerConnection connection = tuple.Item3; - int count = dt.Rows.Count; - List jobHistories = new List(); - if (count > 0) + var result = new AgentJobHistoryResult(); + ConnectionInfo connInfo; + ConnectionServiceInstance.TryFindConnection( + parameters.OwnerUri, + out connInfo); + if (connInfo != null) { - var job = dt.Rows[0]; - string jobName = Convert.ToString(job[JobUtilities.UrnJobName], System.Globalization.CultureInfo.InvariantCulture); - Guid jobId = (Guid) job[JobUtilities.UrnJobId]; - int runStatus = Convert.ToInt32(job[JobUtilities.UrnRunStatus], System.Globalization.CultureInfo.InvariantCulture); - var t = new LogSourceJobHistory(jobName, sqlConnInfo, null, runStatus, jobId, null); - var tlog = t as ILogSource; - tlog.Initialize(); - var logEntries = t.LogEntries; - jobHistories = JobUtilities.ConvertToAgentJobHistoryInfo(logEntries, job); - tlog.CloseReader(); + Tuple tuple = CreateSqlConnection(connInfo, parameters.JobId); + SqlConnectionInfo sqlConnInfo = tuple.Item1; + DataTable dt = tuple.Item2; + ServerConnection connection = tuple.Item3; + int count = dt.Rows.Count; + List jobHistories = new List(); + if (count > 0) + { + var job = dt.Rows[0]; + string jobName = Convert.ToString(job[JobUtilities.UrnJobName], System.Globalization.CultureInfo.InvariantCulture); + Guid jobId = (Guid) job[JobUtilities.UrnJobId]; + int runStatus = Convert.ToInt32(job[JobUtilities.UrnRunStatus], System.Globalization.CultureInfo.InvariantCulture); + var t = new LogSourceJobHistory(jobName, sqlConnInfo, null, runStatus, jobId, null); + var tlog = t as ILogSource; + tlog.Initialize(); + var logEntries = t.LogEntries; + jobHistories = JobUtilities.ConvertToAgentJobHistoryInfo(logEntries, job); + tlog.CloseReader(); + } + result.Jobs = jobHistories.ToArray(); + result.Succeeded = true; + connection.Disconnect(); + await requestContext.SendResult(result); } - result.Jobs = jobHistories.ToArray(); - result.Succeeded = true; - connection.Disconnect(); - await requestContext.SendResult(result); } - } - catch (Exception e) - { - await requestContext.SendError(e); - } + catch (Exception e) + { + await requestContext.SendError(e); + } + }); } /// @@ -177,49 +210,52 @@ namespace Microsoft.SqlTools.ServiceLayer.Agent /// internal async Task HandleJobActionRequest(AgentJobActionParams parameters, RequestContext requestContext) { - var result = new AgentJobActionResult(); - try + await Task.Run(async () => { - ConnectionInfo connInfo; - ConnectionServiceInstance.TryFindConnection( - parameters.OwnerUri, - out connInfo); - if (connInfo != null) + var result = new AgentJobActionResult(); + try { - var sqlConnection = ConnectionService.OpenSqlConnection(connInfo); - var serverConnection = new ServerConnection(sqlConnection); - var jobHelper = new JobHelper(serverConnection); - jobHelper.JobName = parameters.JobName; - switch(parameters.Action) + ConnectionInfo connInfo; + ConnectionServiceInstance.TryFindConnection( + parameters.OwnerUri, + out connInfo); + if (connInfo != null) { - case "run": - jobHelper.Start(); - break; - case "stop": - jobHelper.Stop(); - break; - case "delete": - jobHelper.Delete(); - break; - case "enable": - jobHelper.Enable(true); - break; - case "disable": - jobHelper.Enable(false); - break; - default: - break; + var sqlConnection = ConnectionService.OpenSqlConnection(connInfo); + var serverConnection = new ServerConnection(sqlConnection); + var jobHelper = new JobHelper(serverConnection); + jobHelper.JobName = parameters.JobName; + switch(parameters.Action) + { + case "run": + jobHelper.Start(); + break; + case "stop": + jobHelper.Stop(); + break; + case "delete": + jobHelper.Delete(); + break; + case "enable": + jobHelper.Enable(true); + break; + case "disable": + jobHelper.Enable(false); + break; + default: + break; + } + result.Succeeded = true; + await requestContext.SendResult(result); } - result.Succeeded = true; + } + catch (Exception e) + { + result.Succeeded = false; + result.ErrorMessage = e.Message; await requestContext.SendResult(result); } - } - catch (Exception e) - { - result.Succeeded = false; - result.ErrorMessage = e.Message; - await requestContext.SendResult(result); - } + }); } private Tuple CreateSqlConnection(ConnectionInfo connInfo, String jobId) @@ -234,5 +270,268 @@ namespace Microsoft.SqlTools.ServiceLayer.Agent return new Tuple(sqlConnInfo, dt, serverConnection); } + #endregion // "Jobs Handlers" + + #region "Alert Handlers" + + /// + /// Handle request to get the alerts list + /// + internal async Task HandleAgentAlertsRequest(AgentAlertsParams parameters, RequestContext requestContext) + { + await Task.Run(async () => + { + var result = new AgentAlertsResult(); + result.Alerts = new List().ToArray(); + + ConnectionInfo connInfo; + ConnectionServiceInstance.TryFindConnection( + parameters.OwnerUri, + out connInfo); + + if (connInfo != null) + { + CDataContainer dataContainer = AdminService.CreateDataContainer(connInfo, databaseExists: true); + AlertCollection alerts = dataContainer.Server.JobServer.Alerts; + + + } + + await requestContext.SendResult(result); + }); + } + + private bool ValidateAgentAlertInfo(AgentAlertInfo alert) + { + return alert != null + && !string.IsNullOrWhiteSpace(alert.JobName); + } + + /// + /// Handle request to create an alert + /// + internal async Task HandleCreateAgentAlertRequest(CreateAgentAlertParams parameters, RequestContext requestContext) + { + await Task.Run(async () => + { + var result = new CreateAgentAlertResult(); + ConnectionInfo connInfo; + ConnectionServiceInstance.TryFindConnection( + parameters.OwnerUri, + out connInfo); + + CreateOrUpdateAgentAlert(connInfo, parameters.Alert); + + await requestContext.SendResult(result); + }); + } + + /// + /// Handle request to update an alert + /// + internal async Task HandleUpdateAgentAlertRequest(UpdateAgentAlertParams parameters, RequestContext requestContext) + { + await Task.Run(async () => + { + var result = new UpdateAgentAlertResult(); + ConnectionInfo connInfo; + ConnectionServiceInstance.TryFindConnection( + parameters.OwnerUri, + out connInfo); + + CreateOrUpdateAgentAlert(connInfo, parameters.Alert); + + await requestContext.SendResult(result); + }); + } + + private void CreateOrUpdateAgentAlert(ConnectionInfo connInfo, AgentAlertInfo alert) + { + if (connInfo != null && ValidateAgentAlertInfo(alert)) + { + CDataContainer dataContainer = AdminService.CreateDataContainer(connInfo, databaseExists: true); + STParameters param = new STParameters(dataContainer.Document); + param.SetParam("alert", alert.JobName); + + using (AgentAlert agentAlert = new AgentAlert(dataContainer, alert)) + { + agentAlert.CreateOrUpdate(); + } + } + } + + /// + /// Handle request to delete an alert + /// + internal async Task HandleDeleteAgentAlertRequest(DeleteAgentAlertParams parameters, RequestContext requestContext) + { + await Task.Run(async () => + { + var result = new DeleteAgentAlertResult(); + ConnectionInfo connInfo; + ConnectionServiceInstance.TryFindConnection( + parameters.OwnerUri, + out connInfo); + + AgentAlertInfo alert = parameters.Alert; + if (connInfo != null && ValidateAgentAlertInfo(alert)) + { + CDataContainer dataContainer = AdminService.CreateDataContainer(connInfo, databaseExists: true); + STParameters param = new STParameters(dataContainer.Document); + param.SetParam("alert", alert.JobName); + + using (AgentAlert agentAlert = new AgentAlert(dataContainer, alert)) + { + agentAlert.Drop(); + } + } + + await requestContext.SendResult(result); + }); + } + + #endregion // "Alert Handlers" + + #region "Operator Handlers" + + internal async Task HandleAgentOperatorsRequest(AgentOperatorsParams parameters, RequestContext requestContext) + { + await requestContext.SendResult(null); + } + + internal async Task HandleCreateAgentOperatorRequest(CreateAgentOperatorParams parameters, RequestContext requestContext) + { + await Task.Run(async () => + { + var result = new CreateAgentOperatorResult(); + ConnectionInfo connInfo; + ConnectionServiceInstance.TryFindConnection( + parameters.OwnerUri, + out connInfo); + + AgentOperatorInfo operatorInfo = parameters.Operator; + CDataContainer dataContainer = AdminService.CreateDataContainer(connInfo, databaseExists: true); + STParameters param = new STParameters(dataContainer.Document); + param.SetParam("operator", operatorInfo.Name); + + using (AgentOperator agentOperator = new AgentOperator(dataContainer, operatorInfo)) + { + agentOperator.CreateOrUpdate(); + } + + await requestContext.SendResult(result); + }); + } + + internal async Task HandleUpdateAgentOperatorRequest(UpdateAgentOperatorParams parameters, RequestContext requestContext) + { + await requestContext.SendResult(null); + } + + internal async Task HandleDeleteAgentOperatorRequest(DeleteAgentOperatorParams parameters, RequestContext requestContext) + { + await requestContext.SendResult(null); + } + + #endregion // "Operator Handlers" + + + #region "Proxy Handlers" + + internal async Task HandleAgentProxiesRequest(AgentProxiesParams parameters, RequestContext requestContext) + { + await requestContext.SendResult(null); + } + + internal async Task HandleCreateAgentProxyRequest(CreateAgentProxyParams parameters, RequestContext requestContext) + { + bool succeeded = await ConfigureAgentProxy( + parameters.OwnerUri, + parameters.Proxy.AccountName, + parameters.Proxy, + AgentConfigAction.Create); + + await requestContext.SendResult(new CreateAgentProxyResult() + { + Succeeded = succeeded + }); + } + + internal async Task HandleUpdateAgentProxyRequest(UpdateAgentProxyParams parameters, RequestContext requestContext) + { + bool succeeded = await ConfigureAgentProxy( + parameters.OwnerUri, + parameters.OriginalProxyName, + parameters.Proxy, + AgentConfigAction.Update); + + await requestContext.SendResult(new UpdateAgentProxyResult() + { + Succeeded = succeeded + }); + } + + internal async Task HandleDeleteAgentProxyRequest(DeleteAgentProxyParams parameters, RequestContext requestContext) + { + bool succeeded = await ConfigureAgentProxy( + parameters.OwnerUri, + parameters.Proxy.AccountName, + parameters.Proxy, + AgentConfigAction.Drop); + + await requestContext.SendResult(new DeleteAgentProxyResult() + { + Succeeded = succeeded + }); + } + + internal async Task ConfigureAgentProxy( + string ownerUri, + string accountName, + AgentProxyInfo proxy, + AgentConfigAction configAction) + { + return await Task.Run(() => + { + try + { + ConnectionInfo connInfo; + ConnectionServiceInstance.TryFindConnection( + ownerUri, + out connInfo); + + CDataContainer dataContainer = AdminService.CreateDataContainer(connInfo, databaseExists: true); + STParameters param = new STParameters(dataContainer.Document); + param.SetParam("proxyaccount", accountName); + + using (AgentProxyAccount agentProxy = new AgentProxyAccount(dataContainer, proxy)) + { + if (configAction == AgentConfigAction.Create) + { + return agentProxy.Create(); + } + else if (configAction == AgentConfigAction.Update) + { + return agentProxy.Update(); + } + else if (configAction == AgentConfigAction.Drop) + { + return agentProxy.Drop(); + } + else + { + return false; + } + } + } + catch (Exception) + { + // log exception here + return false; + } + }); + } + + #endregion // "Proxy Handlers" } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/AgentActions.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/AgentActions.cs new file mode 100644 index 00000000..d4380b21 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/AgentActions.cs @@ -0,0 +1,976 @@ +// +// 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.Data; +using System.Drawing; +using System.Threading; +using System.Collections; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Xml; +using Microsoft.SqlServer.Management.Sdk.Sfc; +using Microsoft.SqlServer.Management.Common; +using Microsoft.SqlServer.Management.Diagnostics; +using Microsoft.SqlServer.Management.Smo; +using Microsoft.SqlServer.Management.Smo.Agent; +using Microsoft.SqlServer.Management.UI; +using Microsoft.SqlTools.ServiceLayer.Admin; + +namespace Microsoft.SqlTools.ServiceLayer.Agent +{ + + #region AgentAction class + + /// + /// Main class all af the "immediate" agent actions derive from. These actions execute immediately + /// are not scriptable. We use the progress reporting dialog to give the user feedback on progress + /// etc. + /// + internal abstract class AgentAction + { + #region private members + + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + protected Microsoft.SqlServer.Management.Smo.Server smoServer = null; + protected IManagedConnection managedConnection; + protected Urn[] urnParameters; + protected STParameters param = null; + protected ProgressItemCollection actions = new ProgressItemCollection(); + + #endregion + + protected object ActionObject; + + #region construction + + public AgentAction(XmlDocument document, IServiceProvider source) + : this(document, source, null) + { + } + + public AgentAction(XmlDocument document, IServiceProvider source, object actionObject) + { + // parameter check + if (document == null) + { + throw new ArgumentNullException("document"); + } + + if (source == null) + { + throw new ArgumentNullException("source"); + } + + if (actionObject != null) + { + this.ActionObject = actionObject; + } + + // get the managed connection + managedConnection = source.GetService(typeof (IManagedConnection)) as IManagedConnection; + + // get the connection + SqlOlapConnectionInfoBase ci = managedConnection.Connection; + // get the server connection + ServerConnection serverConnection = + ((SqlConnectionInfoWithConnection) managedConnection.Connection).ServerConnection; + + smoServer = new Microsoft.SqlServer.Management.Smo.Server(serverConnection); + + // get the list or urn's that have been passed in + param = new STParameters(document); + StringCollection urnStrings = new StringCollection(); + + // get a list of urns that have been passed in. + param.GetParam("urn", urnStrings); + + // store the Urn's as real Urns + urnParameters = new Urn[urnStrings.Count]; + for (int i = 0; i < urnStrings.Count; i++) + { + urnParameters[i] = new Urn(urnStrings[i]); + } + } + + protected void OnLoad(EventArgs e) + { + // ask derived classes to build a list of actions to be + // performed. Do not remove this call from OnLoad method! + GenerateActions(); + } + + #endregion + + #region cleanup + + /// + /// Clean up any resources being used. + /// + protected void Dispose(bool disposing) + { + if (disposing) + { + if (components != null) + { + components.Dispose(); + } + } + + if (this.managedConnection != null) + { + try + { + if (disposing) + { + this.managedConnection.Close(); + } + this.managedConnection = null; + } + catch (Exception) + { + } + } + } + + #endregion + + #region abstract methods + + /// + /// Generate the actions the dialog will perform. Derived classes should add + /// IAction based actions to the actions collection. + /// + protected abstract void GenerateActions(); + + #endregion + } + + #endregion + + #region Enable Alerts + + /// + /// Enables one or more alerts. + /// + internal class EnableAgentAlerts : AgentAction + { + public EnableAgentAlerts(XmlDocument document, IServiceProvider source) + : base(document, source) + { + } + + protected override void GenerateActions() + { + if (this.smoServer != null) + { + for (int i = 0; i < this.urnParameters.Length; i++) + { + Alert alert = this.smoServer.GetSmoObject(urnParameters[i]) as Alert; + + // check that the urn really points to an alert + this.actions.AddAction(new EnableAlertAction(alert)); + } + } + } + + /// + /// Performs the actual enabling + /// + internal class EnableAlertAction : IProgressItem + { + private Alert alert; + + public EnableAlertAction(Alert alert) + { + if (alert == null) + { + throw new ArgumentNullException("alert"); + } + + this.alert = alert; + } + + /// + /// Generate a user friendly description of this task.Used in the description + /// of the progress dialog. + /// + /// Description of the aler + public override string ToString() + { + if (this.alert == null) + { + return base.ToString(); + } + else + { + return "AgentActionSR.EnableAlertDescription(this.alert.Name)"; + } + } + + + /// + /// Enable the alert + /// + /// Actions collection + /// this actions index into the actions collection + /// + public ProgressStatus DoAction(ProgressItemCollection actions, int index) + { + //parameter check + if (actions == null) + { + throw new ArgumentNullException("actions"); + } + + // in progress + actions.Progress.UpdateActionStatus(index, ProgressStatus.InProgress); + actions.Progress.AddActionInfoString(index, "AgentActionSR.EnablingAlert(this.alert.Name)"); + + this.alert.IsEnabled = true; + this.alert.Alter(); + + // done + actions.Progress.AddActionInfoString(index, "AgentActionSR.EnabledAlert(this.alert.Name)"); + actions.Progress.UpdateActionStatus(index, ProgressStatus.Success); + return ProgressStatus.Success; + } + } + } + + #endregion + + #region Disable Alerts + + /// + /// Disable one or more alerts + /// + internal class DisableAgentAlerts : AgentAction + { + public DisableAgentAlerts(XmlDocument document, IServiceProvider source) + : base(document, source) + { + } + + protected override void GenerateActions() + { + if (this.smoServer != null) + { + for (int i = 0; i < this.urnParameters.Length; i++) + { + Alert alert = this.smoServer.GetSmoObject(urnParameters[i]) as Alert; + + // check that the urn really points to an alert + this.actions.AddAction(new DisableAlertAction(alert)); + } + } + } + + /// + /// Actually disable the alert + /// + internal class DisableAlertAction : IProgressItem + { + private Alert alert; + + public DisableAlertAction(Alert alert) + { + if (alert == null) + { + throw new ArgumentNullException("alert"); + } + + this.alert = alert; + } + + public override string ToString() + { + if (this.alert == null) + { + return base.ToString(); + } + else + { + return "AgentActionSR.DisableAlertDescription(this.alert.Name)"; + } + } + + /// + /// Disable the alert + /// + /// Actions collection + /// this actions index into the actions collection + /// + public ProgressStatus DoAction(ProgressItemCollection actions, int index) + { + //parameter check + if (actions == null) + { + throw new ArgumentNullException("actions"); + } + + // in progress + actions.Progress.UpdateActionStatus(index, ProgressStatus.InProgress); + actions.Progress.AddActionInfoString(index, "AgentActionSR.DisablingAlert(this.alert.Name)"); + + this.alert.IsEnabled = false; + this.alert.Alter(); + + actions.Progress.AddActionInfoString(index, "AgentActionSR.DisabledAlert(this.alert.Name)"); + + /// done + actions.Progress.UpdateActionStatus(index, ProgressStatus.Success); + return ProgressStatus.Success; + } + } + } + + #endregion + + #region JobAction + + internal class JobAction : AgentAction + { + public JobAction(XmlDocument document, IServiceProvider source) + : base(document, source) + { + + } + + protected override void GenerateActions() + { + return; + } + + /// + /// Initialize context for actions on Jobs + /// Job Activity monitor can call with list if jobids. + /// All other existing callers may call with list of urns + /// To support above 2 scenarios, this method in base class initializes urnParameters if it was not initialized by + /// AgentAction class's constructor + /// + protected void InitializeContext() + { + // If Urn parameters were not initialized it is possible that + // jobids were passed in by caller instead of list of urns + if (null == urnParameters || urnParameters.Length == 0) + { + StringCollection jobIdStrings = new StringCollection(); + + // get list of job ids that were passed in + param.GetParam("jobid", jobIdStrings); + + urnParameters = new Urn[jobIdStrings.Count]; + int index = 0; + if (jobIdStrings.Count > 0) + { + foreach (string jobIdString in jobIdStrings) + { + Job job = smoServer.JobServer.Jobs.ItemById(Guid.Parse(jobIdString)); + urnParameters[index++] = new Urn(job.Urn); + } + } + } + } + } + + #endregion + + #region Enable Jobs + + internal class EnableAgentJobs : JobAction + { + public EnableAgentJobs(XmlDocument document, IServiceProvider source) + : base(document, source) + { + } + + protected override void GenerateActions() + { + if (this.smoServer != null) + { + InitializeContext(); + for (int i = 0; i < this.urnParameters.Length; i++) + { + Job job = this.smoServer.GetSmoObject(urnParameters[i]) as Job; + + // check that the urn really points to a Job + this.actions.AddAction(new EnableJobAction(job)); + } + } + } + + // class that actually enables the job + internal class EnableJobAction : IProgressItem + { + private Job job; + + public EnableJobAction(Job job) + { + if (job == null) + { + throw new ArgumentNullException("job"); + } + + this.job = job; + } + + /// + /// Generate user friendly description of the action. This is displayed in the + /// progress dialog. + /// + /// + public override string ToString() + { + if (this.job == null) + { + return base.ToString(); + } + else + { + return "AgentActionSR.EnableJobDescription(this.job.Name)"; + } + } + + /// + /// Enable the Job + /// + /// Actions collection + /// this actions index into the actions collection + /// + public ProgressStatus DoAction(ProgressItemCollection actions, int index) + { + //parameter check + if (actions == null) + { + throw new ArgumentNullException("actions"); + } + + // in progress + actions.Progress.UpdateActionStatus(index, ProgressStatus.InProgress); + actions.Progress.AddActionInfoString(index, "AgentActionSR.EnablingJob(this.job.Name)"); + + this.job.IsEnabled = true; + this.job.Alter(); + + + // done + actions.Progress.AddActionInfoString(index, "AgentActionSR.EnabledJob(this.job.Name)"); + actions.Progress.UpdateActionStatus(index, ProgressStatus.Success); + return ProgressStatus.Success; + } + } + } + + #endregion + + #region Disable Jobs + + /// + /// Disable a job + /// + internal class DisableAgentJobs : JobAction + { + public DisableAgentJobs(XmlDocument document, IServiceProvider source) + : base(document, source) + { + } + + protected override void GenerateActions() + { + if (this.smoServer != null) + { + InitializeContext(); + for (int i = 0; i < this.urnParameters.Length; i++) + { + Job job = this.smoServer.GetSmoObject(urnParameters[i]) as Job; + + // check that the urn really points to an job + this.actions.AddAction(new DisableJobAction(job)); + } + } + } + + internal class DisableJobAction : IProgressItem + { + private Job job; + + public DisableJobAction(Job job) + { + if (job == null) + { + throw new ArgumentNullException("job"); + } + + this.job = job; + } + + public override string ToString() + { + if (this.job == null) + { + return base.ToString(); + } + else + { + return "AgentActionSR.DisableJobDescription(this.job.Name)"; + } + } + + /// + /// Disable the Job + /// + /// Actions collection + /// this actions index into the actions collection + /// + public ProgressStatus DoAction(ProgressItemCollection actions, int index) + { + //parameter check + if (actions == null) + { + throw new ArgumentNullException("actions"); + } + + // in progress + actions.Progress.UpdateActionStatus(index, ProgressStatus.InProgress); + actions.Progress.AddActionInfoString(index, "AgentActionSR.DisablingJob(this.job.Name)"); + + this.job.IsEnabled = false; + this.job.Alter(); + + // done + actions.Progress.AddActionInfoString(index, "AgentActionSR.DisabledJob(this.job.Name)"); + actions.Progress.UpdateActionStatus(index, ProgressStatus.Success); + return ProgressStatus.Success; + } + } + } + + #endregion + + #region Start Job + + /// + /// Start an agent job. If the jobs have multiple steps we will show a dialog that asks + /// which step the job should be started on. + /// + internal class StartAgentJobs : JobAction + { + public StartAgentJobs(XmlDocument document, IServiceProvider source) + : base(document, source) + { + this.actions.CloseOnUserCancel = true; + this.actions.QuitOnError = true; + + } + + /// + /// The method is generates list of actions and it is gets called from the OnLaod of base Form method + /// + protected override void GenerateActions() + { + if (this.smoServer != null) + { + InitializeContext(); + + for (int i = 0; i < this.urnParameters.Length; i++) + { + Job job = this.smoServer.GetSmoObject(urnParameters[i]) as Job; + + + string selectedStep = null; + + DataTable dtSteps = GetJobDataSteps(job); + if (dtSteps == null || dtSteps.Rows == null) + continue; + + if (dtSteps.Rows.Count > 1) //check if job is multi step job + { + // selectedStep = ShowStepDialog(job, dtSteps); + if (selectedStep == null) //check if the job was canceled + continue; + } + + //Copy the LastRunTime of the job into prevRunTime before the job started. + DateTime prevRunTime = job.LastRunDate; + this.actions.AddAction(new StartJobAction(job, selectedStep)); + this.actions.AddAction(new WaitForJobToFinishAction(job, prevRunTime)); + } + + if (this.actions.Count <= 0) + { + //This is a workaround for the class dialog invocation problem + //This dialog is invoked from two places + //JobsActivityMonitor (JobsPanel.cs) OnStartJobAtStep method + // and SSMS Context menu mpu\ssms\shared\SqlMgmt\src\RunningFormsTable.cs method StartFormExecution + //It is no way for us to prevent the dialog execution from the context menu (when job was canceled), besides redisgning the architecture + //this.Close(); + } + } + } + + /// + /// Returns list of steps of the given job + /// + /// + /// returns list of steps + private DataTable GetJobDataSteps(Job job) + { + if (job == null || job.Parent == null || job.Parent.Parent == null) + return null; + + // perform an enumerator query to get the steps. We could use the + // SMO step object but this is too inefficient as it generates a batch + // per step. + Request request = new Request(); + + request.Fields = new string[] {"Name", "ID", "SubSystem"}; + request.Urn = job.Urn + "/Step"; + request.OrderByList = new OrderBy[] {new OrderBy("ID", OrderBy.Direction.Asc)}; + + Enumerator en = new Enumerator(); + return en.Process(job.Parent.Parent.ConnectionContext, request); + } + + /// + /// This class implements the feature described in project tracking bug 37519. + /// The point is to poll the server for the status of the job to give the user + /// some indication of whether the job succeeded or failed. Polls every 3 seconds. + /// + internal class WaitForJobToFinishAction : IProgressItem + { + private Job job; + private DateTime prevRunTime; + private ManualResetEvent abortEvent; + private const int ServerPollingInterval = 3000; + + public WaitForJobToFinishAction(Job job, DateTime prevRunTime) + { + this.job = job; + this.prevRunTime = prevRunTime; + this.abortEvent = new ManualResetEvent(false); //initial set to busy + } + + /// + /// Prevent default constructor + /// + private WaitForJobToFinishAction() + { + } + + /// + /// generates a friendly description of this step. Used by the progress dialog + /// + /// + public override string ToString() + { + if (this.job == null) + { + return base.ToString(); + } + else + { + return "AgentActionSR.ExecuteJob(job.Name)"; + } + } + + /// + /// This method triggers abort event for the action thread + /// + public void Abort() + { + this.abortEvent.Set(); + } + + /// + /// Perform the action for this class + /// + /// Actions collection + /// array index of this particular action + /// + public ProgressStatus DoAction(ProgressItemCollection actions, int index) + { + ProgressStatus status = ProgressStatus.Error; + + bool jobFinished = false; + + JobServer jobServer = job.Parent; + JobCategory category = jobServer.JobCategories[job.Category]; + + if (category.CategoryType == CategoryType.MultiServerJob) + { + actions.Progress.UpdateActionDescription(index, "AgentActionSR.RequestPostedToTargetServers"); + actions.Progress.UpdateActionStatus(index, ProgressStatus.Success); + return ProgressStatus.Success; + } + + status = ProgressStatus.Aborted; + + // now wait for job to finish... + while (!this.abortEvent.WaitOne(WaitForJobToFinishAction.ServerPollingInterval)) + { + if (actions.Progress.IsAborted) + break; + + this.job.Refresh(); + // If this job hasn't started yet then don't check for its status + if (this.prevRunTime != job.LastRunDate) + { + switch (this.job.CurrentRunStatus) + { + case JobExecutionStatus.Idle: + + actions.Progress.UpdateActionProgress(index, 100); + + // see if the job succeeded. + if (this.job.LastRunOutcome == CompletionResult.Failed) + { + actions.Progress.UpdateActionStatus(index, ProgressStatus.Error); + actions.Progress.AddActionException(index, + new Exception("AgentActionSR.JobFailed(job.Name)")); + status = ProgressStatus.Error; + } + else + { + actions.Progress.UpdateActionDescription(index, "AgentActionSR.ExecuteJob(job.Name)"); + actions.Progress.UpdateActionStatus(index, ProgressStatus.Success); + status = ProgressStatus.Success; + } + + jobFinished = true; + + break; + + case JobExecutionStatus.Suspended: + + actions.Progress.UpdateActionProgress(index, "AgentActionSR.Suspended"); + break; + + case JobExecutionStatus.BetweenRetries: + + actions.Progress.UpdateActionProgress(index, "AgentActionSR.BetweenRetries"); + break; + + case JobExecutionStatus.Executing: + + actions.Progress.UpdateActionProgress(index, "AgentActionSR.Executing"); + break; + + case JobExecutionStatus.PerformingCompletionAction: + + actions.Progress.UpdateActionProgress(index, "AgentActionSR.PerformingCompletionAction"); + break; + + case JobExecutionStatus.WaitingForStepToFinish: + + actions.Progress.UpdateActionProgress(index, "AgentActionSR.WaitingForStepToFinish"); + break; + + case JobExecutionStatus.WaitingForWorkerThread: + + actions.Progress.UpdateActionProgress(index, "AgentActionSR.WaitingForWorkerThread"); + break; + + default: + // unknown JobExecutionStatus, keep waiting. + System.Diagnostics.Debug.Assert(false, + "Unknown JobExecutionStatus found while waiting for job execution to finish"); + break; + } + } + + if (jobFinished) + { + break; + } + + actions.Progress.UpdateActionStatus(index, ProgressStatus.InProgress); + } + + return status; + } + } + + /// + /// starts a job + /// + internal class StartJobAction : IProgressItem + { + private Job job; + //Represent selected job step if any + private string currentJobStep; + + public StartJobAction(Job job, string jobStep) + { + // need a job. The delegate can be null + if (job == null) + { + throw new ArgumentNullException("job"); + } + + this.job = job; + this.currentJobStep = jobStep; + } + + /// + /// generates a friendly description of this step. Used by the progress dialog + /// + /// + public override string ToString() + { + if (this.job == null) + { + return base.ToString(); + } + else + { + return "AgentActionSR.StartJobDescription(this.job.Name)"; + } + } + + + /// + /// Start the Job + /// + /// Actions collection + /// this actions index into the actions collection + /// + public ProgressStatus DoAction(ProgressItemCollection actions, int index) + { + ProgressStatus status = ProgressStatus.Success; + //parameter check + if (actions == null) + { + throw new ArgumentNullException("actions"); + } + + // in progress + actions.Progress.UpdateActionStatus(index, ProgressStatus.InProgress); + + // perform an enumerator query to get the steps. We could use the + // SMO step object but this is too inefficient as it generates a batch + // per step. + Request request = new Request(); + + request.Fields = new string[] {"Name", "ID", "SubSystem"}; + request.Urn = this.job.Urn + "/Step"; + request.OrderByList = new OrderBy[] {new OrderBy("ID", OrderBy.Direction.Asc)}; + + if (this.currentJobStep != null) + { + actions.Progress.AddActionInfoString(index, + "AgentActionSR.StartJobWithStep(this.job.Name, this.currentJobStep)"); + this.job.Start(this.currentJobStep); + } + else + { + actions.Progress.AddActionInfoString(index, "AgentActionSR.StartingJob(this.job.Name)"); + this.job.Start(); + } + + // done + actions.Progress.UpdateActionStatus(index, status); + return status; + } + } + } + + #endregion + + #region Stop Job + + /// + /// stop a job + /// + internal class StopAgentJobs : JobAction + { + public StopAgentJobs(XmlDocument document, IServiceProvider source) + : base(document, source) + { + } + + protected override void GenerateActions() + { + if (this.smoServer != null) + { + InitializeContext(); + + for (int i = 0; i < this.urnParameters.Length; i++) + { + Job job = this.smoServer.GetSmoObject(urnParameters[i]) as Job; + + // check that the urn really points to an job + this.actions.AddAction(new StopJobAction(job)); + } + } + } + + /// + /// class that actually stops a running job + /// + internal class StopJobAction : IProgressItem + { + private Job job; + + public StopJobAction(Job job) + { + if (job == null) + { + throw new ArgumentNullException("job"); + } + + this.job = job; + } + + /// + /// Generate a user friendly description of this task. Used in the description + /// of the progress dialog. + /// + /// Description of the action + public override string ToString() + { + if (this.job == null) + { + return base.ToString(); + } + else + { + return "AgentActionSR.StopJobDescription(this.job.Name)"; + } + } + + /// + /// Stop the Job + /// + /// Actions collection + /// this actions index into the actions collection + /// + public ProgressStatus DoAction(ProgressItemCollection actions, int index) + { + //parameter check + if (actions == null) + { + throw new ArgumentNullException("actions"); + } + + // in progress + actions.Progress.UpdateActionStatus(index, ProgressStatus.InProgress); + actions.Progress.AddActionInfoString(index, "AgentActionSR.StoppingJob(this.job.Name)"); + job.Stop(); + + // done + actions.Progress.AddActionInfoString(index, "AgentActionSR.StoppedJob(this.job.Name)"); + actions.Progress.UpdateActionStatus(index, ProgressStatus.Success); + return ProgressStatus.Success; + } + } + } + + #endregion +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/AgentInterfaces.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/AgentInterfaces.cs similarity index 100% rename from src/Microsoft.SqlTools.ServiceLayer/Agent/AgentInterfaces.cs rename to src/Microsoft.SqlTools.ServiceLayer/Agent/Common/AgentInterfaces.cs diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/ExecutionMode.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/ExecutionMode.cs new file mode 100644 index 00000000..110f334c --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/ExecutionMode.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; + +namespace Microsoft.SqlTools.ServiceLayer.Agent +{ + /// + /// Execution mode enumeration Success if execution succeeded of Failure otherwise for now. + /// This enumeration might be refined more as there are needs for it + /// + public enum ExecutionMode + { + /// + /// indicates that the operation failed + /// + Failure = 0, + + /// + /// indicates that the operation succeded + /// + Success, + + /// + /// indicates that the operation was canceled + /// + Cancel + }; + +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/IExecutionAwareSqlControlCollection.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/IExecutionAwareSqlControlCollection.cs new file mode 100644 index 00000000..a1a33add --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/IExecutionAwareSqlControlCollection.cs @@ -0,0 +1,103 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; + +namespace Microsoft.SqlTools.ServiceLayer.Agent +{ + public class PreProcessExecutionInfo + { + private RunType runType; + private string script; + private PreProcessExecutionInfo() {} + + internal PreProcessExecutionInfo(RunType runType) + { + this.runType = runType; + } + + public RunType RunType + { + get + { + return this.runType; + } + } + + public string Script + { + get + { + return this.script; + } + + set + { + this.script = value; + } + } + } + + + /// + /// IExecutionAwareSqlControlCollection allows control's container to do pre and post + /// processing of the execution commands + /// + public interface IExecutionAwareSqlControlCollection : ISqlControlCollection + { + /// + /// called before dialog's host executes actions on all panels in the dialog one by one. + /// If something fails inside this function and the execution should be aborted, + /// it can either raise an exception [in which case the framework will show message box with exception text] + /// or set executionResult out parameter to be ExecutionMode.Failure + /// NOTE: it might be called from worker thread + /// + /// information about execution action + /// result of the execution + /// + /// true if regular execution should take place, false if everything + /// has been done by this function + /// NOTE: in case of returning false during scripting operation + /// PreProcessExecutionInfo.Script property of executionInfo parameter + /// MUST be set by this function [if execution result is success] + /// + bool PreProcessExecution(PreProcessExecutionInfo executionInfo, out ExecutionMode executionResult); + + /// + /// called when the host received Cancel request. NOTE: this method can return while + /// operation is still being canceled + /// + /// + /// true if the host should do standard cancel for the currently running view or + /// false if the Cancel operation was done entirely inside this method and there is nothing + /// extra that should be done + /// + bool Cancel(); + + /// + /// called after dialog's host executes actions on all panels in the dialog one by one + /// NOTE: it might be called from worker thread + /// + /// result of the execution + /// type of execution + void PostProcessExecution(RunType runType, ExecutionMode executionMode); + + /// + /// called before dialog's host executes OnReset method on all panels in the dialog one by one + /// NOTE: it might be called from worker thread + /// + /// + /// true if regular execution should take place, false if everything + /// has been done by this function + /// + bool PreProcessReset(); + + /// + /// called after dialog's host executes OnReset method on all panels in the dialog one by one + /// NOTE: it might be called from worker thread + /// + void PostProcessReset(); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/IProgressItem.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/IProgressItem.cs new file mode 100644 index 00000000..68ae98f3 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/IProgressItem.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; + +namespace Microsoft.SqlTools.ServiceLayer.Agent +{ +#region interfaces + /// + /// Interface that supports the delegation of individual actions in the progress dialog + /// to individual classes. + /// + public interface IProgressItem + { + /// + /// Perform the action for this class + /// + /// Actions collection + /// array index of this particular action + /// + ProgressStatus DoAction(ProgressItemCollection actions, int index); + } +#endregion +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/ISqlControlCollection.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/ISqlControlCollection.cs new file mode 100644 index 00000000..6b2ff9a1 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/ISqlControlCollection.cs @@ -0,0 +1,24 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; + +namespace Microsoft.SqlTools.ServiceLayer.Agent +{ + /// + /// defines notion of sitable object + /// + public interface IObjectWithSite + { + void SetSite(System.IServiceProvider sp); + } + + /// + /// ISqlControlCollection allows access to a collection of dialog views + /// + public interface ISqlControlCollection : IObjectWithSite + { + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/JobActivityFilter.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/JobActivityFilter.cs similarity index 100% rename from src/Microsoft.SqlTools.ServiceLayer/Agent/JobActivityFilter.cs rename to src/Microsoft.SqlTools.ServiceLayer/Agent/Common/JobActivityFilter.cs diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/LogAggregator.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/LogAggregator.cs similarity index 100% rename from src/Microsoft.SqlTools.ServiceLayer/Agent/LogAggregator.cs rename to src/Microsoft.SqlTools.ServiceLayer/Agent/Common/LogAggregator.cs diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/LogInterfaces.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/LogInterfaces.cs similarity index 100% rename from src/Microsoft.SqlTools.ServiceLayer/Agent/LogInterfaces.cs rename to src/Microsoft.SqlTools.ServiceLayer/Agent/Common/LogInterfaces.cs diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/ManagementActionBase.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/ManagementActionBase.cs new file mode 100644 index 00000000..06ee7469 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/ManagementActionBase.cs @@ -0,0 +1,644 @@ +// +// 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.Drawing; +using System.Collections; +using System.Text; +using System.ComponentModel; +using System.Data; +using System.Xml; +using System.IO; +using System.Threading; +using System.Diagnostics; +using System.Collections.Specialized; +using Microsoft.SqlServer.Management.Smo; +using Microsoft.SqlServer.Management.Common; +using Microsoft.SqlServer.Management.Diagnostics; +using Microsoft.SqlTools.ServiceLayer.Admin; +using Microsoft.SqlTools.ServiceLayer.Agent; + +namespace Microsoft.SqlTools.ServiceLayer.Management +{ + /// + /// base class that can be used to derived from for the main classes [containers] of the dialogs + /// + public class ManagementActionBase : IDisposable + { +#region Members + + /// + /// selected node as specified to SelectNode method + /// + //private TreeNode selectedNode; + + /// + /// service provider of our host. We should direct all host-specific requests to the services + /// implemented by this provider + /// + private IServiceProvider serviceProvider; + + /// + /// data container with initialization-related information + /// + private CDataContainer dataContainer; + //whether we assume complete ownership over it. + //We set this member once the dataContainer is set to be non-null + private bool ownDataContainer = true; + + //if derived class tries to call a protected method that relies on service provider, + //and the service provider hasn't been set yet, we will cache the values and will + //propagate them when we get the provider set + //private System.Drawing.Icon cachedIcon = null; + private string cachedCaption = null; + + //SMO Server connection that MUST be used for all enumerator calls + //We'll get this object out of CDataContainer, that must be initialized + //property by the initialization code + private ServerConnection serverConnection; + +#endregion + +#region Constructors + + /// + /// Constructor + /// + public ManagementActionBase() + { + } + +#endregion + +#region IDisposable implementation + + void IDisposable.Dispose() + { + //BUGBUG - do we need finalizer + Dispose(true);//call protected virtual method + } + + /// + /// do the deterministic cleanup + /// + /// + protected virtual void Dispose(bool disposing) + { + //dispose CDataContainer if needed + if (this.dataContainer != null) + { + if (this.ownDataContainer) + { + this.dataContainer.Dispose(); + } + this.dataContainer = null; + } + } + +#endregion + +#region IObjectWithSite implementation + + public virtual void SetSite(IServiceProvider sp) + { + if (sp == null) + { + throw new ArgumentNullException("sp"); + } + + //allow to be sited only once + if (this.serviceProvider == null) + { + //cache the service provider + this.serviceProvider = sp; + + //call protected virtual method to enable derived classes to do initialization + //OnHosted(); + } + } + +#endregion + +#region IExecutionAwareSqlControlCollection implementation + + /// + /// called before dialog's host executes actions on all panels in the dialog one by one. + /// If something fails inside this function and the execution should be aborted, + /// it can either raise an exception [in which case the framework will show message box with exception text] + /// or set executionResult out parameter to be ExecutionMode.Failure + /// NOTE: it might be called from worker thread + /// + /// information about execution action + /// result of the execution + /// + /// true if regular execution should take place, false if everything + /// has been done by this function + /// NOTE: in case of returning false during scripting operation + /// PreProcessExecutionInfo.Script property of executionInfo parameter + /// MUST be set by this function [if execution result is success] + /// + public bool PreProcessExecution(PreProcessExecutionInfo executionInfo, out ExecutionMode executionResult) + { + //we start from failure + executionResult = ExecutionMode.Failure; + + //OK, we do server switching for scripting for SQL/Analysis Server execution here + RunType runType = executionInfo.RunType; + if (IsScripting(runType)) + { + if (!PreProcessScripting(executionInfo, out executionResult)) + { + return false; + } + } + + if (DataContainer != null) + { + //we take over execution here. We substitute the server here for AMO and SQL + //dialogs + if (DataContainer.ContainerServerType == CDataContainer.ServerType.SQL) + { + ExecuteForSql(executionInfo, out executionResult); + return false;//execution of the entire control was done here + } + } + + + // call virtual function to do regular execution + return DoPreProcessExecution(executionInfo.RunType, out executionResult); + } +#endregion + + + /// + /// whether we own our DataContainer or not. Depending on this value it will or won't be + /// disposed in our Dispose method + /// + protected virtual bool OwnDataContainer + { + get + { + //by default we own it + return true; + } + } + + + /// + /// called by IExecutionAwareSqlControlCollection.PreProcessExecution to enable derived + /// classes to take over execution of the dialog and do entire execution in this method + /// rather than having the framework to execute dialog views one by one. + /// + /// NOTE: it might be called from non-UI thread + /// + /// + /// + /// + /// true if regular execution should take place, false if everything, + /// has been done by this function + /// + protected virtual bool DoPreProcessExecution(RunType runType, out ExecutionMode executionResult) + { + //ask the framework to do normal execution by calling OnRunNOw methods + //of the views one by one + executionResult = ExecutionMode.Success; + return true; + } + + /// + /// called before dialog's host executes OnReset method on all panels in the dialog one by one + /// NOTE: it might be called from worker thread + /// + /// + /// true if regular execution should take place, false if everything + /// has been done by this function + /// + /// + protected virtual bool DoPreProcessReset() + { + if ((this.dataContainer != null) && this.dataContainer.IsNewObject) + { + this.dataContainer.Reset(); + } + + return true; + } + + /// + /// called after dialog's host executes OnReset method on all panels in the dialog one by one + /// NOTE: it might be called from worker thread + /// + protected virtual void DoPostProcessReset() + { + //nothing in the base class + } + + /// + /// Called to intercept scripting operation + /// + /// + /// + /// + /// true if regular execution should take place, false the script + /// has been created by this function + /// + protected virtual bool PreProcessScripting(PreProcessExecutionInfo executionInfo, out ExecutionMode executionResult) + { + //we don't do anything here, but we enable derived classes to do something... + executionResult = ExecutionMode.Success; + return true; + } + + /// + /// CDataContainer accessor + /// + protected CDataContainer DataContainer + { + get + { + return this.dataContainer; + } + set + { + this.dataContainer = value; + this.ownDataContainer = OwnDataContainer; //cache the value + } + } + + +// /// +// /// SMO Server connection that MUST be used for all enumerator calls +// /// We'll get this object out of CDataContainer, that must be initialized +// /// property by the initialization code +// /// + protected ServerConnection ServerConnection + { + get + { + if (this.serverConnection == null && this.dataContainer != null && + this.dataContainer.ContainerServerType == CDataContainer.ServerType.SQL) + { + this.serverConnection = this.dataContainer.ServerConnection; + } + + //it CAN be null here if dataContainer hasn't been set or the container type is non SQL + return this.serverConnection; + } + } + + /// + /// checks whether given run time represents one of scripting options + /// + /// + /// + protected static bool IsScripting(RunType runType) + { + return(runType != RunType.RunNow && runType != RunType.RunNowAndExit); + } + + /// + /// calls DoPreProcessExecution and if it returns false, then it will execute all initialized views + /// one by one. + /// This method allows derived classes to do both preprocessing and normal execution in a way + /// that the framework would normally do. Use this method for special cases when derived class + /// should really handle entire execution while doing exactly the same actions as the framework + /// would do + /// + /// + /// + /// result of the execution. It will let exception fly out if it was raised during execution + /// + protected ExecutionMode DoPreProcessExecutionAndRunViews(RunType runType) + { + ExecutionMode executionResult; + if (DoPreProcessExecution(runType, out executionResult)) + { + //true return value means that we need to do execution ourselves + //executionResult = PanelExecutionHandler.Run(runType, this); + } + + return executionResult; + } + + /// + /// determines whether we need to substitute SMO/AMO server objects with the + /// temporary ones while doing scripting + /// + /// + private bool NeedToSwitchServer + { + get + { + ServerSwitchingAttribute switchingAttrib = + Utils.GetCustomAttribute(this, typeof(ServerSwitchingAttribute)) as ServerSwitchingAttribute; + + if (DataContainer == null) + { + if (switchingAttrib != null) + { + return switchingAttrib.NeedToSwtichServerObject; + } + else + { + throw new InvalidOperationException(); + } + } + + if (DataContainer.ContainerServerType == CDataContainer.ServerType.SQL) + { + if (switchingAttrib != null) + { + return switchingAttrib.NeedToSwtichServerObject; + } + else + { + return true;//switch by default in SQL case + } + } + else + { + throw new InvalidOperationException(); + } + } + } + + protected virtual ServerConnection GetServerConnectionForScript() + { + return this.dataContainer.Server.ConnectionContext; + } + + /// + /// builds a script string from SMO string collections. This function should + /// probably be moved to SMO. Also we need a way to specify the tsql batch + /// separator which for now is GO + /// + /// + private string BuildSqlScript() + { + SqlSmoObject sqlDialogSubject = null; + try + { + sqlDialogSubject = this.DataContainer.SqlDialogSubject; + } + catch (System.Exception) + { + //We may not have a valid dialog subject here (such as if the object hasn't been created yet) + //so in that case we'll just ignore it as that's a normal scenario. + } + + StringCollection sc = GetServerConnectionForScript().CapturedSql.Text; + //Scripting may happen on either the server ExecutionManager or the + //ExecutionManager of the object itself. So we make sure to check + //the subject text if the server ExecutionManager didn't have any + //scripts after doing the scripting + if (sc.Count == 0 && sqlDialogSubject != null) + { + sc = sqlDialogSubject.ExecutionManager.ConnectionContext.CapturedSql.Text; + } + int i; + StringBuilder script = new StringBuilder(4096); + + if (sc != null) + { + for (i = 0; i < sc.Count; i ++) + { + script.AppendFormat("{0}\r\nGO\r\n", sc[i].ToString()); + } + } + + return script.ToString(); + } + + + /// + /// called when we need to script a Sql server dlg. + /// + /// + /// + private void ExecuteForSql(PreProcessExecutionInfo executionInfo, out ExecutionMode executionResult) + { + Microsoft.SqlServer.Management.Smo.Server oldServer = null; + if (NeedToSwitchServer) + { + // We use a new instance of the SMO Server object every time we script + // so that any changes that are made to the SMO server while scripting are + // not kept when the script operation is completed. + oldServer = DataContainer.Server; + + //BUGBUG - see if we can use copy ctor instead + DataContainer.Server = new Microsoft.SqlServer.Management.Smo.Server(DataContainer.ServerConnection); + } + + String szScript = null; + bool isScripting = IsScripting(executionInfo.RunType); + var executionModeOriginal = GetServerConnectionForScript().SqlExecutionModes; + //For Azure the ExecutionManager is different depending on which ExecutionManager + //used - one at the Server level and one at the Database level. So to ensure we + //don't use the wrong execution mode we need to set the mode for both (for on-prem + //this will essentially be a no-op) + SqlExecutionModes subjectExecutionModeOriginal = executionModeOriginal; + SqlSmoObject sqlDialogSubject = null; + try + { + sqlDialogSubject = this.DataContainer.SqlDialogSubject; + } + catch (System.Exception) + { + //We may not have a valid dialog subject here (such as if the object hasn't been created yet) + //so in that case we'll just ignore it as that's a normal scenario. + } + + if (sqlDialogSubject != null) + { + subjectExecutionModeOriginal = sqlDialogSubject.ExecutionManager.ConnectionContext.SqlExecutionModes; + } + try + { + SqlExecutionModes newMode = isScripting + ? SqlExecutionModes.CaptureSql + : SqlExecutionModes.ExecuteSql; + //now, do the execution itself + GetServerConnectionForScript().SqlExecutionModes = newMode; + if (sqlDialogSubject != null) + { + sqlDialogSubject.ExecutionManager.ConnectionContext.SqlExecutionModes = newMode; + } + + executionResult = DoPreProcessExecutionAndRunViews(executionInfo.RunType); + + if (isScripting) + { + if (executionResult == ExecutionMode.Success) + { + szScript = BuildSqlScript(); + } + } + + } + finally + { + GetServerConnectionForScript().SqlExecutionModes = executionModeOriginal; + + if (isScripting) + { + GetServerConnectionForScript().CapturedSql.Clear(); + } + + if (sqlDialogSubject != null) + { + sqlDialogSubject.ExecutionManager.ConnectionContext.SqlExecutionModes = subjectExecutionModeOriginal; + if (isScripting) + { + sqlDialogSubject.ExecutionManager.ConnectionContext.CapturedSql.Clear(); + } + } + + //see if we need to restore the server + if (oldServer != null) + { + DataContainer.Server = oldServer; + } + } + + if (isScripting) + { + executionInfo.Script = szScript; + } + } + +// #region ICustomAttributeProvider +// object[] System.Reflection.ICustomAttributeProvider.GetCustomAttributes(bool inherit) +// { +// //we merge attributes from 2 sources: type attributes and the derived classes, giving preference +// //to the derived classes +// return GetMergedArray(DoGetCustomAttributes(inherit), GetType().GetCustomAttributes(inherit)); +// } +// object[] System.Reflection.ICustomAttributeProvider.GetCustomAttributes(Type attributeType, bool inherit) +// { +// //we merge attributes from 2 sources: type attributes and the derived classes, giving preference +// //to the derived classes +// return GetMergedArray(DoGetCustomAttributes(attributeType, inherit), +// GetType().GetCustomAttributes(attributeType, inherit)); +// } +// bool System.Reflection.ICustomAttributeProvider.IsDefined(Type attributeType, bool inherit) +// { +// //we merge attributes from 2 sources: type attributes and the derived classes, giving preference +// //to the derived classes +// if (!DoIsDefined(attributeType, inherit)) +// { +// return GetType().IsDefined(attributeType, inherit); +// } +// else +// { +// return true; +// } +// } +// #endregion +// #region ICustomAttributeProvider helpers +// protected virtual object[] DoGetCustomAttributes(bool inherit) +// { +// return GetMergedArray(DoGetCustomAttributes(typeof(ScriptTypeAttribute), inherit), +// DoGetCustomAttributes(typeof(DialogScriptableAttribute), inherit)); +// } +// protected virtual object[] DoGetCustomAttributes(Type attributeType, bool inherit) +// { +// //if the type specifies this attribute, we don't bother - it overrides +// //our behavior +// object[] typeAttribs = GetType().GetCustomAttributes(attributeType, inherit); +// if (typeAttribs != null && typeAttribs.Length > 0) +// { +// return null; +// } +// //we expose default custom attribute for script type +// if (attributeType.Equals(typeof(ScriptTypeAttribute))) +// { +// string scriptType = ScriptType; +// if (scriptType != null) +// { +// return new object[] {new ScriptTypeAttribute(scriptType)}; +// } +// else +// { +// return null; +// } +// } +// else if (attributeType.Equals(typeof(DialogScriptableAttribute))) +// { +// bool canScriptToWindow = true; +// bool canScriptToFile = true; +// bool canScriptToClipboard = true; +// bool canScriptToJob = true; +// GetScriptableOptions(out canScriptToWindow, +// out canScriptToFile, +// out canScriptToClipboard, +// out canScriptToJob); +// return new object[] {new DialogScriptableAttribute(canScriptToWindow, +// canScriptToFile, +// canScriptToClipboard, +// canScriptToJob)}; +// } +// return null; +// } +// protected virtual bool DoIsDefined(Type attributeType, bool inherit) +// { +// return false; +// } +// /// +// /// detects whether script types are applicable for this dlg or not. By default +// /// the framework relies on DialogScriptableAttribute set on the dlg class and won't +// /// call this method if the attribute is specified +// /// By default we assume that all script types are enabled +// /// +// /// +// /// +// /// +// protected virtual void GetScriptableOptions(out bool canScriptToWindow, +// out bool canScriptToFile, +// out bool canScriptToClipboard, +// out bool canScriptToJob) +// { +// canScriptToWindow = canScriptToFile = canScriptToClipboard = canScriptToJob = true; +// } +// #endregion +// protected IServiceProvider ServiceProvider +// { +// get +// { +// if (this.serviceProvider == null) +// { +// STrace.Assert(false, "Cannot work without service provider!"); +// STrace.LogExThrow(); +// //BUGBUG - should we have our own exception here? +// throw new InvalidOperationException(); +// } +// return this.serviceProvider; +// } +// } +// /// +// /// returns combination of the given 2 arrays +// /// +// /// +// /// +// /// +// protected object[] GetMergedArray(object[] array1, object[] array2) +// { +// if (array1 == null) +// { +// return array2; +// } +// else if (array2 == null) +// { +// return array1; +// } +// else +// { +// object[] finalReturnValue = new object[array1.Length + array2.Length]; +// array1.CopyTo(finalReturnValue, 0); +// array2.CopyTo(finalReturnValue, array1.Length); +// return finalReturnValue; +// } +// } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/ProgressItemCollection.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/ProgressItemCollection.cs new file mode 100644 index 00000000..84073f9f --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/ProgressItemCollection.cs @@ -0,0 +1,331 @@ +// +// 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.Globalization; +using System.Collections; +using System.Threading; +using System.Text; + +namespace Microsoft.SqlTools.ServiceLayer.Agent +{ + /// + /// Allows for the mapping of objects that implement IProgressItem to individual items in the + /// progress dialog. + /// + public class ProgressItemCollection : ICollection + { + #region internal helper classes + /// + /// Allows us to map an action to its index in the progress dialog. + /// + public class ActionIndexMap + { + /// + /// action + /// + public IProgressItem Action; + /// + /// index + /// + public int Index; + + public ActionIndexMap(IProgressItem action) + { + this.Action = action; + // index isn't known yet + this.Index = -1; + } + } + #endregion + + #region private data members + /// + /// list of actions we will perform. + /// + private ArrayList actions = new ArrayList(); + #endregion + + #region construction + public ProgressItemCollection() + { + } + #endregion + + #region properties + + private bool closeOnUserCancel = false; + /// + /// Indicates whether to close the dialog immediately if the user cancels an operation + /// + public bool CloseOnUserCancel + { + get + { + return closeOnUserCancel; + } + set + { + closeOnUserCancel = value; + } + } + + private bool automaticClose = false; + /// + /// Indicates whether to automatically close the dialog when all actions are complete + /// successfully. + /// + public bool CloseOnSuccessfulCompletion + { + get + { + return automaticClose; + } + set + { + automaticClose = value; + } + } + + private bool quitOnError = false; + /// + /// Indicates whether the operation should be terminated if any individual step fails. + /// + public bool QuitOnError + { + get + { + return this.quitOnError; + } + set + { + this.quitOnError = value; + } + } + private OperationStatus operationStatus = OperationStatus.Invalid; + /// + /// Indicates the status of the operation. + /// + public OperationStatus OperationStatus + { + get + { + return this.operationStatus; + } + } + /// + /// Progress object this action collection will work with + /// + private IProgress progress = null; + public IProgress Progress + { + get + { + return this.progress; + } + set + { + if (this.progress != value) + { + this.progress = value; + if (this.progress != null) + { + // add the actions to the progress dialog, and + // fixup our event handler + FixUpActionsToProgress(); + } + } + } + } + #endregion + + #region public overrides + /// + /// Generate a string representaion of this object. It will convert all of it's IProgressItem members + /// to strings in a new line. + /// + /// string description of the actions this object contains + public override string ToString() + { + // if there are no actions then just return the default ToString + if (this.actions == null || this.actions.Count == 0) + { + return base.ToString(); + } + else + { + // convert all of the actions to strings on their own line + StringBuilder sb = new StringBuilder(((ActionIndexMap)actions[0]).Action.ToString()); + for (int i = 1; i < this.actions.Count; i++) + { + sb.AppendFormat(CultureInfo.InvariantCulture, "\r\n{0}", ((ActionIndexMap)actions[i]).Action.ToString()); + } + return sb.ToString(); + } + } + #endregion + + #region ICollection implementation + /// + /// Gets the number of actions in this collection + /// + public int Count + { + get + { + return this.actions.Count; + } + } + /// + /// not supported + /// + public bool IsSynchronized + { + get + { + throw new NotSupportedException(); + } + } + /// + /// not supported + /// + public object SyncRoot + { + get + { + throw new NotSupportedException(); + } + } + public void CopyTo(IProgressItem[] array, int start) + { + this.actions.CopyTo(array, start); + } + public void CopyTo(Array array, int start) + { + this.actions.CopyTo(array, start); + } + public IEnumerator GetEnumerator() + { + return this.actions.GetEnumerator(); + } + #endregion + + #region public methods + /// + /// Add an action to the collection + /// + /// action to be added + public void AddAction(IProgressItem action) + { + ActionIndexMap map = new ActionIndexMap(action); + this.actions.Add(map); + } + + #endregion + + #region internal implementation + /// + /// delegate called when the progress dialog wants us to perform work on a new thread. + /// + private void DoWorkOnThread() + { + if (this.Progress == null) + { + return; + } + + try + { + System.Threading.Thread.CurrentThread.Name = "Worker thread for " + progress.GetType(); + } + catch (InvalidOperationException) + { } + + // default to succeeded. + operationStatus = OperationStatus.Success; + + // carry out each action. + foreach (ActionIndexMap map in this.actions) + { + // abort if the user has decided to cancel. + if (this.Progress.IsAborted) + { + this.Progress.UpdateActionStatus(map.Index, ProgressStatus.Aborted); + operationStatus = OperationStatus.Aborted; + break; + } + ProgressStatus stepStatus = ProgressStatus.Invalid; + try + { + // perform the action. + stepStatus = map.Action.DoAction(this, map.Index); + this.Progress.UpdateActionStatus(map.Index, stepStatus); + } + catch (Exception e) + { + // fail the step with errors, add the error messages to the control. + this.Progress.AddActionException(map.Index, e); + this.Progress.UpdateActionStatus(map.Index, ProgressStatus.Error); + stepStatus = ProgressStatus.Error; + } + if (stepStatus == ProgressStatus.Error) + { + // see if we're supposed to fail if any step fails + if (this.QuitOnError == true) + { + // fail and quit + this.operationStatus = OperationStatus.Error; + break; + } + else + { + this.operationStatus = OperationStatus.CompletedWithErrors; + } + } + else if (stepStatus != ProgressStatus.Success) + { + this.operationStatus = OperationStatus.CompletedWithErrors; + } + } + + // tell the dialog we're finishing. + this.Progress.WorkerThreadExiting(operationStatus); + + // close the dialog if asked to. We have to put this after + // the WorkerThreadExiting call because the progress dialog + // won't allow itself to be closed until worker thread says + // it's finished. + if ((this.CloseOnSuccessfulCompletion && (this.operationStatus == OperationStatus.Success)) || + (this.CloseOnUserCancel && this.Progress.IsAborted)) + { + //((Form)this.Progress).BeginInvoke(new CloseProgressWindowCallback(CloseProgressWindowHandler), new object[] { this.Progress }); + } + } + + private delegate void CloseProgressWindowCallback(IProgress progress); + private void CloseProgressWindowHandler(IProgress progress) + { + } + + /// + /// Adds the actions to an IProgress interface. + /// + private void FixUpActionsToProgress() + { + if (this.Progress == null) + { + return; + } + // add actions + foreach (ActionIndexMap map in this.actions) + { + map.Index = this.Progress.AddAction(map.Action.ToString()); + } + // add our delegate + this.Progress.WorkerThreadStart = new ThreadStart(this.DoWorkOnThread); + } + #endregion + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/ProgressReportCommon.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/ProgressReportCommon.cs new file mode 100644 index 00000000..4c66c7e9 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/ProgressReportCommon.cs @@ -0,0 +1,301 @@ +// +// 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.Drawing; +using System.Threading; +using System.Runtime.InteropServices; + +namespace Microsoft.SqlTools.ServiceLayer.Agent +{ + /// + /// Enumeration for status of individual actions + /// + public enum ProgressStatus + { + Invalid = -1, + NotStarted, // Not started + InProgress, // In progress + Success, // Completed + SuccessWithInfo, // Completed, display additional info + Warning, // Completed with warning, display exceptions + Error, // Not completed because of error, display exceptions + Aborted, // Aborted + RolledBack, // Rolled back because of a subsequent error + StatusCount // = Number of status values - For validation only + } + + /// + /// Enumeration for status of the overall operation + /// + public enum OperationStatus + { + Invalid = -1, + InProgress, // In progress + Success, // Completed successfully + CompletedWithErrors, // Completed with non-fatal errors + Error, // Not completed because of error + Aborted, // Abort complete + StatusCount // = Number of status values - For validation only + } + + /// + /// Interface defining core functionality of a progress control container + /// + /// NOTE: Refer to the comments for each method/property individually to determine + /// whether it is thread-safe and may be used from the worker thread. Also note + /// that some members are asynchronous. + /// + public interface IProgress + { + //----------- + // Properties + //----------- + + /// + /// The property that determines if the user should be allowed to abort the + /// operation. The default value is true. + /// + /// NOTE: This property is not thread safe. Set from UI thread. + /// + bool AllowAbort + { + get; + set; + } + + /// + /// The confirmation prompt to display if the user hits the Abort button. + /// The abort prompt should be worded such that the abort is confirmed + /// if the user hits "Yes". + /// + /// NOTE: This property is not thread safe. Set from UI thread. + /// + string AbortPrompt + { + get; + set; + } + + /// + /// The ThreadStart delegate. The method referenced by this delegate is run by + /// the worker thread to perform the operation. + /// + /// NOTE: This property is not thread safe. Set from UI thread. + /// + ThreadStart WorkerThreadStart + { + get; + set; + } + + /// + /// Aborted status of the operation. + /// + /// NOTE: This property is thread safe and may be called from the worker + /// thread. Accessing this property may cause caller to block if UI + /// is waiting for user confirmation of abort. + /// + bool IsAborted + { + get; + } + + /// + /// This property determines whether updates should be allowed for + /// actions. If this is set to false, any calls that are made + /// to add or update an action are ignored. The default value is true. + /// + /// NOTE: This property is thread safe and may be called from the worker + /// thread. + /// + bool ActionUpdateEnabled + { + get; + set; + } + + //-------- + // Methods + //-------- + + /// + /// Add an action to the displayed list of actions. Actions are + /// displayed in the order they are added. An action can be referenced + /// in future calls using the zero-based index that is returned. + /// + /// The description must be a non-empty string. + /// + /// NOTE: This method is not thread safe. Call from the UI thread. + /// + /// Description of the action + /// The index of the newly added action. + int AddAction(string description); + + /// + /// Add an action to the displayed list of actions. This is meant + /// to be called from the worker thread to add an action after + /// the operation is already in progress. + /// + /// Actions are displayed in the order they are added. Use the + /// zero-based index of the action based on past actions added + /// to reference it in future calls. The description must be a + /// non-empty string. + /// + /// NOTE: This method is thread safe and asynchronous. It may be + /// called from the worker thread. + /// + /// Description of the action + void AddActionDynamic(string description); + + /// + /// Update the description of an action + /// + /// The description must be a non-empty string. + /// + /// NOTE: This method is thread safe and asynchronous. It may be + /// called from the worker thread. + /// + /// Index of the action + /// New description of the action + void UpdateActionDescription(int actionIndex, string description); + + /// + /// Update the status of an action + /// + /// NOTE: This method is thread safe and asynchronous. It may be + /// called from the worker thread. + /// + /// Index of the action + /// New status of the action + void UpdateActionStatus(int actionIndex, ProgressStatus status); + + /// + /// Update the progress of an action in terms of percentage complete + /// + /// NOTE: This method is thread safe and asynchronous. It may be + /// called from the worker thread. + /// + /// Index of the action + /// Percentage of the action that is complete (0-100) + void UpdateActionProgress(int actionIndex, int percentComplete); + + /// + /// Update the progress of an action with a text description of + /// the progress + /// + /// The description must be a non-empty string. + /// + /// NOTE: This method is thread safe and asynchronous. It may be + /// called from the worker thread. + /// + /// Index of the action + /// Description of progress + void UpdateActionProgress(int actionIndex, string description); + + /// + /// Add an exception to an action + /// + /// Exceptions are displayed in the action grid only for actions + /// with "Error" or "Warning" status. + /// + /// NOTE: This method is thread safe and asynchronous. It may be + /// called from the worker thread. + /// + /// Index of the action + /// Exception to be added + void AddActionException(int actionIndex, Exception e); + + /// + /// Add an info string to an action in the progress report control + /// + /// Information strings are displayed in the action grid only for + /// actions with "SuccessWithInfo" status. The info string must + /// be a non-empty string. It should not be formatted or contain + /// newline characters. + /// + /// NOTE: This method is thread safe and asynchronous. It may be + /// called from the worker thread. + /// + /// Index of the action + /// Information string to be added + void AddActionInfoString(int actionIndex, string infoString); + + /// + /// Call this method when the worker thread performing the operation + /// is about to exit. The final result of the operation is supplied in + /// the form of a OperationStatus value. + /// + /// NOTE: This method is thread safe and asynchronous. It may be + /// called from the worker thread. + /// + /// Result of the operation + void WorkerThreadExiting(OperationStatus result); + } + + /// + /// Enumeration for status of the progress report control w.r.t the operation + /// + [System.Runtime.InteropServices.ComVisible(false)] + public enum ProgressCtrlStatus + { + Invalid = -1, + InProgress, // In progress + Success, // Completed successfully + CompletedWithErrors, // Completed with non-fatal errors + Error, // Not completed because of error + Aborting, // User clicked "Abort", aborting operation + Aborted, // Abort complete + Closed, // User clicked "Close" + StatusCount // = Number of status values - For validation only + } + + /// + /// Delegate used with ProgressCtrlStatusChanged event. + /// + public delegate void ProgressCtrlStatusChangedEventHandler(object source, ProgressCtrlStatusChangedEventArgs e); + + /// + /// EventArgs class for use with ProgressCtrlStatusChanged event + /// + sealed public class ProgressCtrlStatusChangedEventArgs : EventArgs + { + //------------------ + // Public Properties + //------------------ + public ProgressCtrlStatus Status + { + get { return m_status; } + set { m_status = value; } + } + + //------------- + // Private Data + //------------- + private ProgressCtrlStatus m_status = ProgressCtrlStatus.Invalid; + } + + // Enumeration for progress action grid columns + internal enum ProgressActionColumn + { + Invalid = -1, + ActionStatusBitmap, + ActionDescription, + ActionStatusText, + ActionMessage, + ActionColumnCount // = Number of columns - For validation only + } + + // Enumeration for progress action display filter + internal enum ProgressActionDisplayMode + { + Invalid = -1, + DisplayAllActions, + DisplayErrors, + DisplaySuccess, + DisplayWarnings, + ActionDisplayModeCount // = Number of display modes - For validation only + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/RunType.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/RunType.cs new file mode 100644 index 00000000..5117d2e2 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/RunType.cs @@ -0,0 +1,23 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; + +namespace Microsoft.SqlTools.ServiceLayer.Agent +{ + /// + /// what type of actions does the worker know to execute + /// + public enum RunType + { + RunNow = 0, + RunNowAndExit, + ScriptToFile, + ScriptToWindow, + ScriptToClipboard, + ScriptToJob + } + +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/ServerSwitchingAttribute.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/ServerSwitchingAttribute.cs new file mode 100644 index 00000000..44cc1941 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/Common/ServerSwitchingAttribute.cs @@ -0,0 +1,35 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; + +namespace Microsoft.SqlTools.ServiceLayer.Agent +{ + /// + /// Custom attribute that can be applied on particular DB commander to + /// indicate whether the base class should switch SMO servers before + /// execution or not. + /// + [AttributeUsage(AttributeTargets.Class)] + public sealed class ServerSwitchingAttribute : Attribute + { + private bool needToSwitch = false; + + private ServerSwitchingAttribute() {} + + public ServerSwitchingAttribute(bool needToSwitchServer) + { + this.needToSwitch = needToSwitchServer; + } + + public bool NeedToSwtichServerObject + { + get + { + return this.needToSwitch; + } + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentAlertInfo.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentAlertInfo.cs new file mode 100644 index 00000000..98982e2f --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentAlertInfo.cs @@ -0,0 +1,58 @@ +// +// 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 Microsoft.SqlTools.ServiceLayer.Agent; + +namespace Microsoft.SqlTools.ServiceLayer.Agent.Contracts +{ + [Flags] + public enum NotifyMethods + { + None = 0, + NotifyEmail = 1, + Pager = 2, + NetSend = 4, + NotifyAll = 7 + } + + public enum AlertType + { + SqlServerEvent = 1, + SqlServerPerformanceCondition = 2, + NonSqlServerEvent = 3, + WmiEvent = 4 + } + + /// + /// a class for storing various properties of agent alerts + /// + public class AgentAlertInfo + { + public int Id { get; set; } + public int DelayBetweenResponses { get; set; } + public string EventDescriptionKeyword { get; set; } + public string EventSource { get; set; } + public int HasNotification { get; set; } + public NotifyMethods IncludeEventDescription { get; set; } + public bool IsEnabled { get; set; } + public Guid JobId { get; set; } + public string JobName { get; set; } + public DateTime LastOccurrenceDate { get; set; } + public DateTime LastResponseDate { get; set; } + public int MessageId { get; set; } + public string NotificationMessage { get; set; } + public int OccurrenceCount { get; } + public string PerformanceCondition { get; set; } + public int Severity { get; set; } + public string DatabaseName { get; set; } + public DateTime CountResetDate { get; } + public string CategoryName { get; set; } + public AlertType AlertType { get; set; } + public string WmiEventNamespace { get; set; } + public string WmiEventQuery { get; set; } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentAlertsRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentAlertsRequest.cs new file mode 100644 index 00000000..29955836 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentAlertsRequest.cs @@ -0,0 +1,144 @@ +// +// 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; +using Microsoft.SqlTools.ServiceLayer.Utility; +using Microsoft.SqlTools.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.Agent.Contracts +{ + /// + /// SQL Agent Job activity parameters + /// + public class AgentAlertsParams : GeneralRequestDetails + { + public string OwnerUri { get; set; } + } + + /// + /// SQL Agent Job activity result + /// + public class AgentAlertsResult + { + + public bool Succeeded { get; set; } + + public string ErrorMessage { get; set; } + + public AgentAlertInfo[] Alerts { get; set; } + } + + /// + /// SQL Agent Alerts request type + /// + public class AgentAlertsRequest + { + /// + /// Request definition + /// + public static readonly + RequestType Type = + RequestType.Create("agent/alerts"); + } + + /// + /// SQL Agent create Alert params + /// + public class CreateAgentAlertParams : GeneralRequestDetails + { + public string OwnerUri { get; set; } + + public AgentAlertInfo Alert { get; set; } + } + + /// + /// SQL Agent create Alert result + /// + public class CreateAgentAlertResult + { + public bool Succeeded { get; set; } + + public string ErrorMessage { get; set; } + } + + /// + /// SQL Agent create Alert request type + /// + public class CreateAgentAlertRequest + { + /// + /// Request definition + /// + public static readonly + RequestType Type = + RequestType.Create("agent/createalert"); + } + + /// + /// SQL Agent delete Alert params + /// + public class DeleteAgentAlertParams : GeneralRequestDetails + { + public string OwnerUri { get; set; } + + public AgentAlertInfo Alert { get; set; } + } + + /// + /// SQL Agent delete Alert result + /// + public class DeleteAgentAlertResult + { + public bool Succeeded { get; set; } + + public string ErrorMessage { get; set; } + } + + /// + /// SQL Agent delete Alert request type + /// + public class DeleteAgentAlertRequest + { + /// + /// Request definition + /// + public static readonly + RequestType Type = + RequestType.Create("agent/deletealert"); + } + + /// + /// SQL Agent update Alert params + /// + public class UpdateAgentAlertParams : GeneralRequestDetails + { + public string OwnerUri { get; set; } + + public AgentAlertInfo Alert { get; set; } + } + + /// + /// SQL Agent update Alert result + /// + public class UpdateAgentAlertResult + { + public bool Succeeded { get; set; } + + public string ErrorMessage { get; set; } + } + + /// + /// SQL Agent update Alert request type + /// + public class UpdateAgentAlertRequest + { + /// + /// Request definition + /// + public static readonly + RequestType Type = + RequestType.Create("agent/updatealert"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentOperatorInfo.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentOperatorInfo.cs new file mode 100644 index 00000000..4aa2316e --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentOperatorInfo.cs @@ -0,0 +1,50 @@ +// +// 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 Microsoft.SqlTools.ServiceLayer.Agent; + +namespace Microsoft.SqlTools.ServiceLayer.Agent.Contracts +{ + [Flags] + public enum WeekDays + { + Sunday = 1, + Monday = 2, + Tuesday = 4, + Wednesday = 8, + Thursday = 16, + Friday = 32, + WeekDays = 62, + Saturday = 64, + WeekEnds = 65, + EveryDay = 127 + } + + /// + /// a class for storing various properties of agent operators + /// + public class AgentOperatorInfo + { + public string Name { get; set; } + public int Id { get; set; } + public string EmailAddress { get; set; } + public bool Enabled { get; set; } + public DateTime LastEmailDate { get; set; } + public DateTime LastNetSendDate { get; set; } + public DateTime LastPagerDate { get; set; } + public string PagerAddress { get; set; } + public string CategoryName { get; set; } + public WeekDays PagerDays { get; set; } + public TimeSpan SaturdayPagerEndTime { get; set; } + public TimeSpan SaturdayPagerStartTime { get; set; } + public TimeSpan SundayPagerEndTime { get; set; } + public TimeSpan SundayPagerStartTime { get; set; } + public string NetSendAddress { get; set; } + public TimeSpan WeekdayPagerStartTime { get; set; } + public TimeSpan WeekdayPagerEndTime { get; set; } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentOperatorsRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentOperatorsRequest.cs new file mode 100644 index 00000000..c082e954 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentOperatorsRequest.cs @@ -0,0 +1,143 @@ +// +// 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; +using Microsoft.SqlTools.ServiceLayer.Utility; +using Microsoft.SqlTools.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.Agent.Contracts +{ + /// + /// SQL Agent Operators request parameters + /// + public class AgentOperatorsParams : GeneralRequestDetails + { + public string OwnerUri { get; set; } + } + + /// + /// SQL Agent Operators request result + /// + public class AgentOperatorsResult + { + public bool Succeeded { get; set; } + + public string ErrorMessage { get; set; } + + public AgentOperatorInfo[] Operators { get; set; } + } + + /// + /// SQL Agent Operators request type + /// + public class AgentOperatorsRequest + { + /// + /// Request definition + /// + public static readonly + RequestType Type = + RequestType.Create("agent/operators"); + } + + /// + /// SQL Agent create Operator params + /// + public class CreateAgentOperatorParams : GeneralRequestDetails + { + public string OwnerUri { get; set; } + + public AgentOperatorInfo Operator { get; set; } + } + + /// + /// SQL Agent create Operator result + /// + public class CreateAgentOperatorResult + { + public bool Succeeded { get; set; } + + public string ErrorMessage { get; set; } + } + + /// + /// SQL Agent create Operator request type + /// + public class CreateAgentOperatorRequest + { + /// + /// Request definition + /// + public static readonly + RequestType Type = + RequestType.Create("agent/createoperator"); + } + + /// + /// SQL Agent delete Operator params + /// + public class DeleteAgentOperatorParams : GeneralRequestDetails + { + public string OwnerUri { get; set; } + + public AgentOperatorInfo Operator { get; set; } + } + + /// + /// SQL Agent delete Operator result + /// + public class DeleteAgentOperatorResult + { + public bool Succeeded { get; set; } + + public string ErrorMessage { get; set; } + } + + /// + /// SQL Agent delete Operator request type + /// + public class DeleteAgentOperatorRequest + { + /// + /// Request definition + /// + public static readonly + RequestType Type = + RequestType.Create("agent/deleteoperator"); + } + + /// + /// SQL Agent update Operator params + /// + public class UpdateAgentOperatorParams : GeneralRequestDetails + { + public string OwnerUri { get; set; } + + public AgentOperatorInfo Operator { get; set; } + } + + /// + /// SQL Agent update Operator result + /// + public class UpdateAgentOperatorResult + { + public bool Succeeded { get; set; } + + public string ErrorMessage { get; set; } + } + + /// + /// SQL Agent update Operator request type + /// + public class UpdateAgentOperatorRequest + { + /// + /// Request definition + /// + public static readonly + RequestType Type = + RequestType.Create("agent/updateoperator"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentProxyInfo.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentProxyInfo.cs new file mode 100644 index 00000000..a8f9572a --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentProxyInfo.cs @@ -0,0 +1,25 @@ +// +// 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 Microsoft.SqlTools.ServiceLayer.Agent; + +namespace Microsoft.SqlTools.ServiceLayer.Agent.Contracts +{ + /// + /// a class for storing various properties of agent proxy accounts + /// + public class AgentProxyInfo + { + public int Id { get; set; } + public string AccountName { get; set; } + public string Description { get; set; } + public string CredentialName { get; set; } + public string CredentialIdentity { get; set; } + public int CredentialId { get; set; } + public bool IsEnabled { get; set; } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentProxyRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentProxyRequest.cs new file mode 100644 index 00000000..7e5a3e7f --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/Contracts/AgentProxyRequest.cs @@ -0,0 +1,145 @@ +// +// 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; +using Microsoft.SqlTools.ServiceLayer.Utility; +using Microsoft.SqlTools.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.Agent.Contracts +{ + /// + /// SQL Agent proxy accounts parameters + /// + public class AgentProxiesParams : GeneralRequestDetails + { + public string OwnerUri { get; set; } + } + + /// + /// SQL Agent proxy accounts result + /// + public class AgentProxiesResult + { + public bool Succeeded { get; set; } + + public string ErrorMessage { get; set; } + + public AgentProxyInfo[] Proxies { get; set; } + } + + /// + /// SQL Agent Proxy Accounts request type + /// + public class AgentProxiesRequest + { + /// + /// Request definition + /// + public static readonly + RequestType Type = + RequestType.Create("agent/proxies"); + } + + /// + /// SQL Agent create Proxy Account params + /// + public class CreateAgentProxyParams : GeneralRequestDetails + { + public string OwnerUri { get; set; } + + public AgentProxyInfo Proxy { get; set; } + } + + /// + /// SQL Agent create Proxy result + /// + public class CreateAgentProxyResult + { + public bool Succeeded { get; set; } + + public string ErrorMessage { get; set; } + } + + /// + /// SQL Agent create Proxy request type + /// + public class CreateAgentProxyRequest + { + /// + /// Request definition + /// + public static readonly + RequestType Type = + RequestType.Create("agent/createproxy"); + } + + /// + /// SQL Agent delete Proxy params + /// + public class DeleteAgentProxyParams : GeneralRequestDetails + { + public string OwnerUri { get; set; } + + public AgentProxyInfo Proxy { get; set; } + } + + /// + /// SQL Agent delete Proxy result + /// + public class DeleteAgentProxyResult + { + public bool Succeeded { get; set; } + + public string ErrorMessage { get; set; } + } + + /// + /// SQL Agent delete Proxy request type + /// + public class DeleteAgentProxyRequest + { + /// + /// Request definition + /// + public static readonly + RequestType Type = + RequestType.Create("agent/deleteproxy"); + } + + /// + /// SQL Agent update Proxy params + /// + public class UpdateAgentProxyParams : GeneralRequestDetails + { + public string OwnerUri { get; set; } + + public string OriginalProxyName { get; set; } + + public AgentProxyInfo Proxy { get; set; } + } + + /// + /// SQL Agent update Proxy result + /// + public class UpdateAgentProxyResult + { + public bool Succeeded { get; set; } + + public string ErrorMessage { get; set; } + } + + /// + /// SQL Agent update Proxy request type + /// + public class UpdateAgentProxyRequest + { + /// + /// Request definition + /// + public static readonly + RequestType Type = + RequestType.Create("agent/updateproxy"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/Jobs/AgentAlert.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Jobs/AgentAlert.cs new file mode 100644 index 00000000..24a2395d --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/Jobs/AgentAlert.cs @@ -0,0 +1,166 @@ +// +// 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; +using System.Data; +using Microsoft.SqlServer.Management.Common; +using Microsoft.SqlServer.Management.Diagnostics; +using Microsoft.SqlServer.Management.Sdk.Sfc; +using Microsoft.SqlServer.Management.Smo; +using Microsoft.SqlServer.Management.Smo.Agent; +using Microsoft.SqlTools.ServiceLayer.Admin; +using Microsoft.SqlTools.ServiceLayer.Agent.Contracts; +using Microsoft.SqlTools.ServiceLayer.Management; + +namespace Microsoft.SqlTools.ServiceLayer.Agent +{ + /// + /// AgentAlert class + /// + internal class AgentAlert : ManagementActionBase + { + /// + /// Agent alert info instance + /// + private AgentAlertInfo alertInfo = null; + + /// + /// Default constructor that will be used to create dialog + /// + /// + public AgentAlert(CDataContainer dataContainer, AgentAlertInfo alertInfo) + { + this.alertInfo = alertInfo; + this.DataContainer = dataContainer; + } + + private static string GetAlertName(CDataContainer container) + { + string alertName = null; + STParameters parameters = new STParameters(); + parameters.SetDocument(container.Document); + if (parameters.GetParam("alert", ref alertName) == false || string.IsNullOrWhiteSpace(alertName)) + { + throw new Exception("SRError.AlertNameCannotBeBlank"); + } + return alertName.Trim(); + } + + public bool Drop() + { + // fail if the user is not in the sysadmin role + if (!this.DataContainer.Server.ConnectionContext.IsInFixedServerRole(FixedServerRoles.SysAdmin)) + { + return false; + } + + string alertName = GetAlertName(this.DataContainer); + if (this.DataContainer.Server.JobServer.Alerts.Contains(alertName)) + { + this.DataContainer.Server.JobServer.Alerts.Refresh(); + if (this.DataContainer.Server.JobServer.Alerts.Contains(alertName)) + { + Alert alert = this.DataContainer.Server.JobServer.Alerts[alertName]; + if (alert != null) + { + alert.DropIfExists(); + } + } + } + return true; + } + + public bool CreateOrUpdate() + { + Alert alert = null; + string alertName = GetAlertName(this.DataContainer); + bool createNewAlert = true; + + try + { + // check if alert already exists + if (this.DataContainer.Server.JobServer.Alerts.Contains(alertName)) + { + this.DataContainer.Server.JobServer.Alerts.Refresh(); // Try to recover + if (this.DataContainer.Server.JobServer.Alerts.Contains(alertName)) // If still no luck + { + // use the existing alert + alert = this.DataContainer.Server.JobServer.Alerts[alertName]; + createNewAlert = false; + } + } + + // create a new alert + if (createNewAlert) + { + alert = new Alert(this.DataContainer.Server.JobServer, alertName); + } + + // apply changes from input parameter to SMO alert object + UpdateAlertProperties(alert); + + if (createNewAlert) + { + alert.Create(); + } + else + { + // don't bother trying to update the alert unless they are sysadmin. + if (!this.DataContainer.Server.ConnectionContext.IsInFixedServerRole(FixedServerRoles.SysAdmin)) + { + return false; + } + alert.Alter(); + } + + return true; + } + catch (Exception e) + { + ApplicationException applicationException; + if (createNewAlert) + { + applicationException = new ApplicationException("AgentAlertSR.CannotCreateNewAlert", e); + } + else + { + applicationException = new ApplicationException("AgentAlertSR.CannotAlterAlert", e); + } + throw applicationException; + } + } + + private void UpdateAlertProperties(Alert alert) + { + if (alert == null) + { + throw new ArgumentNullException("alert"); + } + + if (!string.IsNullOrWhiteSpace(this.DataContainer.ConnectionInfo.DatabaseName)) + { + alert.DatabaseName = this.DataContainer.ConnectionInfo.DatabaseName; + } + + if (!string.IsNullOrWhiteSpace(this.alertInfo.CategoryName)) + { + alert.CategoryName = this.alertInfo.CategoryName; + } + + alert.IsEnabled = this.alertInfo.IsEnabled; + + if (alertInfo.AlertType == Contracts.AlertType.SqlServerEvent) + { + alert.Severity = this.alertInfo.Severity; + alert.MessageID = this.alertInfo.MessageId; + if (!string.IsNullOrWhiteSpace(this.alertInfo.EventDescriptionKeyword)) + { + alert.EventDescriptionKeyword = this.alertInfo.EventDescriptionKeyword; + } + } + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/Jobs/AgentOperator.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Jobs/AgentOperator.cs new file mode 100644 index 00000000..c26ad534 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/Jobs/AgentOperator.cs @@ -0,0 +1,947 @@ +// +// 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; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Diagnostics; +using System.Globalization; +using System.Threading; +using Microsoft.SqlServer.Management.Common; +using Microsoft.SqlServer.Management.Diagnostics; +using Microsoft.SqlServer.Management.Sdk.Sfc; +using Microsoft.SqlServer.Management.Smo; +using Microsoft.SqlServer.Management.Smo.Agent; +using Microsoft.SqlTools.ServiceLayer.Admin; +using Microsoft.SqlTools.ServiceLayer.Agent.Contracts; +using Microsoft.SqlTools.ServiceLayer.Management; + +namespace Microsoft.SqlTools.ServiceLayer.Agent +{ + /// + /// Agent Operators management class + /// + internal class AgentOperator : ManagementActionBase + { + private AgentOperatorInfo operatorInfo; + + AgentOperatorsData operatorsData = null; + + /// + /// Constructor + /// + public AgentOperator(CDataContainer dataContainer, AgentOperatorInfo operatorInfo) + { + try + { + if (dataContainer == null) + { + throw new ArgumentNullException("dataContainer"); + } + + if (operatorInfo == null) + { + throw new ArgumentNullException("operatorInfo"); + } + + this.operatorInfo = operatorInfo; + this.DataContainer = dataContainer; + + STParameters parameters = new STParameters(); + parameters.SetDocument(dataContainer.Document); + + string agentOperatorName = null; + if (parameters.GetParam("operator", ref agentOperatorName)) + { + this.operatorsData = new AgentOperatorsData(dataContainer, agentOperatorName); + } + else + { + throw new ArgumentNullException("agentOperatorName"); + } + } + catch(Exception e) + { + throw new ApplicationException("AgentOperatorsSR.FailedToCreateInitializeAgentOperatorDialog", e); + } + } + + /// + /// Clean up any resources being used. + /// + protected override void Dispose(bool disposing) + { + if(disposing) + { + } + base.Dispose(disposing); + } + + public bool CreateOrUpdate() + { + this.operatorsData.ApplyChanges(this.operatorInfo); + return true; + } + } + + #region internal structures + /// + /// Provides data to be consumed in the job notification grid + /// + internal struct AgentJobNotificationHelper + { + /// + /// constructor + /// + /// job name + /// + /// + public AgentJobNotificationHelper(string name, CompletionAction notifyEmail, CompletionAction notifyPager) + { + this.Name = name; + this.NotifyEmail = notifyEmail; + this.NotifyPager = notifyPager; + } + /// + /// Name of the job + /// + public string Name; + /// + /// job email notification action + /// + public CompletionAction NotifyEmail; + /// + /// job pager notification action + /// + public CompletionAction NotifyPager; + } + /// + /// Provides data to be consumed in the alert notification grid + /// + internal struct AgentAlertNotificationHelper + { + /// + /// constructor + /// + /// Name of the alert + /// + /// + /// Alert object + public AgentAlertNotificationHelper(string name, bool notifyEmail, bool notifyPager, Alert alert) + { + this.Name = name; + this.NotifyEmail = notifyEmail; + this.NotifyPager = notifyPager; + this.Alert = alert; + } + /// + /// Alert name + /// + public string Name; + /// + /// Indicates whether the alert will notify the operator through email + /// + public bool NotifyEmail; + /// + /// Indicates whether the alert will notify the operator through pager + /// + public bool NotifyPager; + /// + /// Alert object. optimisation to stop us having to lookup the alert object when needed + /// + public Alert Alert; + } + #endregion + /// + /// Proxy class for the AgentOperators dialog and property pages. + /// Performs lazy instantiation of groups of data based around the operators dialog property pages + /// + internal class AgentOperatorsData + { + #region members + /// + /// Data container + /// + CDataContainer dataContainer; + /// + /// Original operator name. Empty if we are creating a new operator + /// + string originalOperatorName = String.Empty; + /// + /// Indicates whether we are creating an operator or not + /// + bool createMode; + + /// + /// Has then data for the general page been initialised + /// + bool generalInitialized = false; + /// + /// has the data for the history page been initialised + /// + bool historyInitialized = false; + + /// + /// True if this operator cannot be modified + /// + bool readOnly = false; + + #region general items + string name; + bool enabled; + string emailAddress; + string pagerAddress; + SqlServer.Management.Smo.Agent.WeekDays pagerDays; + DateTime weekdayStartTime; + DateTime weekdayEndTime; + DateTime saturdayStartTime; + DateTime saturdayEndTime; + DateTime sundayStartTime; + DateTime sundayEndTime; + #endregion + + #region notification items + /// + /// will be null if the alert notifications have not been initialised + /// + IList alertNotifications; + /// + /// will be null if the job notifications have not been initialised + /// + IList jobNotifications; + #endregion + + #region history items + DateTime lastEmailDate; + DateTime lastPagerDate; + #endregion + #endregion + + #region properties + /// + /// indicates if the data is in create mode + /// + public bool Creating + { + get + { + return this.createMode; + } + } + /// + /// name of the object + /// + public string Name + { + get + { + LoadGeneralData(); + return name; + } + set + { + LoadGeneralData(); + name = value; + } + } + /// + /// Indicates if the dataobject is readonly + /// + public bool ReadOnly + { + get + { + return this.readOnly; + } + } + #region general items + /// + /// indicates whether or not the operator is enabled + /// + public bool Enabled + { + get + { + LoadGeneralData(); + return enabled; + } + set + { + LoadGeneralData(); + enabled = value; + } + } + /// + /// email address of this operator + /// + public string EmailAddress + { + get + { + LoadGeneralData(); + return this.emailAddress; + } + set + { + LoadGeneralData(); + this.emailAddress = value; + } + } + /// + /// pager address of this operator + /// + public string PagerAddress + { + get + { + LoadGeneralData(); + return this.pagerAddress; + } + set + { + LoadGeneralData(); + this.pagerAddress = value; + } + } + + /// + /// the days of the week the operator is active + /// + public SqlServer.Management.Smo.Agent.WeekDays PagerDays + { + get + { + LoadGeneralData(); + return this.pagerDays; + } + set + { + LoadGeneralData(); + this.pagerDays = value; + } + } + /// + /// Weekday start time for this operator to be active + /// + public DateTime WeekdayStartTime + { + get + { + LoadGeneralData(); + return this.weekdayStartTime; + } + set + { + LoadGeneralData(); + this.weekdayStartTime = value; + } + } + /// + /// Weekday end time for this operator to be active + /// + public DateTime WeekdayEndTime + { + get + { + LoadGeneralData(); + return this.weekdayEndTime; + } + set + { + LoadGeneralData(); + this.weekdayEndTime = value; + } + } + /// + /// Saturday start time for this operator to be active + /// + public DateTime SaturdayStartTime + { + get + { + LoadGeneralData(); + return this.saturdayStartTime; + } + set + { + LoadGeneralData(); + this.saturdayStartTime = value; + } + } + /// + /// Saturday end time for this operator to be active + /// + public DateTime SaturdayEndTime + { + get + { + LoadGeneralData(); + return this.saturdayEndTime; + } + set + { + LoadGeneralData(); + this.saturdayEndTime = value; + } + } + /// + /// Sunday start time for this operator to be active + /// + public DateTime SundayStartTime + { + get + { + LoadGeneralData(); + return this.sundayStartTime; + } + set + { + LoadGeneralData(); + this.sundayStartTime = value; + } + } + /// + /// Saturday end time for this operator to be active + /// + public DateTime SundayEndTime + { + get + { + LoadGeneralData(); + return this.sundayEndTime; + } + set + { + LoadGeneralData(); + this.sundayEndTime = value; + } + } + #endregion + + #region notification items + /// + /// Alerts that notify this operator + /// + public IList AlertNotifications + { + get + { + LoadAlertNotificationData(); + return this.alertNotifications; + } + set + { + this.alertNotifications = value; + } + } + /// + /// Jobs that notify this operator. This has to be set through the jobs dialog and is read only + /// + public IList JobNotifications + { + get + { + LoadJobNotificationData(); + return this.jobNotifications; + } + } + #endregion + + #region history items + /// + /// Date this operator was last emailed + /// + public DateTime LastEmailDate + { + get + { + LoadHistoryData(); + return this.lastEmailDate; + } + } + /// + /// Date this operator was last paged + /// + public DateTime LastPagerDate + { + get + { + LoadHistoryData(); + return this.lastPagerDate; + } + } + + #endregion + #endregion + + #region Constructors + + public AgentOperatorsData(CDataContainer dataContainer, string operatorName) + { + if (dataContainer == null) + { + throw new ArgumentNullException("dataContainer"); + } + if (operatorName == null) + { + throw new ArgumentNullException("operatorName"); + } + + this.dataContainer = dataContainer; + + this.readOnly = !this.dataContainer.Server.ConnectionContext.IsInFixedServerRole(FixedServerRoles.SysAdmin); + this.originalOperatorName = operatorName; + this.name = operatorName; + + this.createMode = operatorName.Length == 0; + } + #endregion + + #region data loading + /// + /// load data for the general tab. This can be called multiple times but will only load the data once intially + /// or after a reset + /// + private void LoadGeneralData() + { + if(this.generalInitialized) + return; + + // load defaults if we're creating + if(createMode) + { + LoadGeneralDefaults(); + return; + } + + // lookup the operator this will throw if it has been deleted. + Microsoft.SqlServer.Management.Smo.Agent.Operator currentOperator = GetCurrentOperator(); + + // setup the members + this.name = currentOperator.Name; + this.enabled = currentOperator.Enabled; + this.emailAddress = currentOperator.EmailAddress; + this.pagerAddress = currentOperator.PagerAddress; + + this.pagerDays = currentOperator.PagerDays; + + this.weekdayStartTime = ConvertAgentTime(currentOperator.WeekdayPagerStartTime); + this.weekdayEndTime = ConvertAgentTime(currentOperator.WeekdayPagerEndTime); + + this.saturdayStartTime = ConvertAgentTime(currentOperator.SaturdayPagerStartTime); + this.saturdayEndTime = ConvertAgentTime(currentOperator.SaturdayPagerEndTime); + + this.sundayStartTime = ConvertAgentTime(currentOperator.SundayPagerStartTime); + this.sundayEndTime = ConvertAgentTime(currentOperator.SundayPagerEndTime); + + this.generalInitialized = true; + + } + /// + /// Load the data for the jobs that notify this operator. Can be called multiple times and will + /// only load the data initially, or after a reset. + /// + private void LoadJobNotificationData() + { + if(this.jobNotifications != null) + return; + + // just set defaults if we're creating as no jobs will point to this operator yet. + if(createMode) + { + LoadJobNotificationDefaults(); + return; + } + + JobServer jobServer = GetJobServer(); + + this.jobNotifications = new List(); + + // we have to loop through each job and see if it notifies us. + foreach(Job job in jobServer.Jobs) + { + bool emailOperator = (job.OperatorToEmail == this.originalOperatorName); + bool pageOperator = (job.OperatorToPage == this.originalOperatorName); + if(emailOperator || pageOperator ) + { + // only return jobs that notify this operator + AgentJobNotificationHelper notification = new AgentJobNotificationHelper(job.Name + , job.EmailLevel + , job.PageLevel + ); + this.jobNotifications.Add(notification); + } + } + } + /// + /// Load alerts that notify this operator + /// + private void LoadAlertNotificationData() + { + if (this.alertNotifications != null) + { + return; + } + + // defaults in create ode + if (createMode) + { + LoadAlertNotificationDefaults(); + return; + } + + this.alertNotifications = new List(); + + Microsoft.SqlServer.Management.Smo.Agent.Operator agentOperator = GetCurrentOperator(); + JobServer jobServer = GetJobServer(); + + // see all alerts that notifuy this operator + DataTable notifications = agentOperator.EnumNotifications(); + DataRow alertRow; + bool notifyEmail; + bool notifyPager; + AgentAlertNotificationHelper alertNotification; + + // Add every alert to the structure + foreach (Alert alert in jobServer.Alerts) + { + alertRow = null; + + // see if the alert notifies us already + foreach (DataRow row in notifications.Rows) + { + if((string)row["AlertName"] == alert.Name) + { + alertRow = row; + break; + } + } + + // set if the current alert notifies this operator + // if so so how + if (alertRow != null) + { + notifyEmail = (bool)alertRow["UseEmail"]; + notifyPager = (bool)alertRow["UsePager"]; + } + else + { notifyEmail = false; + notifyPager = false; + } + + alertNotification = new AgentAlertNotificationHelper(alert.Name + ,notifyEmail + ,notifyPager + ,alert); + + this.alertNotifications.Add(alertNotification); + } + } + + /// + /// load the notifiaction history for the operator + /// + private void LoadHistoryData() + { + if (this.historyInitialized) + { + return; + } + + if (this.createMode) + { + LoadHistoryDefaults(); + return; + } + + Microsoft.SqlServer.Management.Smo.Agent.Operator currentOperator = GetCurrentOperator(); + + this.lastEmailDate = currentOperator.LastEmailDate; + this.lastPagerDate = currentOperator.LastPagerDate; + + } + #endregion + + #region saving + /// + /// apply any changes to the operator. If the operator does not exist create it. + /// + public void ApplyChanges(AgentOperatorInfo operatorInfo) + { + // do nothing if we are read only + if (this.readOnly) + { + return; + } + + JobServer jobServer = GetJobServer(); + + // get the operator. This will create a new one if it does not already exist + Microsoft.SqlServer.Management.Smo.Agent.Operator currentOperator = GetCurrentOperator(); + + // general tab + currentOperator.Enabled = operatorInfo.Enabled; + + if (!string.IsNullOrWhiteSpace(operatorInfo.EmailAddress)) + { + currentOperator.EmailAddress = operatorInfo.EmailAddress; + } + + if (!string.IsNullOrWhiteSpace(operatorInfo.PagerAddress)) + { + currentOperator.PagerAddress = operatorInfo.PagerAddress; + } + + currentOperator.PagerDays = this.pagerDays; + + if ((operatorInfo.PagerDays & Contracts.WeekDays.WeekDays) > 0) + { + currentOperator.WeekdayPagerStartTime = operatorInfo.WeekdayPagerStartTime; + currentOperator.WeekdayPagerEndTime = operatorInfo.WeekdayPagerEndTime; + } + + if ((operatorInfo.PagerDays & Contracts.WeekDays.Saturday) > 0) + { + currentOperator.SaturdayPagerStartTime = operatorInfo.SaturdayPagerStartTime; + currentOperator.SaturdayPagerEndTime = operatorInfo.SaturdayPagerEndTime; + } + + if ((operatorInfo.PagerDays & Contracts.WeekDays.Sunday) > 0) + { + currentOperator.SundayPagerStartTime = operatorInfo.SundayPagerStartTime; + currentOperator.SundayPagerEndTime = operatorInfo.SundayPagerEndTime; + } + + if (this.createMode) + { + // create the object + currentOperator.Create(); + this.originalOperatorName = this.name; + } + else + { + // alter the object + currentOperator.Alter(); + } + + // only set this up if the notifications has been set + if (this.alertNotifications != null) + { + SqlServer.Management.Smo.Agent.NotifyMethods notifyMethods; + for (int i = 0; i < alertNotifications.Count; ++i) + { + notifyMethods = 0; + + if (alertNotifications[i].NotifyEmail) + { + notifyMethods |= SqlServer.Management.Smo.Agent.NotifyMethods.NotifyEmail; + } + if (alertNotifications[i].NotifyPager) + { + notifyMethods |= SqlServer.Management.Smo.Agent.NotifyMethods.Pager; + } + + bool alertAlreadyNotifiesOperator = false; + + // if we're not creating see if the current alert already notifies this operator + if (!createMode) + { + DataTable notifications = alertNotifications[i].Alert.EnumNotifications(this.originalOperatorName); + if (notifications.Rows.Count > 0) + { + alertAlreadyNotifiesOperator = true; + } + } + + // either update or clear existing notifications + if (alertAlreadyNotifiesOperator) + { + if(notifyMethods != SqlServer.Management.Smo.Agent.NotifyMethods.None) + { + alertNotifications[i].Alert.UpdateNotification(this.originalOperatorName, notifyMethods); + } + else + { + alertNotifications[i].Alert.RemoveNotification(this.originalOperatorName); + } + } + else if(notifyMethods != SqlServer.Management.Smo.Agent.NotifyMethods.None) + { + // add a new notification + alertNotifications[i].Alert.AddNotification(this.originalOperatorName, notifyMethods); + } + } + } + + // see if we need to rename. This has to be done last otherwise any scripts generated will be incorrect. + if (!this.createMode && currentOperator.Name != this.originalOperatorName) + { + currentOperator.Rename(this.name); + if(this.dataContainer.Server.ConnectionContext.SqlExecutionModes != SqlExecutionModes.CaptureSql) + { + this.originalOperatorName = this.name; + } + } + // update state if we aren't scripting + if (this.createMode && this.dataContainer.Server.ConnectionContext.SqlExecutionModes != SqlExecutionModes.CaptureSql) + { + this.createMode = false; + } + } + #endregion + + #region reset + /// + /// Reset the object to it's original state / reload any data from the erver + /// + public void Reset() + { + JobServer jobServer = GetJobServer(); + this.generalInitialized = false; + if (this.jobNotifications != null) + { + // ensure the individual jobs are reset also + jobServer.Jobs.Refresh(true); + this.jobNotifications = null; + } + if (this.alertNotifications != null) + { + // ensure the individual jobs are reset also + jobServer.Alerts.Refresh(true); + this.alertNotifications = null; + } + this.historyInitialized = false; + } + #endregion + + #region defaults + /// + /// set general tab defaults + /// + private void LoadGeneralDefaults() + { + name = String.Empty; + this.emailAddress = String.Empty; + this.pagerAddress = String.Empty; + enabled = true; + pagerDays = 0; + + weekdayStartTime = saturdayStartTime = sundayStartTime = new DateTime(2000, 1, 1, 8, 0, 0); + weekdayEndTime = saturdayEndTime = sundayEndTime = new DateTime(2000, 1, 1, 18, 0, 0); + + this.generalInitialized = true; + } + /// + /// Set job notification defaults. This is just an empty list + /// + private void LoadJobNotificationDefaults() + { + this.jobNotifications = new List(); + } + /// + /// set the alert notification defaults. This list will contain all of the alerts + /// + private void LoadAlertNotificationDefaults() + { + this.alertNotifications = new List(); + + JobServer jobServer = GetJobServer(); + + AgentAlertNotificationHelper alertNotification; + foreach(Alert alert in jobServer.Alerts) + { + alertNotification = new AgentAlertNotificationHelper(alert.Name, notifyEmail:false, notifyPager:false, alert: alert); + this.alertNotifications.Add(alertNotification); + } + } + /// + /// load defaults for the history page + /// + private void LoadHistoryDefaults() + { + this.lastEmailDate = DateTime.MinValue; + this.lastPagerDate = DateTime.MinValue; + + this.historyInitialized = true; + } + #endregion + + #region helpers + /// + /// Get the job server. Will throw if it is not available + /// + /// Job server object + private JobServer GetJobServer() + { + JobServer jobServer = this.dataContainer.Server.JobServer; + if(jobServer == null) + { + throw new ApplicationException("AgentOperatorsSR.JobServerIsNotAvailable"); + } + return jobServer; + } + + /// + /// Get the current operator. If we are creating this will be a new operator. If we are modifying + /// an existing operator it will be the existing operator, and will throw if the operator has been + /// deleted. + /// + /// Operator object + private Operator GetCurrentOperator() + { + JobServer jobServer = GetJobServer(); + + Operator currentOperator = jobServer.Operators[this.originalOperatorName]; + this.createMode = (currentOperator == null); + if (this.createMode) + { + currentOperator = new Microsoft.SqlServer.Management.Smo.Agent.Operator(jobServer, this.name); + } + return currentOperator; + } + + /// + /// + /// + /// + /// + static public TimeSpan ConvertAgentTime(DateTime dateTime) + { + return new TimeSpan(dateTime.Hour, dateTime.Minute, dateTime.Second); + } + /// + /// + /// + /// + /// + static public DateTime ConvertAgentTime(TimeSpan dateTime) + { + return new DateTime(2000, 1, 1, dateTime.Hours, dateTime.Minutes, dateTime.Seconds); + } + + /// + /// + /// + /// + /// + static public int ConvertAgentTimeToInt(DateTime dateTime) + { + return dateTime.Hour * 10000 + dateTime.Minute * 100 + dateTime.Second; + } + + /// + /// + /// + /// + /// + static public DateTime ConvertAgentTime(int dateTime) + { + return new DateTime(2000, 1, 1, (int)(dateTime / 10000), (int)((dateTime - (dateTime / 10000) * 10000) / 100), (int)(dateTime - (dateTime / 100) * 100)); + } + #endregion + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/Jobs/AgentProxyAccount.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Jobs/AgentProxyAccount.cs new file mode 100644 index 00000000..6b07c0cb --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/Jobs/AgentProxyAccount.cs @@ -0,0 +1,467 @@ +// +// 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.ComponentModel; +using System.Collections; +using System.Data; +using System.Data.SqlClient; +using Microsoft.SqlServer.Management.Common; +using Microsoft.SqlServer.Management.Diagnostics; +using Microsoft.SqlServer.Management.Sdk.Sfc; +using Microsoft.SqlServer.Management.Smo; +using Microsoft.SqlServer.Management.Smo.Agent; +using Microsoft.SqlTools.ServiceLayer.Admin; +using Microsoft.SqlTools.ServiceLayer.Agent.Contracts; +using Microsoft.SqlTools.ServiceLayer.Management; + +namespace Microsoft.SqlTools.ServiceLayer.Agent +{ + internal class AgentProxyAccount : ManagementActionBase + { + #region Constants + internal const string ProxyAccountPropertyName = "proxyaccount"; + internal const string ProxyAccountSubsystem = "SubSystem"; + internal const string ProxyAccountMode = "Mode"; + internal const string ProxyAccountDuplicateMode = "Duplicate"; + internal const int SysnameLength = 256; + internal const int DescriptionLength = 512; + #endregion + + internal enum ProxyPrincipalType + { + SqlLogin, + MsdbRole, + ServerRole + } + + // Collections of principals for logins/server roles/msdb roles + private ArrayList[] principals; + + // Name of the proxy account we work with + private string proxyAccountName; + + private AgentProxyInfo proxyInfo; + + // Flag indicating that proxy account should be duplicated + private bool duplicate; + + public static string SysadminAccount + { + get { return "AgentProxyAccountSR.SysadminAccount"; } + } + + /// + /// Main constructor. Creates all pages and adds them + /// to the tree control. + /// + public AgentProxyAccount(CDataContainer dataContainer, AgentProxyInfo proxyInfo) + { + this.DataContainer = dataContainer; + this.proxyInfo = proxyInfo; + + // Find out if we are creating a new proxy account or + // modifying an existing one. + GetProxyAccountName(dataContainer, ref this.proxyAccountName, ref this.duplicate); + } + + /// + /// It creates a new ProxyAccount or gets an existing + /// one from JobServer and updates all properties. + /// + private bool CreateOrUpdateProxyAccount( + AgentProxyInfo proxyInfo, + bool isUpdate) + { + ProxyAccount proxyAccount = null; + if (!isUpdate) + { + proxyAccount = new ProxyAccount(this.DataContainer.Server.JobServer, + proxyInfo.AccountName, + proxyInfo.CredentialName, + proxyInfo.IsEnabled, + proxyInfo.Description); + + UpdateProxyAccount(proxyAccount); + proxyAccount.Create(); + } + else if (this.DataContainer.Server.JobServer.ProxyAccounts.Contains(this.proxyAccountName)) + { + // Try refresh and check again + this.DataContainer.Server.JobServer.ProxyAccounts.Refresh(); + if (this.DataContainer.Server.JobServer.ProxyAccounts.Contains(this.proxyAccountName)) + { + // fail since account exists and asked to create a new one + if (!isUpdate) + { + return false; + } + + proxyAccount = AgentProxyAccount.GetProxyAccount(this.proxyAccountName, this.DataContainer.Server.JobServer); + // Set the other properties + proxyAccount.CredentialName = proxyInfo.CredentialName; + proxyAccount.Description = proxyInfo.Description; + + UpdateProxyAccount(proxyAccount); + proxyAccount.Alter(); + + // Rename the proxy if needed + // This has to be done after Alter, in order to + // work correcly when scripting this action. + if (this.proxyAccountName != proxyInfo.AccountName) + { + proxyAccount.Rename(proxyInfo.AccountName); + } + } + } + else + { + return false; + } + + return true; + +#if false // @TODO - reenable subsystem code below + + // Update the subsystems + foreach (AgentSubSystem subsystem in this.addSubSystems) + { + proxyAccount.AddSubSystem(subsystem); + } + + foreach (AgentSubSystem subsystem in this.removeSubSystems) + { + proxyAccount.RemoveSubSystem(subsystem); + + // Update jobsteps that use this proxy accunt + // when some subsystems are removed from it + string reassignToProxyName = this.reassignToProxyNames[(int)subsystem]; + + if (reassignToProxyName != null) + { + // if version is sql 11 and above call SMO API to reassign proxy account + if (Utils.IsSql11OrLater(this.DataContainer.Server.ServerVersion)) + { + proxyAccount.Reassign(reassignToProxyName); + } + else + { + // legacy code + // Get a list of all job step objects that use this proxy and this subsystem + Request req = new Request(); + req.Urn = string.Format(System.Globalization.CultureInfo.InvariantCulture, + "Server/JobServer/Job/Step[@ProxyName=\'{0}\' and @SubSystem={1}]", + Urn.EscapeString(proxyAccount.Name), + (int)subsystem); + req.Fields = new string[] { "Name" }; + req.ParentPropertiesRequests = new PropertiesRequest[1] { new PropertiesRequest() }; + req.ParentPropertiesRequests[0].Fields = new string[] { "Name" }; + + Enumerator en = new Enumerator(); + DataTable table = en.Process(this.DataContainer.ServerConnection, req); + foreach (DataRow row in table.Rows) + { + // Get the actual job step object using urn + string urnString = string.Format(System.Globalization.CultureInfo.InvariantCulture, "Server/JobServer/Job[@Name=\"{0}\"/Step[@Name=\"{1}\"", row["Job_Name"], row["Name"]); + Urn urn = new Urn(urnString); + JobStep jobStep = (JobStep)this.DataContainer.Server.GetSmoObject(urn); + + jobStep.ProxyName = reassignToProxyName; + jobStep.Alter(); + } + } + } + } +#endif + } + + public bool Create() + { + CreateOrUpdateProxyAccount(this.proxyInfo, false); + return true; + } + + public bool Update() + { + CreateOrUpdateProxyAccount(this.proxyInfo, true); + return true; + } + + public bool Drop() + { + if (this.DataContainer.Server.JobServer.ProxyAccounts.Contains(this.proxyAccountName)) + { + // Try refresh and check again + this.DataContainer.Server.JobServer.ProxyAccounts.Refresh(); + if (this.DataContainer.Server.JobServer.ProxyAccounts.Contains(this.proxyAccountName)) + { + ProxyAccount proxyAccount = AgentProxyAccount.GetProxyAccount(this.proxyAccountName, this.DataContainer.Server.JobServer); + proxyAccount.DropIfExists(); + } + } + + return false; + } + + /// + /// Called to update the proxy object with properties + /// from this page. + /// + public void UpdateProxyAccount(ProxyAccount proxyAccount) + { + if (proxyAccount == null) + { + throw new ArgumentNullException("proxyAccount"); + } + + ArrayList principalsToAdd = new ArrayList(); + ArrayList principalsToRemove = new ArrayList(); + + // Process Sql Logins + if (ExtractPermissionsToAddAndRemove(this.proxyAccountName != null? proxyAccount.EnumLogins() : null, this.principals[(int) ProxyPrincipalType.SqlLogin], principalsToAdd, principalsToRemove)) + { + foreach (string principal in principalsToRemove) + { + proxyAccount.RemoveLogin(principal); + } + + foreach (string principal in principalsToAdd) + { + proxyAccount.AddLogin(principal); + } + } + + // Process Server Roles + if (ExtractPermissionsToAddAndRemove(this.proxyAccountName != null? proxyAccount.EnumServerRoles() : null, this.principals[(int) ProxyPrincipalType.ServerRole], principalsToAdd, principalsToRemove)) + { + foreach (string principal in principalsToRemove) + { + proxyAccount.RemoveServerRole(principal); + } + + foreach (string principal in principalsToAdd) + { + proxyAccount.AddServerRole(principal); + } + } + + // Process Msdb Roles + if (ExtractPermissionsToAddAndRemove(this.proxyAccountName != null? proxyAccount.EnumMsdbRoles() : null, this.principals[(int) ProxyPrincipalType.MsdbRole], principalsToAdd, principalsToRemove)) + { + foreach (string principal in principalsToRemove) + { + proxyAccount.RemoveMsdbRole(principal); + } + + foreach (string principal in principalsToAdd) + { + proxyAccount.AddMsdbRole(principal); + } + } + } + + + /// + /// This method scans two list of principals - an existing one extracted from ProxyAccount object + /// and a new one obtained from this panel and then it creates a two differential lists: one of + /// principals to add and the other of principals to remove. + /// + /// true if there are any changes between existingPermissions and newPermissions lists + private bool ExtractPermissionsToAddAndRemove(DataTable existingPermissions, ArrayList newPermissions, ArrayList principalsToAdd, ArrayList principalsToRemove) + { + // Reset both output lists + principalsToAdd.Clear(); + principalsToRemove.Clear(); + + // Sort both input lists + DataRow[] existingRows = existingPermissions != null? existingPermissions.Select(string.Empty, "Name DESC") : new DataRow[] {}; + newPermissions.Sort(); + + // Go through both lists at the same time and find differences + int existingPos = 0; + int newPos = 0; + + while (newPos < newPermissions.Count && existingPos < existingRows.Length) + { + int result = string.Compare(existingRows[existingPos]["Name"] as string, newPermissions[newPos] as string,StringComparison.Ordinal); + + if (result < 0) + { + // element in existingRows is lower then element in newPermissions + // mark element in existingRows for removal + principalsToRemove.Add(existingRows[existingPos]["Name"]); + ++existingPos; + } + else if (result > 0) + { + // element in existingRows is greater then element in newPermissions + // mark element in newPermissions for adding + principalsToAdd.Add(newPermissions[newPos]); + ++newPos; + } + else + { + // Both elements are equal. + // Advance to the next element + ++existingPos; + ++newPos; + } + } + + while (newPos < newPermissions.Count) + { + // Some elements are still left + // Copy them all to Add collection + principalsToAdd.Add(newPermissions[newPos++]); + } + + while (existingPos < existingRows.Length) + { + // Some elements are still left + // Copy them all to Remove collection + principalsToRemove.Add(existingRows[existingPos++]["Name"]); + } + + return(principalsToAdd.Count > 0 || principalsToRemove.Count > 0); + } + + private void RefreshData() + { + // List all the jobsteps that use current + // proxy account + Request req = new Request(); + req.Urn = string.Format(System.Globalization.CultureInfo.InvariantCulture, + "Server/JobServer/Job/Step[@ProxyName=\'{0}\']", + Urn.EscapeString(this.proxyAccountName)); + req.ResultType = ResultType.IDataReader; + req.Fields = new string[] {"Name", "SubSystem"}; + req.ParentPropertiesRequests = new PropertiesRequest[1]; + req.ParentPropertiesRequests[0] = new PropertiesRequest(new string[] {"Name"}); + + Enumerator en = new Enumerator(); + using (IDataReader reader = en.Process(this.DataContainer.ServerConnection, req).Data as IDataReader) + { + while (reader.Read()) + { + //JobStepSubSystems. + // @TODO - write to output collection + // new GridCell(reader.GetString(0)), // Job Name (parent property is first) + // new GridCell(reader.GetString(1)), // JobStep Name + // new GridCell(JobStepSubSystems.LookupFriendlyName((AgentSubSystem) reader.GetInt32(2))) // JobStep SubSystem + } + } + } + + #region Static methods + /// + /// Retrieves an instance of ProxyAccount from job server using name provided. + /// If proxy does not exist it throws an exception. + /// + /// Name of the proxy to get + /// Job server to get the proxy from + /// A valid proxy account. + internal static ProxyAccount GetProxyAccount(string proxyAccountName, JobServer jobServer) + { + if (proxyAccountName == null || proxyAccountName.Length == 0) + { + throw new ArgumentException("proxyAccountName"); + } + + if (jobServer == null) + { + throw new ArgumentNullException("jobServer"); + } + + ProxyAccount proxyAccount = jobServer.ProxyAccounts[proxyAccountName]; + if (proxyAccount == null) + { + // proxy not found. Try refreshing the collection + jobServer.ProxyAccounts.Refresh(); + proxyAccount = jobServer.ProxyAccounts[proxyAccountName]; + + // if still cannot get the proxy throw an exception + if (proxyAccount == null) + { + throw new ApplicationException("SRError.ProxyAccountNotFound(proxyAccountName)"); + } + } + return proxyAccount; + } + + /// + /// Retrieves a proxy account name from shared data containter. + /// + internal static void GetProxyAccountName(CDataContainer dataContainer, ref string proxyAccountName, ref bool duplicate) + { + STParameters parameters = new STParameters(); + parameters.SetDocument(dataContainer.Document); + + // Get proxy name + parameters.GetParam(AgentProxyAccount.ProxyAccountPropertyName, ref proxyAccountName); + if (proxyAccountName != null && proxyAccountName.Length == 0) + { + // Reset empty name back to null + proxyAccountName = null; + } + + // Get duplicate flag + string mode = string.Empty; + if (parameters.GetParam(AgentProxyAccount.ProxyAccountMode, ref mode) && + 0 == string.Compare(mode, AgentProxyAccount.ProxyAccountDuplicateMode, StringComparison.Ordinal)) + { + duplicate = true; + } + } + + /// + /// Uses enumerator to list names of all proxy accounts that use specified Agent SubSystem. + /// + /// Connection to use. + /// Requested SubSystem name + /// If set to true, 'sysadmin' account is added as a first entry in + /// the list of proxy accounts. + /// An array containing names of proxy accounts + internal static string[] ListProxyAccountsForSubsystem( + ServerConnection serverConnection, + string subsystemName, + bool includeSysadmin) + { + ArrayList proxyAccounts = new ArrayList(); + + // This method is valid only on Yukon and later + if (serverConnection.ServerVersion.Major >= 9) + { + if (includeSysadmin) + { + proxyAccounts.Add(AgentProxyAccount.SysadminAccount); + } + + // Get the list of proxy accounts + Request req = new Request(); + + req.Urn = string.Format(System.Globalization.CultureInfo.InvariantCulture, + "Server/JobServer/ProxyAccount/AgentSubSystem[@Name = \"{0}\"]", + Urn.EscapeString(subsystemName)); + req.ResultType = ResultType.IDataReader; + req.Fields = new string[] { "Name" }; + req.ParentPropertiesRequests = new PropertiesRequest[1] { new PropertiesRequest() }; + req.ParentPropertiesRequests[0].Fields = new string[] { "Name" }; + + Enumerator en = new Enumerator(); + + using (IDataReader reader = en.Process(serverConnection, req).Data as IDataReader) + { + while (reader.Read()) + { + proxyAccounts.Add(reader.GetString(0)); + } + } + } + + return (string[]) proxyAccounts.ToArray(typeof(string)); + } + #endregion + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/JobFetcher.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Jobs/JobFetcher.cs similarity index 74% rename from src/Microsoft.SqlTools.ServiceLayer/Agent/JobFetcher.cs rename to src/Microsoft.SqlTools.ServiceLayer/Agent/Jobs/JobFetcher.cs index 70b79234..0f3adfc0 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Agent/JobFetcher.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/Jobs/JobFetcher.cs @@ -403,153 +403,4 @@ namespace Microsoft.SqlTools.ServiceLayer.Agent return foundJobs; } } - - /// - /// a class for storing various properties of agent jobs, - /// used by the Job Activity Monitor - /// - public class JobProperties - { - private string name; - private int currentExecutionStatus; - private int lastRunOutcome; - private string currentExecutionStep; - private bool enabled; - private bool hasTarget; - private bool hasSchedule; - private bool hasStep; - private bool runnable; - private string category; - private int categoryID; - private int categoryType; - private DateTime lastRun; - private DateTime nextRun; - private Guid jobId; - - private JobProperties() - { - } - - public JobProperties(DataRow row) - { - System.Diagnostics.Debug.Assert(row["Name"] != DBNull.Value, "Name is null!"); - System.Diagnostics.Debug.Assert(row["IsEnabled"] != DBNull.Value, "IsEnabled is null!"); - System.Diagnostics.Debug.Assert(row["Category"] != DBNull.Value, "Category is null!"); - System.Diagnostics.Debug.Assert(row["CategoryID"] != DBNull.Value, "CategoryID is null!"); - System.Diagnostics.Debug.Assert(row["CategoryType"] != DBNull.Value, "CategoryType is null!"); - System.Diagnostics.Debug.Assert(row["CurrentRunStatus"] != DBNull.Value, "CurrentRunStatus is null!"); - System.Diagnostics.Debug.Assert(row["CurrentRunStep"] != DBNull.Value, "CurrentRunStep is null!"); - System.Diagnostics.Debug.Assert(row["HasSchedule"] != DBNull.Value, "HasSchedule is null!"); - System.Diagnostics.Debug.Assert(row["HasStep"] != DBNull.Value, "HasStep is null!"); - System.Diagnostics.Debug.Assert(row["HasServer"] != DBNull.Value, "HasServer is null!"); - System.Diagnostics.Debug.Assert(row["LastRunOutcome"] != DBNull.Value, "LastRunOutcome is null!"); - System.Diagnostics.Debug.Assert(row["JobID"] != DBNull.Value, "JobID is null!"); - - this.name = row["Name"].ToString(); - this.enabled = Convert.ToBoolean(row["IsEnabled"], CultureInfo.InvariantCulture); - this.category = row["Category"].ToString(); - this.categoryID = Convert.ToInt32(row["CategoryID"], CultureInfo.InvariantCulture); - this.categoryType = Convert.ToInt32(row["CategoryType"], CultureInfo.InvariantCulture); - this.currentExecutionStatus = Convert.ToInt32(row["CurrentRunStatus"], CultureInfo.InvariantCulture); - this.currentExecutionStep = row["CurrentRunStep"].ToString(); - this.hasSchedule = Convert.ToBoolean(row["HasSchedule"], CultureInfo.InvariantCulture); - this.hasStep = Convert.ToBoolean(row["HasStep"], CultureInfo.InvariantCulture); - this.hasTarget = Convert.ToBoolean(row["HasServer"], CultureInfo.InvariantCulture); - this.lastRunOutcome = Convert.ToInt32(row["LastRunOutcome"], CultureInfo.InvariantCulture); - this.jobId = Guid.Parse(row["JobID"].ToString()); ; - - // for a job to be runnable, it must: - // 1. have a target server - // 2. have some steps - this.runnable = this.hasTarget && this.hasStep; - - if (row["LastRunDate"] != DBNull.Value) - { - this.lastRun = Convert.ToDateTime(row["LastRunDate"], CultureInfo.InvariantCulture); - } - - if (row["NextRunDate"] != DBNull.Value) - { - this.nextRun = Convert.ToDateTime(row["NextRunDate"], CultureInfo.InvariantCulture); - } - } - - public bool Runnable - { - get{ return runnable;} - } - - public string Name - { - get{ return name;} - } - - public string Category - { - get{ return category;} - } - - public int CategoryID - { - get{ return categoryID;} - } - - public int CategoryType - { - get{ return categoryType;} - } - - public int LastRunOutcome - { - get{ return lastRunOutcome;} - } - - public int CurrentExecutionStatus - { - get{ return currentExecutionStatus;} - } - - public string CurrentExecutionStep - { - get{ return currentExecutionStep;} - } - - public bool Enabled - { - get{ return enabled;} - } - - public bool HasTarget - { - get{ return hasTarget;} - } - - public bool HasStep - { - get{ return hasStep;} - } - - public bool HasSchedule - { - get{ return hasSchedule;} - } - - public DateTime NextRun - { - get{ return nextRun;} - } - - public DateTime LastRun - { - get{ return lastRun;} - } - - public Guid JobID - { - get - { - return this.jobId; - } - } - } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/JobHelper.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Jobs/JobHelper.cs similarity index 100% rename from src/Microsoft.SqlTools.ServiceLayer/Agent/JobHelper.cs rename to src/Microsoft.SqlTools.ServiceLayer/Agent/Jobs/JobHelper.cs diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/JobHistoryItem.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Jobs/JobHistoryItem.cs similarity index 100% rename from src/Microsoft.SqlTools.ServiceLayer/Agent/JobHistoryItem.cs rename to src/Microsoft.SqlTools.ServiceLayer/Agent/Jobs/JobHistoryItem.cs diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/Jobs/JobProperties.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Jobs/JobProperties.cs new file mode 100644 index 00000000..d9ba727b --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Agent/Jobs/JobProperties.cs @@ -0,0 +1,167 @@ +// +// 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.Text; +using System.Data; +using System.Globalization; +using System.Collections.Generic; +using Microsoft.SqlServer.Management.Common; +using Microsoft.SqlServer.Management.Sdk.Sfc; +using Microsoft.SqlServer.Management.Smo.Agent; +using SMO = Microsoft.SqlServer.Management.Smo; +using Microsoft.SqlTools.ServiceLayer.Agent.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.Agent +{ + /// + /// a class for storing various properties of agent jobs, + /// used by the Job Activity Monitor + /// + public class JobProperties + { + private string name; + private int currentExecutionStatus; + private int lastRunOutcome; + private string currentExecutionStep; + private bool enabled; + private bool hasTarget; + private bool hasSchedule; + private bool hasStep; + private bool runnable; + private string category; + private int categoryID; + private int categoryType; + private DateTime lastRun; + private DateTime nextRun; + private Guid jobId; + + private JobProperties() + { + } + + public JobProperties(DataRow row) + { + System.Diagnostics.Debug.Assert(row["Name"] != DBNull.Value, "Name is null!"); + System.Diagnostics.Debug.Assert(row["IsEnabled"] != DBNull.Value, "IsEnabled is null!"); + System.Diagnostics.Debug.Assert(row["Category"] != DBNull.Value, "Category is null!"); + System.Diagnostics.Debug.Assert(row["CategoryID"] != DBNull.Value, "CategoryID is null!"); + System.Diagnostics.Debug.Assert(row["CategoryType"] != DBNull.Value, "CategoryType is null!"); + System.Diagnostics.Debug.Assert(row["CurrentRunStatus"] != DBNull.Value, "CurrentRunStatus is null!"); + System.Diagnostics.Debug.Assert(row["CurrentRunStep"] != DBNull.Value, "CurrentRunStep is null!"); + System.Diagnostics.Debug.Assert(row["HasSchedule"] != DBNull.Value, "HasSchedule is null!"); + System.Diagnostics.Debug.Assert(row["HasStep"] != DBNull.Value, "HasStep is null!"); + System.Diagnostics.Debug.Assert(row["HasServer"] != DBNull.Value, "HasServer is null!"); + System.Diagnostics.Debug.Assert(row["LastRunOutcome"] != DBNull.Value, "LastRunOutcome is null!"); + System.Diagnostics.Debug.Assert(row["JobID"] != DBNull.Value, "JobID is null!"); + + this.name = row["Name"].ToString(); + this.enabled = Convert.ToBoolean(row["IsEnabled"], CultureInfo.InvariantCulture); + this.category = row["Category"].ToString(); + this.categoryID = Convert.ToInt32(row["CategoryID"], CultureInfo.InvariantCulture); + this.categoryType = Convert.ToInt32(row["CategoryType"], CultureInfo.InvariantCulture); + this.currentExecutionStatus = Convert.ToInt32(row["CurrentRunStatus"], CultureInfo.InvariantCulture); + this.currentExecutionStep = row["CurrentRunStep"].ToString(); + this.hasSchedule = Convert.ToBoolean(row["HasSchedule"], CultureInfo.InvariantCulture); + this.hasStep = Convert.ToBoolean(row["HasStep"], CultureInfo.InvariantCulture); + this.hasTarget = Convert.ToBoolean(row["HasServer"], CultureInfo.InvariantCulture); + this.lastRunOutcome = Convert.ToInt32(row["LastRunOutcome"], CultureInfo.InvariantCulture); + this.jobId = Guid.Parse(row["JobID"].ToString()); ; + + // for a job to be runnable, it must: + // 1. have a target server + // 2. have some steps + this.runnable = this.hasTarget && this.hasStep; + + if (row["LastRunDate"] != DBNull.Value) + { + this.lastRun = Convert.ToDateTime(row["LastRunDate"], CultureInfo.InvariantCulture); + } + + if (row["NextRunDate"] != DBNull.Value) + { + this.nextRun = Convert.ToDateTime(row["NextRunDate"], CultureInfo.InvariantCulture); + } + } + + public bool Runnable + { + get{ return runnable;} + } + + public string Name + { + get{ return name;} + } + + public string Category + { + get{ return category;} + } + + public int CategoryID + { + get{ return categoryID;} + } + + public int CategoryType + { + get{ return categoryType;} + } + + public int LastRunOutcome + { + get{ return lastRunOutcome;} + } + + public int CurrentExecutionStatus + { + get{ return currentExecutionStatus;} + } + + public string CurrentExecutionStep + { + get{ return currentExecutionStep;} + } + + public bool Enabled + { + get{ return enabled;} + } + + public bool HasTarget + { + get{ return hasTarget;} + } + + public bool HasStep + { + get{ return hasStep;} + } + + public bool HasSchedule + { + get{ return hasSchedule;} + } + + public DateTime NextRun + { + get{ return nextRun;} + } + + public DateTime LastRun + { + get{ return lastRun;} + } + + public Guid JobID + { + get + { + return this.jobId; + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/Agent/JobUtilities.cs b/src/Microsoft.SqlTools.ServiceLayer/Agent/Jobs/JobUtilities.cs similarity index 100% rename from src/Microsoft.SqlTools.ServiceLayer/Agent/JobUtilities.cs rename to src/Microsoft.SqlTools.ServiceLayer/Agent/Jobs/JobUtilities.cs diff --git a/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs b/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs index f0aa83c8..790574ea 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs @@ -20,6 +20,7 @@ using Microsoft.SqlTools.ServiceLayer.Metadata; using Microsoft.SqlTools.ServiceLayer.Profiler; using Microsoft.SqlTools.ServiceLayer.QueryExecution; using Microsoft.SqlTools.ServiceLayer.Scripting; +using Microsoft.SqlTools.ServiceLayer.Security; using Microsoft.SqlTools.ServiceLayer.SqlContext; using Microsoft.SqlTools.ServiceLayer.Workspace; @@ -106,6 +107,9 @@ namespace Microsoft.SqlTools.ServiceLayer ProfilerService.Instance.InitializeService(serviceHost); serviceProvider.RegisterSingleService(ProfilerService.Instance); + SecurityService.Instance.InitializeService(serviceHost); + serviceProvider.RegisterSingleService(SecurityService.Instance); + InitializeHostedServices(serviceProvider, serviceHost); serviceHost.ServiceProvider = serviceProvider; diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs index e03d89d0..b23073c2 100755 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs @@ -3661,6 +3661,518 @@ namespace Microsoft.SqlTools.ServiceLayer } } + public static string UserCancelledSelectStep + { + get + { + return Keys.GetString(Keys.UserCancelledSelectStep); + } + } + + public static string RequestPostedToTargetServers + { + get + { + return Keys.GetString(Keys.RequestPostedToTargetServers); + } + } + + public static string Executing + { + get + { + return Keys.GetString(Keys.Executing); + } + } + + public static string BetweenRetries + { + get + { + return Keys.GetString(Keys.BetweenRetries); + } + } + + public static string Suspended + { + get + { + return Keys.GetString(Keys.Suspended); + } + } + + public static string PerformingCompletionAction + { + get + { + return Keys.GetString(Keys.PerformingCompletionAction); + } + } + + public static string WaitingForStepToFinish + { + get + { + return Keys.GetString(Keys.WaitingForStepToFinish); + } + } + + public static string WaitingForWorkerThread + { + get + { + return Keys.GetString(Keys.WaitingForWorkerThread); + } + } + + public static string AllDatabases + { + get + { + return Keys.GetString(Keys.AllDatabases); + } + } + + public static string Severity001 + { + get + { + return Keys.GetString(Keys.Severity001); + } + } + + public static string Severity002 + { + get + { + return Keys.GetString(Keys.Severity002); + } + } + + public static string Severity003 + { + get + { + return Keys.GetString(Keys.Severity003); + } + } + + public static string Severity004 + { + get + { + return Keys.GetString(Keys.Severity004); + } + } + + public static string Severity005 + { + get + { + return Keys.GetString(Keys.Severity005); + } + } + + public static string Severity006 + { + get + { + return Keys.GetString(Keys.Severity006); + } + } + + public static string Severity007 + { + get + { + return Keys.GetString(Keys.Severity007); + } + } + + public static string Severity008 + { + get + { + return Keys.GetString(Keys.Severity008); + } + } + + public static string Severity009 + { + get + { + return Keys.GetString(Keys.Severity009); + } + } + + public static string Severity010 + { + get + { + return Keys.GetString(Keys.Severity010); + } + } + + public static string Severity011 + { + get + { + return Keys.GetString(Keys.Severity011); + } + } + + public static string Severity012 + { + get + { + return Keys.GetString(Keys.Severity012); + } + } + + public static string Severity013 + { + get + { + return Keys.GetString(Keys.Severity013); + } + } + + public static string Severity014 + { + get + { + return Keys.GetString(Keys.Severity014); + } + } + + public static string Severity015 + { + get + { + return Keys.GetString(Keys.Severity015); + } + } + + public static string Severity016 + { + get + { + return Keys.GetString(Keys.Severity016); + } + } + + public static string Severity017 + { + get + { + return Keys.GetString(Keys.Severity017); + } + } + + public static string Severity018 + { + get + { + return Keys.GetString(Keys.Severity018); + } + } + + public static string Severity019 + { + get + { + return Keys.GetString(Keys.Severity019); + } + } + + public static string Severity020 + { + get + { + return Keys.GetString(Keys.Severity020); + } + } + + public static string Severity021 + { + get + { + return Keys.GetString(Keys.Severity021); + } + } + + public static string Severity022 + { + get + { + return Keys.GetString(Keys.Severity022); + } + } + + public static string Severity023 + { + get + { + return Keys.GetString(Keys.Severity023); + } + } + + public static string Severity024 + { + get + { + return Keys.GetString(Keys.Severity024); + } + } + + public static string Severity025 + { + get + { + return Keys.GetString(Keys.Severity025); + } + } + + public static string PagerScheduleMonFri + { + get + { + return Keys.GetString(Keys.PagerScheduleMonFri); + } + } + + public static string PagerScheduleSatSun + { + get + { + return Keys.GetString(Keys.PagerScheduleSatSun); + } + } + + public static string PagerScheduleWarning + { + get + { + return Keys.GetString(Keys.PagerScheduleWarning); + } + } + + public static string General + { + get + { + return Keys.GetString(Keys.General); + } + } + + public static string Notifications + { + get + { + return Keys.GetString(Keys.Notifications); + } + } + + public static string History + { + get + { + return Keys.GetString(Keys.History); + } + } + + public static string Day + { + get + { + return Keys.GetString(Keys.Day); + } + } + + public static string StartTime + { + get + { + return Keys.GetString(Keys.StartTime); + } + } + + public static string EndTime + { + get + { + return Keys.GetString(Keys.EndTime); + } + } + + public static string ColumnIndexIsInvalid + { + get + { + return Keys.GetString(Keys.ColumnIndexIsInvalid); + } + } + + public static string RowIndexIsInvalid + { + get + { + return Keys.GetString(Keys.RowIndexIsInvalid); + } + } + + public static string NewOperatorProperties + { + get + { + return Keys.GetString(Keys.NewOperatorProperties); + } + } + + public static string FailedToCreateInitializeAgentOperatorDialog + { + get + { + return Keys.GetString(Keys.FailedToCreateInitializeAgentOperatorDialog); + } + } + + public static string JobServerIsNotAvailable + { + get + { + return Keys.GetString(Keys.JobServerIsNotAvailable); + } + } + + public static string CannotCreateInitializeGeneralPage + { + get + { + return Keys.GetString(Keys.CannotCreateInitializeGeneralPage); + } + } + + public static string CannotCreateInitializeNotificationsPage + { + get + { + return Keys.GetString(Keys.CannotCreateInitializeNotificationsPage); + } + } + + public static string CannotCreateInitializeHistoryPage + { + get + { + return Keys.GetString(Keys.CannotCreateInitializeHistoryPage); + } + } + + public static string CannotResetOperator + { + get + { + return Keys.GetString(Keys.CannotResetOperator); + } + } + + public static string AlertList + { + get + { + return Keys.GetString(Keys.AlertList); + } + } + + public static string JobList + { + get + { + return Keys.GetString(Keys.JobList); + } + } + + public static string Email + { + get + { + return Keys.GetString(Keys.Email); + } + } + + public static string Pager + { + get + { + return Keys.GetString(Keys.Pager); + } + } + + public static string AlertName + { + get + { + return Keys.GetString(Keys.AlertName); + } + } + + public static string JobName + { + get + { + return Keys.GetString(Keys.JobName); + } + } + + public static string Always + { + get + { + return Keys.GetString(Keys.Always); + } + } + + public static string Never + { + get + { + return Keys.GetString(Keys.Never); + } + } + + public static string OnFailure + { + get + { + return Keys.GetString(Keys.OnFailure); + } + } + + public static string OnSuccess + { + get + { + return Keys.GetString(Keys.OnSuccess); + } + } + + public static string CannotModifyAlerts + { + get + { + return Keys.GetString(Keys.CannotModifyAlerts); + } + } + + public static string CannotCreateScriptForModifyAlerts + { + get + { + return Keys.GetString(Keys.CannotCreateScriptForModifyAlerts); + } + } + public static string ConnectionServiceListDbErrorNotConnected(string uri) { return Keys.GetString(Keys.ConnectionServiceListDbErrorNotConnected, uri); @@ -3751,6 +4263,151 @@ namespace Microsoft.SqlTools.ServiceLayer return Keys.GetString(Keys.EditDataIncorrectTable, tableName); } + public static string EnableAlertsTitle(String serverName) + { + return Keys.GetString(Keys.EnableAlertsTitle, serverName); + } + + public static string EnableAlertDescription(String alertName) + { + return Keys.GetString(Keys.EnableAlertDescription, alertName); + } + + public static string EnablingAlert(String alertName) + { + return Keys.GetString(Keys.EnablingAlert, alertName); + } + + public static string EnabledAlert(String alertName) + { + return Keys.GetString(Keys.EnabledAlert, alertName); + } + + public static string DisableAlertsTitle(String serverName) + { + return Keys.GetString(Keys.DisableAlertsTitle, serverName); + } + + public static string DisableAlertDescription(String alertName) + { + return Keys.GetString(Keys.DisableAlertDescription, alertName); + } + + public static string DisablingAlert(String alertName) + { + return Keys.GetString(Keys.DisablingAlert, alertName); + } + + public static string DisabledAlert(String alertName) + { + return Keys.GetString(Keys.DisabledAlert, alertName); + } + + public static string EnableJobsTitle(String serverName) + { + return Keys.GetString(Keys.EnableJobsTitle, serverName); + } + + public static string EnableJobDescription(String jobName) + { + return Keys.GetString(Keys.EnableJobDescription, jobName); + } + + public static string EnablingJob(String jobName) + { + return Keys.GetString(Keys.EnablingJob, jobName); + } + + public static string EnabledJob(String jobName) + { + return Keys.GetString(Keys.EnabledJob, jobName); + } + + public static string DisableJobsTitle(String serverName) + { + return Keys.GetString(Keys.DisableJobsTitle, serverName); + } + + public static string DisableJobDescription(String jobName) + { + return Keys.GetString(Keys.DisableJobDescription, jobName); + } + + public static string DisablingJob(String jobName) + { + return Keys.GetString(Keys.DisablingJob, jobName); + } + + public static string DisabledJob(String jobName) + { + return Keys.GetString(Keys.DisabledJob, jobName); + } + + public static string StartJobsTitle(String serverName) + { + return Keys.GetString(Keys.StartJobsTitle, serverName); + } + + public static string StartJobDescription(String jobName) + { + return Keys.GetString(Keys.StartJobDescription, jobName); + } + + public static string GettingStartStep(String jobName) + { + return Keys.GetString(Keys.GettingStartStep, jobName); + } + + public static string StartingJob(String jobName) + { + return Keys.GetString(Keys.StartingJob, jobName); + } + + public static string StartJobWithStep(String jobName, String stepName) + { + return Keys.GetString(Keys.StartJobWithStep, jobName, stepName); + } + + public static string ExecuteJob(string jobName) + { + return Keys.GetString(Keys.ExecuteJob, jobName); + } + + public static string JobFailed(string jobName) + { + return Keys.GetString(Keys.JobFailed, jobName); + } + + public static string StopJobsTitle(String serverName) + { + return Keys.GetString(Keys.StopJobsTitle, serverName); + } + + public static string StopJobDescription(String jobName) + { + return Keys.GetString(Keys.StopJobDescription, jobName); + } + + public static string StoppingJob(String jobName) + { + return Keys.GetString(Keys.StoppingJob, jobName); + } + + public static string StoppedJob(String jobName) + { + return Keys.GetString(Keys.StoppedJob, jobName); + } + + public static string UnknownSeverity(int severity) + { + return Keys.GetString(Keys.UnknownSeverity, severity); + } + + public static string OperatorProperties(string operatorName) + { + return Keys.GetString(Keys.OperatorProperties, operatorName); + } + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class Keys { @@ -5175,6 +5832,285 @@ namespace Microsoft.SqlTools.ServiceLayer public const string ProfilerConnectionNotFound = "ProfilerConnectionNotFound"; + public const string EnableAlertsTitle = "EnableAlertsTitle"; + + + public const string EnableAlertDescription = "EnableAlertDescription"; + + + public const string EnablingAlert = "EnablingAlert"; + + + public const string EnabledAlert = "EnabledAlert"; + + + public const string DisableAlertsTitle = "DisableAlertsTitle"; + + + public const string DisableAlertDescription = "DisableAlertDescription"; + + + public const string DisablingAlert = "DisablingAlert"; + + + public const string DisabledAlert = "DisabledAlert"; + + + public const string EnableJobsTitle = "EnableJobsTitle"; + + + public const string EnableJobDescription = "EnableJobDescription"; + + + public const string EnablingJob = "EnablingJob"; + + + public const string EnabledJob = "EnabledJob"; + + + public const string DisableJobsTitle = "DisableJobsTitle"; + + + public const string DisableJobDescription = "DisableJobDescription"; + + + public const string DisablingJob = "DisablingJob"; + + + public const string DisabledJob = "DisabledJob"; + + + public const string StartJobsTitle = "StartJobsTitle"; + + + public const string StartJobDescription = "StartJobDescription"; + + + public const string GettingStartStep = "GettingStartStep"; + + + public const string UserCancelledSelectStep = "UserCancelledSelectStep"; + + + public const string StartingJob = "StartingJob"; + + + public const string StartJobWithStep = "StartJobWithStep"; + + + public const string RequestPostedToTargetServers = "RequestPostedToTargetServers"; + + + public const string ExecuteJob = "ExecuteJob"; + + + public const string JobFailed = "JobFailed"; + + + public const string Executing = "Executing"; + + + public const string BetweenRetries = "BetweenRetries"; + + + public const string Suspended = "Suspended"; + + + public const string PerformingCompletionAction = "PerformingCompletionAction"; + + + public const string WaitingForStepToFinish = "WaitingForStepToFinish"; + + + public const string WaitingForWorkerThread = "WaitingForWorkerThread"; + + + public const string StopJobsTitle = "StopJobsTitle"; + + + public const string StopJobDescription = "StopJobDescription"; + + + public const string StoppingJob = "StoppingJob"; + + + public const string StoppedJob = "StoppedJob"; + + + public const string AllDatabases = "AllDatabases"; + + + public const string UnknownSeverity = "UnknownSeverity"; + + + public const string Severity001 = "Severity001"; + + + public const string Severity002 = "Severity002"; + + + public const string Severity003 = "Severity003"; + + + public const string Severity004 = "Severity004"; + + + public const string Severity005 = "Severity005"; + + + public const string Severity006 = "Severity006"; + + + public const string Severity007 = "Severity007"; + + + public const string Severity008 = "Severity008"; + + + public const string Severity009 = "Severity009"; + + + public const string Severity010 = "Severity010"; + + + public const string Severity011 = "Severity011"; + + + public const string Severity012 = "Severity012"; + + + public const string Severity013 = "Severity013"; + + + public const string Severity014 = "Severity014"; + + + public const string Severity015 = "Severity015"; + + + public const string Severity016 = "Severity016"; + + + public const string Severity017 = "Severity017"; + + + public const string Severity018 = "Severity018"; + + + public const string Severity019 = "Severity019"; + + + public const string Severity020 = "Severity020"; + + + public const string Severity021 = "Severity021"; + + + public const string Severity022 = "Severity022"; + + + public const string Severity023 = "Severity023"; + + + public const string Severity024 = "Severity024"; + + + public const string Severity025 = "Severity025"; + + + public const string PagerScheduleMonFri = "PagerScheduleMonFri"; + + + public const string PagerScheduleSatSun = "PagerScheduleSatSun"; + + + public const string PagerScheduleWarning = "PagerScheduleWarning"; + + + public const string General = "General"; + + + public const string Notifications = "Notifications"; + + + public const string History = "History"; + + + public const string Day = "Day"; + + + public const string StartTime = "StartTime"; + + + public const string EndTime = "EndTime"; + + + public const string ColumnIndexIsInvalid = "ColumnIndexIsInvalid"; + + + public const string RowIndexIsInvalid = "RowIndexIsInvalid"; + + + public const string NewOperatorProperties = "NewOperatorProperties"; + + + public const string OperatorProperties = "OperatorProperties"; + + + public const string FailedToCreateInitializeAgentOperatorDialog = "FailedToCreateInitializeAgentOperatorDialog"; + + + public const string JobServerIsNotAvailable = "JobServerIsNotAvailable"; + + + public const string CannotCreateInitializeGeneralPage = "CannotCreateInitializeGeneralPage"; + + + public const string CannotCreateInitializeNotificationsPage = "CannotCreateInitializeNotificationsPage"; + + + public const string CannotCreateInitializeHistoryPage = "CannotCreateInitializeHistoryPage"; + + + public const string CannotResetOperator = "CannotResetOperator"; + + + public const string AlertList = "AlertList"; + + + public const string JobList = "JobList"; + + + public const string Email = "Email"; + + + public const string Pager = "Pager"; + + + public const string AlertName = "AlertName"; + + + public const string JobName = "JobName"; + + + public const string Always = "Always"; + + + public const string Never = "Never"; + + + public const string OnFailure = "OnFailure"; + + + public const string OnSuccess = "OnSuccess"; + + + public const string CannotModifyAlerts = "CannotModifyAlerts"; + + + public const string CannotCreateScriptForModifyAlerts = "CannotCreateScriptForModifyAlerts"; + + private Keys() { } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx index cbae3db6..84a5652b 100755 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx @@ -2023,4 +2023,405 @@ Connection not found + + Enable Alerts - {0} + . + Parameters: 0 - serverName (String) + + + Enable Alert '{0}' + . + Parameters: 0 - alertName (String) + + + Enabling Alert '{0}' + . + Parameters: 0 - alertName (String) + + + Enabled Alert '{0}' + . + Parameters: 0 - alertName (String) + + + Disable Alerts - {0} + . + Parameters: 0 - serverName (String) + + + Disable Alert '{0}' + . + Parameters: 0 - alertName (String) + + + Disabling Alert '{0}' + . + Parameters: 0 - alertName (String) + + + Disabled Alert '{0}' + . + Parameters: 0 - alertName (String) + + + Enable Jobs - {0} + . + Parameters: 0 - serverName (String) + + + Enable Job '{0}' + . + Parameters: 0 - jobName (String) + + + Enabling Job '{0}' + . + Parameters: 0 - jobName (String) + + + Enabled Job '{0}' + . + Parameters: 0 - jobName (String) + + + Disable Jobs - {0} + . + Parameters: 0 - serverName (String) + + + Disable Job '{0}' + . + Parameters: 0 - jobName (String) + + + Disabling Job '{0}' + . + Parameters: 0 - jobName (String) + + + Disabled Job '{0}' + . + Parameters: 0 - jobName (String) + + + Start Jobs - {0} + . + Parameters: 0 - serverName (String) + + + Start Job '{0}' + . + Parameters: 0 - jobName (String) + + + Job '{0}' has more than one step. Getting step to start + . + Parameters: 0 - jobName (String) + + + User canceled select step. Job will not be started + + + + Starting Job '{0}' + . + Parameters: 0 - jobName (String) + + + Start Job '{0}' with step '{1}' + . + Parameters: 0 - jobName (String), 1 - stepName (String) + + + Posted remote job execution request + + + + Execute job '{0}' + . + Parameters: 0 - jobName (string) + + + Execution of job '{0}' failed. See the history log for details. + . + Parameters: 0 - jobName (string) + + + Executing + + + + Between retries + + + + Suspended + + + + Performing completion action + + + + Waiting for step to finish + + + + Waiting for worker thread + + + + Stop Jobs - {0} + . + Parameters: 0 - serverName (String) + + + Stop Job '{0}' + . + Parameters: 0 - jobName (String) + + + Stopping Job '{0}' + . + Parameters: 0 - jobName (String) + + + Stopped Job '{0}' + . + Parameters: 0 - jobName (String) + + + <all databases> + First item in database name drop down list + + + Unknown severity: {0} + Exception thrown when agent alert has unknown severity level. + Parameters: 0 - severity (int) + + + 001 - Miscellaneous System Information + + + + 002 - Reserved + + + + 003 - Reserved + + + + 004 - Reserved + + + + 005 - Reserved + + + + 006 - Reserved + + + + 007 - Notification: Status Information + + + + 008 - Notification: User Intervention Required + + + + 009 - User Defined + + + + 010 - Information + + + + 011 - Specified Database Object Not Found + + + + 012 - Unused + + + + 013 - User Transaction Syntax Error + + + + 014 - Insufficient Permission + + + + 015 - Syntax Error in SQL Statements + + + + 016 - Miscellaneous User Error + + + + 017 - Insufficient Resources + + + + 018 - Nonfatal Internal Error + + + + 019 - Fatal Error in Resource + + + + 020 - Fatal Error in Current Process + + + + 021 - Fatal Error in Database Processes + + + + 022 - Fatal Error: Table Integrity Suspect + + + + 023 - Fatal Error: Database Integrity Suspect + + + + 024 - Fatal Error: Hardware Error + + + + 025 - Fatal Error + + + + Pager schedule end time is earlier than start time on Mon-Fri. Do you want to continue? + Message box that displayed if start time is more than end time on Mon-Fri + + + Pager schedule end time is earlier than start time on Sat-Sun. Do you want to continue? + Message box that displayed if start time is more than end time on Sat-Sun + + + Pager schedule warning + Message box caption + + + General + Tree node name + + + Notifications + Tree node name + + + History + Tree node name + + + Day + Pager schedule grid column name + + + Start Time + Pager schedule grid column name + + + End Time + Pager schedule grid column name + + + Column index is invalid. + Exception thrown when column index is invalid + + + Row index is invalid. + Exception thrown when row index is invalid + + + New Operator + Name of the operator dialog in create new operator mode + + + {0} Properties + Name of the operator dialog in modify operator mode. + Parameters: 0 - operatorName (string) + + + Unable to create/initialize Agent Operator dialog. + Exception thrown when dialog cannot be created/intialized. + + + Job server is not available. + Exception thrown when job server is not available + + + Cannot create/initialize General page. + Exception thrown when we cannot create/initialize agent operators general page + + + Cannot create/initialize Notifications page. + Exception thrown when we cannot create/initialize agent operators notifications page + + + Cannot create/initialize History page. + Exception thrown when we cannot create/initialize agent operators history page + + + Cannot reset operator. + Exception throw when dialog cannot refresh operator + + + Alert list: + Name of label on notifications page + + + Job list: + Name of label on notifications page + + + E-mail + Name of column on notifications page + + + Pager + Name of column on notifications page + + + Alert name + Name of column on notifications page + + + Job name + Name of column on notifications page + + + Always + Completion Action + + + Never + Completion Action + + + On failure + Completion Action + + + On success + Completion Action + + + Cannot modify alerts. + Exception thrown when we cannot modify alerts + + + Cannot create script for modify alerts. + Exception thrown when we cannot create script that modify alerts + diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings index 1c14741b..d4a4cd27 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings @@ -887,3 +887,158 @@ InvalidPathError = Cannot access the specified path on the server: {0} ############################################################################ # Profiler ProfilerConnectionNotFound = Connection not found + + + + + + + + + + + + + +############################################################################# +# SQL Agent +EnableAlertsTitle(String serverName) = Enable Alerts - {0} +EnableAlertDescription(String alertName) = Enable Alert '{0}' +EnablingAlert(String alertName) = Enabling Alert '{0}' +EnabledAlert(String alertName) = Enabled Alert '{0}' + +DisableAlertsTitle(String serverName) = Disable Alerts - {0} +DisableAlertDescription(String alertName) = Disable Alert '{0}' +DisablingAlert(String alertName) = Disabling Alert '{0}' +DisabledAlert(String alertName) = Disabled Alert '{0}' + +EnableJobsTitle(String serverName) = Enable Jobs - {0} +EnableJobDescription(String jobName) = Enable Job '{0}' +EnablingJob(String jobName) = Enabling Job '{0}' +EnabledJob(String jobName) = Enabled Job '{0}' + +DisableJobsTitle(String serverName) = Disable Jobs - {0} +DisableJobDescription(String jobName) = Disable Job '{0}' +DisablingJob(String jobName) = Disabling Job '{0}' +DisabledJob(String jobName) = Disabled Job '{0}' + +StartJobsTitle(String serverName) = Start Jobs - {0} +StartJobDescription(String jobName) = Start Job '{0}' +GettingStartStep(String jobName) = Job '{0}' has more than one step. Getting step to start +UserCancelledSelectStep = User canceled select step. Job will not be started +StartingJob(String jobName) = Starting Job '{0}' +StartJobWithStep(String jobName, String stepName) = Start Job '{0}' with step '{1}' +RequestPostedToTargetServers = Posted remote job execution request + +ExecuteJob(string jobName) = Execute job '{0}' +JobFailed(string jobName) = Execution of job '{0}' failed. See the history log for details. +Executing = Executing +BetweenRetries = Between retries +Suspended = Suspended +PerformingCompletionAction = Performing completion action +WaitingForStepToFinish = Waiting for step to finish +WaitingForWorkerThread = Waiting for worker thread + +StopJobsTitle(String serverName) = Stop Jobs - {0} +StopJobDescription(String jobName) = Stop Job '{0}' +StoppingJob(String jobName) = Stopping Job '{0}' +StoppedJob(String jobName) = Stopped Job '{0}' + +; First item in database name drop down list +AllDatabases = +; Exception thrown when agent alert has unknown severity level +UnknownSeverity(int severity) = Unknown severity: {0} + +#severity types +Severity001 = 001 - Miscellaneous System Information +Severity002 = 002 - Reserved +Severity003 = 003 - Reserved +Severity004 = 004 - Reserved +Severity005 = 005 - Reserved +Severity006 = 006 - Reserved +Severity007 = 007 - Notification: Status Information +Severity008 = 008 - Notification: User Intervention Required +Severity009 = 009 - User Defined +Severity010 = 010 - Information +Severity011 = 011 - Specified Database Object Not Found +Severity012 = 012 - Unused +Severity013 = 013 - User Transaction Syntax Error +Severity014 = 014 - Insufficient Permission +Severity015 = 015 - Syntax Error in SQL Statements +Severity016 = 016 - Miscellaneous User Error +Severity017 = 017 - Insufficient Resources +Severity018 = 018 - Nonfatal Internal Error +Severity019 = 019 - Fatal Error in Resource +Severity020 = 020 - Fatal Error in Current Process +Severity021 = 021 - Fatal Error in Database Processes +Severity022 = 022 - Fatal Error: Table Integrity Suspect +Severity023 = 023 - Fatal Error: Database Integrity Suspect +Severity024 = 024 - Fatal Error: Hardware Error +Severity025 = 025 - Fatal Error + +; Message box that displayed if start time is more than end time on Mon-Fri +PagerScheduleMonFri = Pager schedule end time is earlier than start time on Mon-Fri. Do you want to continue? +; Message box that displayed if start time is more than end time on Sat-Sun +PagerScheduleSatSun = Pager schedule end time is earlier than start time on Sat-Sun. Do you want to continue? +; Message box caption +PagerScheduleWarning = Pager schedule warning + +; Tree node name +General = General +; Tree node name +Notifications = Notifications +; Tree node name +History = History +; Pager schedule grid column name +Day = Day +; Pager schedule grid column name +StartTime = Start Time +; Pager schedule grid column name +EndTime = End Time +; Exception thrown when column index is invalid +ColumnIndexIsInvalid = Column index is invalid. +; Exception thrown when row index is invalid +RowIndexIsInvalid = Row index is invalid. +; Name of the operator dialog in create new operator mode +NewOperatorProperties = New Operator +; Name of the operator dialog in modify operator mode +OperatorProperties(string operatorName) = {0} Properties +; Exception thrown when dialog cannot be created/intialized. +FailedToCreateInitializeAgentOperatorDialog = Unable to create/initialize Agent Operator dialog. +; Exception thrown when job server is not available +JobServerIsNotAvailable = Job server is not available. +; Exception thrown when we cannot create/initialize agent operators general page +CannotCreateInitializeGeneralPage = Cannot create/initialize General page. +; Exception thrown when we cannot create/initialize agent operators notifications page +CannotCreateInitializeNotificationsPage = Cannot create/initialize Notifications page. +; Exception thrown when we cannot create/initialize agent operators history page +CannotCreateInitializeHistoryPage = Cannot create/initialize History page. +; Exception throw when dialog cannot refresh operator +CannotResetOperator = Cannot reset operator. + + +# +; Name of label on notifications page +AlertList = Alert list: +; Name of label on notifications page +JobList = Job list: +; Name of column on notifications page +Email = E-mail +; Name of column on notifications page +Pager = Pager +; Name of column on notifications page +AlertName = Alert name +; Name of column on notifications page +JobName = Job name +; Completion Action +Always = Always +; Completion Action +Never = Never +; Completion Action +OnFailure = On failure +; Completion Action +OnSuccess = On success +; Exception thrown when we cannot modify alerts +CannotModifyAlerts = Cannot modify alerts. +; Exception thrown when we cannot create script that modify alerts +CannotCreateScriptForModifyAlerts = Cannot create script for modify alerts. diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf index 4070e2df..664ab869 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf @@ -2379,6 +2379,500 @@ . Parameters: 0 - tableName (string) + + Enable Alerts - {0} + Enable Alerts - {0} + . + Parameters: 0 - serverName (String) + + + Enable Alert '{0}' + Enable Alert '{0}' + . + Parameters: 0 - alertName (String) + + + Enabling Alert '{0}' + Enabling Alert '{0}' + . + Parameters: 0 - alertName (String) + + + Enabled Alert '{0}' + Enabled Alert '{0}' + . + Parameters: 0 - alertName (String) + + + Disable Alerts - {0} + Disable Alerts - {0} + . + Parameters: 0 - serverName (String) + + + Disable Alert '{0}' + Disable Alert '{0}' + . + Parameters: 0 - alertName (String) + + + Disabling Alert '{0}' + Disabling Alert '{0}' + . + Parameters: 0 - alertName (String) + + + Disabled Alert '{0}' + Disabled Alert '{0}' + . + Parameters: 0 - alertName (String) + + + Enable Jobs - {0} + Enable Jobs - {0} + . + Parameters: 0 - serverName (String) + + + Enable Job '{0}' + Enable Job '{0}' + . + Parameters: 0 - jobName (String) + + + Enabling Job '{0}' + Enabling Job '{0}' + . + Parameters: 0 - jobName (String) + + + Enabled Job '{0}' + Enabled Job '{0}' + . + Parameters: 0 - jobName (String) + + + Disable Jobs - {0} + Disable Jobs - {0} + . + Parameters: 0 - serverName (String) + + + Disable Job '{0}' + Disable Job '{0}' + . + Parameters: 0 - jobName (String) + + + Disabling Job '{0}' + Disabling Job '{0}' + . + Parameters: 0 - jobName (String) + + + Disabled Job '{0}' + Disabled Job '{0}' + . + Parameters: 0 - jobName (String) + + + Start Jobs - {0} + Start Jobs - {0} + . + Parameters: 0 - serverName (String) + + + Start Job '{0}' + Start Job '{0}' + . + Parameters: 0 - jobName (String) + + + Job '{0}' has more than one step. Getting step to start + Job '{0}' has more than one step. Getting step to start + . + Parameters: 0 - jobName (String) + + + User canceled select step. Job will not be started + User canceled select step. Job will not be started + + + + Starting Job '{0}' + Starting Job '{0}' + . + Parameters: 0 - jobName (String) + + + Start Job '{0}' with step '{1}' + Start Job '{0}' with step '{1}' + . + Parameters: 0 - jobName (String), 1 - stepName (String) + + + Posted remote job execution request + Posted remote job execution request + + + + Execute job '{0}' + Execute job '{0}' + . + Parameters: 0 - jobName (string) + + + Execution of job '{0}' failed. See the history log for details. + Execution of job '{0}' failed. See the history log for details. + . + Parameters: 0 - jobName (string) + + + Executing + Executing + + + + Between retries + Between retries + + + + Suspended + Suspended + + + + Performing completion action + Performing completion action + + + + Waiting for step to finish + Waiting for step to finish + + + + Waiting for worker thread + Waiting for worker thread + + + + Stop Jobs - {0} + Stop Jobs - {0} + . + Parameters: 0 - serverName (String) + + + Stop Job '{0}' + Stop Job '{0}' + . + Parameters: 0 - jobName (String) + + + Stopping Job '{0}' + Stopping Job '{0}' + . + Parameters: 0 - jobName (String) + + + Stopped Job '{0}' + Stopped Job '{0}' + . + Parameters: 0 - jobName (String) + + + <all databases> + <all databases> + First item in database name drop down list + + + Unknown severity: {0} + Unknown severity: {0} + Exception thrown when agent alert has unknown severity level. + Parameters: 0 - severity (int) + + + 001 - Miscellaneous System Information + 001 - Miscellaneous System Information + + + + 002 - Reserved + 002 - Reserved + + + + 003 - Reserved + 003 - Reserved + + + + 004 - Reserved + 004 - Reserved + + + + 005 - Reserved + 005 - Reserved + + + + 006 - Reserved + 006 - Reserved + + + + 007 - Notification: Status Information + 007 - Notification: Status Information + + + + 008 - Notification: User Intervention Required + 008 - Notification: User Intervention Required + + + + 009 - User Defined + 009 - User Defined + + + + 010 - Information + 010 - Information + + + + 011 - Specified Database Object Not Found + 011 - Specified Database Object Not Found + + + + 012 - Unused + 012 - Unused + + + + 013 - User Transaction Syntax Error + 013 - User Transaction Syntax Error + + + + 014 - Insufficient Permission + 014 - Insufficient Permission + + + + 015 - Syntax Error in SQL Statements + 015 - Syntax Error in SQL Statements + + + + 016 - Miscellaneous User Error + 016 - Miscellaneous User Error + + + + 017 - Insufficient Resources + 017 - Insufficient Resources + + + + 018 - Nonfatal Internal Error + 018 - Nonfatal Internal Error + + + + 019 - Fatal Error in Resource + 019 - Fatal Error in Resource + + + + 020 - Fatal Error in Current Process + 020 - Fatal Error in Current Process + + + + 021 - Fatal Error in Database Processes + 021 - Fatal Error in Database Processes + + + + 022 - Fatal Error: Table Integrity Suspect + 022 - Fatal Error: Table Integrity Suspect + + + + 023 - Fatal Error: Database Integrity Suspect + 023 - Fatal Error: Database Integrity Suspect + + + + 024 - Fatal Error: Hardware Error + 024 - Fatal Error: Hardware Error + + + + 025 - Fatal Error + 025 - Fatal Error + + + + Pager schedule end time is earlier than start time on Mon-Fri. Do you want to continue? + Pager schedule end time is earlier than start time on Mon-Fri. Do you want to continue? + Message box that displayed if start time is more than end time on Mon-Fri + + + Pager schedule end time is earlier than start time on Sat-Sun. Do you want to continue? + Pager schedule end time is earlier than start time on Sat-Sun. Do you want to continue? + Message box that displayed if start time is more than end time on Sat-Sun + + + Pager schedule warning + Pager schedule warning + Message box caption + + + General + General + Tree node name + + + Notifications + Notifications + Tree node name + + + History + History + Tree node name + + + Day + Day + Pager schedule grid column name + + + Start Time + Start Time + Pager schedule grid column name + + + End Time + End Time + Pager schedule grid column name + + + Column index is invalid. + Column index is invalid. + Exception thrown when column index is invalid + + + Row index is invalid. + Row index is invalid. + Exception thrown when row index is invalid + + + New Operator + New Operator + Name of the operator dialog in create new operator mode + + + {0} Properties + {0} Properties + Name of the operator dialog in modify operator mode. + Parameters: 0 - operatorName (string) + + + Unable to create/initialize Agent Operator dialog. + Unable to create/initialize Agent Operator dialog. + Exception thrown when dialog cannot be created/intialized. + + + Job server is not available. + Job server is not available. + Exception thrown when job server is not available + + + Cannot create/initialize General page. + Cannot create/initialize General page. + Exception thrown when we cannot create/initialize agent operators general page + + + Cannot create/initialize Notifications page. + Cannot create/initialize Notifications page. + Exception thrown when we cannot create/initialize agent operators notifications page + + + Cannot create/initialize History page. + Cannot create/initialize History page. + Exception thrown when we cannot create/initialize agent operators history page + + + Cannot reset operator. + Cannot reset operator. + Exception throw when dialog cannot refresh operator + + + Alert list: + Alert list: + Name of label on notifications page + + + Job list: + Job list: + Name of label on notifications page + + + E-mail + E-mail + Name of column on notifications page + + + Pager + Pager + Name of column on notifications page + + + Alert name + Alert name + Name of column on notifications page + + + Job name + Job name + Name of column on notifications page + + + Always + Always + Completion Action + + + Never + Never + Completion Action + + + On failure + On failure + Completion Action + + + On success + On success + Completion Action + + + Cannot modify alerts. + Cannot modify alerts. + Exception thrown when we cannot modify alerts + + + Cannot create script for modify alerts. + Cannot create script for modify alerts. + Exception thrown when we cannot create script that modify alerts + \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/CredentialInfo.cs b/src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/CredentialInfo.cs new file mode 100644 index 00000000..2fbd29d4 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/CredentialInfo.cs @@ -0,0 +1,23 @@ +// +// 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 Microsoft.SqlTools.ServiceLayer.Agent; + +namespace Microsoft.SqlTools.ServiceLayer.Security.Contracts +{ + /// + /// a class for storing various credential properties + /// + public class CredentialInfo + { + public string Identity { get; set; } + public int Id { get; } + public DateTime DateLastModified { get; } + public DateTime CreateDate { get; } + public string ProviderName { get; set; } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/CredentialRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/CredentialRequest.cs new file mode 100644 index 00000000..abbf4b36 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/CredentialRequest.cs @@ -0,0 +1,45 @@ +// +// 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; +using Microsoft.SqlTools.ServiceLayer.Utility; +using Microsoft.SqlTools.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.Security.Contracts +{ + /// + /// Create Credential parameters + /// + public class CreateCredentialParams : GeneralRequestDetails + { + public string OwnerUri { get; set; } + + public CredentialInfo Credential { get; set; } + } + + /// + /// Create Credential result + /// + public class CreateCredentialResult + { + + public bool Succeeded { get; set; } + + public string ErrorMessage { get; set; } + } + + /// + /// Create Credential request type + /// + public class CreateCredentialRequest + { + /// + /// Request definition + /// + public static readonly + RequestType Type = + RequestType.Create("security/createcredential"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Security/Credential.cs b/src/Microsoft.SqlTools.ServiceLayer/Security/Credential.cs new file mode 100644 index 00000000..6bee400d --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Security/Credential.cs @@ -0,0 +1,114 @@ +// +// 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; +using System.ComponentModel; +using System.Data; +using System.Data.SqlClient; +using System.Xml; +using Microsoft.SqlServer.Management.Common; +using Microsoft.SqlServer.Management.Sdk.Sfc; +using Microsoft.SqlServer.Management.Smo; +using Microsoft.SqlServer.Management.Diagnostics; +using Microsoft.SqlTools.ServiceLayer.Management; +using Microsoft.SqlTools.ServiceLayer.Admin; + +namespace Microsoft.SqlTools.ServiceLayer.Security +{ + internal class Credential : ManagementActionBase + { + +// #region Trace support +// private const string componentName = "Credential"; + +// public string ComponentName +// { +// get +// { +// return componentName; +// } +// } +// #endregion + +#region Constants + private const int MAX_SQL_SYS_NAME_LENGTH = 128; // max sql sys name length + private const string PASSWORD_MASK_STRING = "**********"; +#endregion + +#region Variables + private CredentialData credentialData = null; +#endregion + +#region Constructors / Dispose + /// + /// required when loading from Object Explorer context + /// + /// + public Credential(CDataContainer context) + { + this.DataContainer = context; + this.credentialData = new CredentialData(context); + this.credentialData.Initialize(); + } + + /// + /// Clean up any resources being used. + /// + protected override void Dispose( bool disposing ) + { + base.Dispose( disposing ); + if (disposing == true) + { + if (this.credentialData != null) + { + this.credentialData.Dispose(); + } + } + } +#endregion + +#region Overrides SqlManagementUserControl + /// + /// called on background thread by the framework to execute the action + /// + /// + public void OnRunNow(object sender) + { + this.credentialData.SendDataToServer(); + + } + +#endregion + + /// + /// update logic layer based on content of user interface + /// + private void UpdateLogicLayer() + { + + this.credentialData.CredentialName = "this.textBoxCredentialName.Text"; + this.credentialData.CredentialIdentity = "this.textBoxIdentity.Text"; + + + this.credentialData.SecurePassword = AdminService.BuildSecureStringFromPassword("password"); + this.credentialData.SecurePasswordConfirm = AdminService.BuildSecureStringFromPassword("password"); + + if (this.ServerConnection.ServerVersion.Major >= 10) + { + // need to update only during create time + this.credentialData.IsEncryptionByProvider = true; //this.checkBoxUseProvider.Checked; + if (this.credentialData.IsEncryptionByProvider) + { + this.credentialData.ProviderName = "this.comboBoxProviderName.SelectedItem.ToString()"; + } + else + { + this.credentialData.ProviderName = string.Empty; + } + } + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Security/CredentialData.cs b/src/Microsoft.SqlTools.ServiceLayer/Security/CredentialData.cs new file mode 100644 index 00000000..3b2a9225 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Security/CredentialData.cs @@ -0,0 +1,256 @@ + +// +// 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.Data.SqlClient; +using System.Security; +using System.Text; +using Microsoft.SqlServer.Management.Common; +using Microsoft.SqlServer.Management.Diagnostics; +using Microsoft.SqlServer.Management.Sdk.Sfc; +using Microsoft.SqlServer.Management.Smo; +using Microsoft.SqlTools.ServiceLayer.Admin; + +namespace Microsoft.SqlTools.ServiceLayer.Security +{ + internal class CredentialException : Exception + { + public CredentialException(string message) + : base(message) + { + } + } + + internal class CredentialData : IDisposable + { + #region Properties + private string credentialName = string.Empty; + public string CredentialName + { + get { return credentialName; } + set { credentialName = value; } + } + + private string credentialIdentity = string.Empty; + public string CredentialIdentity + { + get { return credentialIdentity; } + set { credentialIdentity = value; } + } + + private SecureString securePassword; + public SecureString SecurePassword + { + get { return securePassword; } + set + { + securePassword = value; + PasswordWasChanged = true; + } + } + + private SecureString securePasswordConfirm; + public SecureString SecurePasswordConfirm + { + get { return securePasswordConfirm; } + set + { + securePasswordConfirm = value; + PasswordWasChanged = true; + } + } + + private bool isPropertiesMode = false; + public bool IsPropertiesMode + { + get + { + return isPropertiesMode; + } + } + + private bool passwordWasChanged = false; + public bool PasswordWasChanged + { + get { return passwordWasChanged; } + set { passwordWasChanged = value; } + } + + private bool isEncryptionByProvider = false; + public bool IsEncryptionByProvider + { + get { return isEncryptionByProvider; } + set { isEncryptionByProvider = value; } + } + + private string providerName = string.Empty; + public string ProviderName + { + get { return providerName; } + set { providerName = value; } + } + #endregion + + private const string ENUMERATOR_FIELD_IDENTITY = "Identity"; + private const string ENUMERATOR_FIELD_PROVIDER_NAME = "ProviderName"; + + #region Constructor + private CDataContainer context = null; + private CDataContainer Context { get { return context; } set { context = value; } } + public CredentialData(CDataContainer ctxt) + { + this.Context = ctxt; + LoadDataFromXmlContext(); + } + #endregion + + #region Implementation: LoadDataFromXmlContext(), LoadDataFromServer(), SendDataToServer() + + public void Initialize() + { + LoadDataFromXmlContext(); + LoadDataFromServer(); + } + + /// + /// LoadDataFromXmlContext + /// + /// loads context information from xml - e.g. name of object + /// + private void LoadDataFromXmlContext() + { + this.CredentialName = this.Context.GetDocumentPropertyString("credential"); + this.isPropertiesMode = (this.CredentialName != null) && (this.CredentialName.Length > 0); + } + + /// + /// LoadDataFromServer + /// + /// talks with enumerator an retrieves info that is not available in the xml context but on server + /// + private void LoadDataFromServer() + { + if (this.IsPropertiesMode == true) + { + bool isKatmaiAndNotMatrix = (this.context.Server.Version.Major >= 10); + + Urn urn = new Urn("Server/Credential[@Name='" + Urn.EscapeString(this.CredentialName) + "']"); + string [] fields; + if (isKatmaiAndNotMatrix) + { + fields = new string[] { ENUMERATOR_FIELD_IDENTITY, ENUMERATOR_FIELD_PROVIDER_NAME }; + } + else + { + fields = new string[] { ENUMERATOR_FIELD_IDENTITY }; + } + Request r = new Request(urn, fields); + System.Data.DataTable dataTable = Enumerator.GetData(this.Context.ConnectionInfo, r); + + if (dataTable.Rows.Count == 0) + { + throw new Exception("SRError.CredentialNoLongerExists"); + } + + System.Data.DataRow dataRow = dataTable.Rows[0]; + this.CredentialIdentity = Convert.ToString(dataRow[ENUMERATOR_FIELD_IDENTITY], System.Globalization.CultureInfo.InvariantCulture); + + if (isKatmaiAndNotMatrix) + { + this.providerName = Convert.ToString(dataRow[ENUMERATOR_FIELD_PROVIDER_NAME], System.Globalization.CultureInfo.InvariantCulture); + this.isEncryptionByProvider = !string.IsNullOrEmpty(providerName); + } + } + else + { + this.CredentialName = string.Empty; + this.CredentialIdentity = string.Empty; + this.providerName = string.Empty; + this.isEncryptionByProvider = false; + } + + this.SecurePassword = new SecureString(); + this.SecurePasswordConfirm = new SecureString(); + + this.PasswordWasChanged = false; + } + + + /// + /// SendDataToServer + /// + /// here we talk with server via smo and do the actual data changing + /// + public void SendDataToServer() + { + if (this.IsPropertiesMode == true) + { + SendToServerAlterCredential(); + } + else + { + SendToServerCreateCredential(); + } + } + + /// + /// create credential - create mode + /// + private void SendToServerCreateCredential() + { + Microsoft.SqlServer.Management.Smo.Credential smoCredential = new Microsoft.SqlServer.Management.Smo.Credential ( + this.Context.Server, + this.CredentialName); + if (this.isEncryptionByProvider) + { + smoCredential.MappedClassType = MappedClassType.CryptographicProvider; + smoCredential.ProviderName = this.providerName; + } + smoCredential.Create(this.CredentialIdentity, this.SecurePassword.ToString()); + GC.Collect(); // this.SecurePassword.ToString() just created an immutable string that lives in memory + } + + /// + /// alter credential - properties mode + /// + private void SendToServerAlterCredential() + { + Microsoft.SqlServer.Management.Smo.Credential smoCredential = this.Context.Server.Credentials[this.CredentialName]; + + if (smoCredential != null) + { + if (this.PasswordWasChanged == false) + { + if (smoCredential.Identity != this.CredentialIdentity) + { + smoCredential.Alter(this.CredentialIdentity); + } + } + else + { + smoCredential.Alter(this.CredentialIdentity, this.SecurePassword.ToString()); + GC.Collect(); // this.SecurePassword.ToString() just created an immutable string that lives in memory + } + } + else + { + throw new Exception("SRError.CredentialNoLongerExists"); + } + } + #endregion + + #region IDisposable Members + + public void Dispose() + { + this.SecurePassword.Dispose(); + this.SecurePasswordConfirm.Dispose(); + } + + #endregion + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Security/SecurityService.cs b/src/Microsoft.SqlTools.ServiceLayer/Security/SecurityService.cs new file mode 100644 index 00000000..c28c8cc0 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Security/SecurityService.cs @@ -0,0 +1,116 @@ +// +// 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.Threading.Tasks; +using Microsoft.SqlTools.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.Admin; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.Hosting; +using Microsoft.SqlTools.ServiceLayer.Security.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.Security +{ + /// + /// Main class for Security Service functionality + /// + public sealed class SecurityService : IDisposable + { + private bool disposed; + + private ConnectionService connectionService = null; + + private static readonly Lazy instance = new Lazy(() => new SecurityService()); + + /// + /// Construct a new SecurityService instance with default parameters + /// + public SecurityService() + { + } + + /// + /// Gets the singleton instance object + /// + public static SecurityService Instance + { + get { return instance.Value; } + } + + /// + /// Internal for testing purposes only + /// + internal ConnectionService ConnectionServiceInstance + { + get + { + if (connectionService == null) + { + connectionService = ConnectionService.Instance; + } + return connectionService; + } + + set + { + connectionService = value; + } + } + + /// + /// Service host object for sending/receiving requests/events. + /// Internal for testing purposes. + /// + internal IProtocolEndpoint ServiceHost + { + get; + set; + } + + /// + /// Initializes the Security Service instance + /// + public void InitializeService(ServiceHost serviceHost) + { + this.ServiceHost = serviceHost; + this.ServiceHost.SetRequestHandler(CreateCredentialRequest.Type, HandleCreateCredentialRequest); + } + + /// + /// Handle request to create a credential + /// + internal async Task HandleCreateCredentialRequest(CreateCredentialParams parameters, RequestContext requestContext) + { + try + { + var result = new CreateCredentialResult(); + ConnectionInfo connInfo; + ConnectionServiceInstance.TryFindConnection( + parameters.OwnerUri, + out connInfo); + + CDataContainer dataContainer = AdminService.CreateDataContainer(connInfo, databaseExists: true); + Credential credential = new Credential(dataContainer); + + await requestContext.SendResult(result); + } + catch (Exception e) + { + await requestContext.SendError(e); + } + } + + /// + /// Disposes the service + /// + public void Dispose() + { + if (!disposed) + { + disposed = true; + } + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Utility/ReturnResult.cs b/src/Microsoft.SqlTools.ServiceLayer/Utility/ReturnResult.cs new file mode 100644 index 00000000..0e589313 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Utility/ReturnResult.cs @@ -0,0 +1,15 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + + +namespace Microsoft.SqlTools.ServiceLayer.Utility +{ + public class ReturnResult + { + public bool Succeeded { get; set; } + + public string ErrorMessage { get; set; } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Agent/AgentAlertTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Agent/AgentAlertTests.cs new file mode 100644 index 00000000..8b77d0ce --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Agent/AgentAlertTests.cs @@ -0,0 +1,145 @@ +// +// 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.Data.SqlClient; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SqlServer.Management.Smo; +using Microsoft.SqlServer.Management.XEvent; +using Microsoft.SqlTools.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.Agent; +using Microsoft.SqlTools.ServiceLayer.Agent.Contracts; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility; +using Microsoft.SqlTools.ServiceLayer.Profiler; +using Microsoft.SqlTools.ServiceLayer.Profiler.Contracts; +using Microsoft.SqlTools.ServiceLayer.Test.Common; +using Moq; +using Xunit; + +namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Agent +{ + public class AgentAlertTests + { + /// + /// Verify default agent/alerts handlers + /// + [Fact] + public async Task TestHandleAgentAlertsRequest() + { + using (SelfCleaningTempFile queryTempFile = new SelfCleaningTempFile()) + { + var connectionResult = await LiveConnectionHelper.InitLiveConnectionInfoAsync("master", queryTempFile.FilePath); + + var requestParams = new AgentAlertsParams() + { + OwnerUri = connectionResult.ConnectionInfo.OwnerUri + }; + + var requestContext = new Mock>(); + + AgentService service = new AgentService(); + await service.HandleAgentAlertsRequest(requestParams, requestContext.Object); + } + + } + + /// + /// Verify the default "create agent alert" request handler with valid parameters + /// + [Fact] + public async Task TestHandleCreateAgentAlertsRequest() + { + using (SelfCleaningTempFile queryTempFile = new SelfCleaningTempFile()) + { + var connectionResult = await LiveConnectionHelper.InitLiveConnectionInfoAsync("master", queryTempFile.FilePath); + + var requestParams = new CreateAgentAlertParams + { + OwnerUri = connectionResult.ConnectionInfo.OwnerUri, + Alert = new AgentAlertInfo() + { + JobName = "Test Job Name" + } + }; + + var requestContext = new Mock>(); + + AgentService service = new AgentService(); + await service.HandleCreateAgentAlertRequest(requestParams, requestContext.Object); + + // var agentAlerts = await AgentTestUtils.GetAgentAlerts(connectionResult.ConnectionInfo.OwnerUri); + // if (agentAlerts != null && agentAlerts.Length > 0) + // { + // foreach (var agentAlert in agentAlerts) + // { + // if (agentAlert.JobName.Equals(alert.JobName)) + // { + // await service.HandleDeleteAgentAlertRequest(new DeleteAgentAlertParams() + // { + // OwnerUri = connectionResult.ConnectionInfo.OwnerUri, + // Alert = alert + // }, deleteContext.Object); + // } + // } + // } + } + } + + /// + /// Verify the default "update agent alert" request handler with valid parameters + /// + [Fact] + public async Task TestHandleUpdateAgentAlertsRequest() + { + using (SelfCleaningTempFile queryTempFile = new SelfCleaningTempFile()) + { + var createContext = new Mock>(); + var updateContext = new Mock>(); + var deleteContext = new Mock>(); + + var service = new AgentService(); + var alert = new AgentAlertInfo() + { + JobName = "test_update_job", + AlertType = AlertType.SqlServerEvent, + Severity = 1 + }; + + var connectionResult = await LiveConnectionHelper.InitLiveConnectionInfoAsync("master", queryTempFile.FilePath); + await service.HandleDeleteAgentAlertRequest(new DeleteAgentAlertParams() + { + OwnerUri = connectionResult.ConnectionInfo.OwnerUri, + Alert = alert + }, deleteContext.Object); + + await service.HandleCreateAgentAlertRequest(new CreateAgentAlertParams + { + OwnerUri = connectionResult.ConnectionInfo.OwnerUri, + Alert = alert + }, createContext.Object); + + await service.HandleUpdateAgentAlertRequest(new UpdateAgentAlertParams() + { + OwnerUri = connectionResult.ConnectionInfo.OwnerUri, + Alert = alert + }, updateContext.Object); + + await service.HandleDeleteAgentAlertRequest(new DeleteAgentAlertParams() + { + OwnerUri = connectionResult.ConnectionInfo.OwnerUri, + Alert = alert + }, deleteContext.Object); + + createContext.VerifyAll(); + updateContext.VerifyAll(); + deleteContext.VerifyAll(); + } + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Agent/AgentOperatorTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Agent/AgentOperatorTests.cs new file mode 100644 index 00000000..9b974802 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Agent/AgentOperatorTests.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Threading.Tasks; +using Microsoft.SqlTools.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.Agent; +using Microsoft.SqlTools.ServiceLayer.Agent.Contracts; +using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility; +using Microsoft.SqlTools.ServiceLayer.Test.Common; +using Moq; +using Xunit; + +namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Agent +{ + public class AgentOperatorTests + { + + /// + /// Verify the default "update agent alert" request handler with valid parameters + /// + [Fact] + public async Task TestHandleUpdateAgentOperatorRequest() + { + using (SelfCleaningTempFile queryTempFile = new SelfCleaningTempFile()) + { + var createContext = new Mock>(); + + var service = new AgentService(); + var operatorInfo = new AgentOperatorInfo() + { + Id = 10, + Name = "Joe DBA", + EmailAddress = "test@aol.com" + }; + + var connectionResult = await LiveConnectionHelper.InitLiveConnectionInfoAsync("master", queryTempFile.FilePath); + + await service.HandleCreateAgentOperatorRequest(new CreateAgentOperatorParams + { + OwnerUri = connectionResult.ConnectionInfo.OwnerUri, + Operator = operatorInfo + }, createContext.Object); + + createContext.VerifyAll(); + } + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Agent/AgentProxyTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Agent/AgentProxyTests.cs new file mode 100644 index 00000000..fdea1e3b --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Agent/AgentProxyTests.cs @@ -0,0 +1,79 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Threading.Tasks; +using Microsoft.SqlTools.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.Agent; +using Microsoft.SqlTools.ServiceLayer.Agent.Contracts; +using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility; +using Microsoft.SqlTools.ServiceLayer.Test.Common; +using Moq; +using Xunit; + +namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Agent +{ + public class AgentProxyTests + { + /// + /// Verify the default "update agent alert" request handler with valid parameters + /// + [Fact] + public async Task TestHandleUpdateAgentProxyRequest() + { + using (SelfCleaningTempFile queryTempFile = new SelfCleaningTempFile()) + { + var createContext = new Mock>(); + var updateContext = new Mock>(); + var deleteContext = new Mock>(); + + var service = new AgentService(); + var proxy = new AgentProxyInfo() + { + Id = 10, + AccountName = "Test Proxy 2", + CredentialName = "User", + Description = "", + IsEnabled = true + }; + + var connectionResult = await LiveConnectionHelper.InitLiveConnectionInfoAsync("master", queryTempFile.FilePath); + await service.HandleDeleteAgentProxyRequest(new DeleteAgentProxyParams() + { + OwnerUri = connectionResult.ConnectionInfo.OwnerUri, + Proxy = proxy + }, deleteContext.Object); + + deleteContext.VerifyAll(); + + await service.HandleCreateAgentProxyRequest(new CreateAgentProxyParams + { + OwnerUri = connectionResult.ConnectionInfo.OwnerUri, + Proxy = proxy + }, createContext.Object); + + createContext.VerifyAll(); + + string originalProxyName = proxy.AccountName; + proxy.AccountName = proxy.AccountName + " Updated"; + await service.HandleUpdateAgentProxyRequest(new UpdateAgentProxyParams() + { + OwnerUri = connectionResult.ConnectionInfo.OwnerUri, + OriginalProxyName = originalProxyName, + Proxy = proxy + }, updateContext.Object); + + updateContext.VerifyAll(); + + await service.HandleDeleteAgentProxyRequest(new DeleteAgentProxyParams() + { + OwnerUri = connectionResult.ConnectionInfo.OwnerUri, + Proxy = proxy + }, deleteContext.Object); + + deleteContext.VerifyAll(); + } + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Agent/AgentServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Agent/AgentServiceTests.cs index 12fc236e..29062b1c 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Agent/AgentServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Agent/AgentServiceTests.cs @@ -46,7 +46,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Agent AgentService service = new AgentService(); await service.HandleAgentJobsRequest(requestParams, requestContext.Object); requestContext.VerifyAll(); - } + } } /// diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Agent/AgentTestUtils.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Agent/AgentTestUtils.cs new file mode 100644 index 00000000..18a094f9 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Agent/AgentTestUtils.cs @@ -0,0 +1,34 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Threading.Tasks; +using Microsoft.SqlTools.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.Agent; +using Microsoft.SqlTools.ServiceLayer.Agent.Contracts; +using Moq; + +namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Agent +{ + public static class AgentTestUtils + { + internal static async Task GetAgentAlerts(string connectionUri) + { + var requestParams = new AgentAlertsParams() + { + OwnerUri = connectionUri + }; + + var requestContext = new Mock>(); + + AgentAlertInfo[] agentAlerts = null; + requestContext.Setup(x => x.SendResult(It.IsAny())) + .Callback(r => agentAlerts = r.Alerts); + + AgentService service = new AgentService(); + await service.HandleAgentAlertsRequest(requestParams, requestContext.Object); + return agentAlerts; + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Security/SecurityServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Security/SecurityServiceTests.cs new file mode 100644 index 00000000..500f575b --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Security/SecurityServiceTests.cs @@ -0,0 +1,54 @@ +// +// 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; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility; +using Microsoft.SqlTools.ServiceLayer.Security; +using Microsoft.SqlTools.ServiceLayer.Security.Contracts; +using Microsoft.SqlTools.ServiceLayer.Test.Common; +using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; +using Moq; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using static Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility.LiveConnectionHelper; + +namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Security +{ + /// + /// Tests for the security service component + /// + public class SecuritygServiceTests + { + /// + /// Verify the script object request + /// + [Fact] + public async Task TestHandleCreateCredentialRequest() + { + using (SelfCleaningTempFile queryTempFile = new SelfCleaningTempFile()) + { + var createContext = new Mock>(); + var connectionResult = await LiveConnectionHelper.InitLiveConnectionInfoAsync("master", queryTempFile.FilePath); + + var service = new SecurityService(); + var credential = new CredentialInfo() + { + }; + + await service.HandleCreateCredentialRequest(new CreateCredentialParams + { + OwnerUri = connectionResult.ConnectionInfo.OwnerUri, + Credential = credential + }, createContext.Object); + + createContext.VerifyAll(); + } + } + } +}