mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-01-26 01:25:42 -05:00
* job step type and error message details change * added newline for both environments * checked for inner exceptions * sorted imports
1404 lines
43 KiB
C#
1404 lines
43 KiB
C#
//
|
|
// 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.Data;
|
|
using System.Globalization;
|
|
using Microsoft.SqlServer.Management.Common;
|
|
using Microsoft.SqlServer.Management.Sdk.Sfc;
|
|
using Microsoft.SqlServer.Management.Smo;
|
|
using Microsoft.SqlServer.Management.Smo.Agent;
|
|
using Microsoft.SqlTools.ServiceLayer.Agent.Contracts;
|
|
using Microsoft.SqlTools.ServiceLayer.Management;
|
|
using SMO = Microsoft.SqlServer.Management.Smo;
|
|
|
|
namespace Microsoft.SqlTools.ServiceLayer.Agent
|
|
{
|
|
[Flags]
|
|
internal enum UserRoles
|
|
{
|
|
NotSet = -1, None = 0, AgentUser = 1, AgentReader = 2, AgentOperator = 4, SysAdmin = 8
|
|
};
|
|
|
|
/// <summary>
|
|
/// Summary description for JobPropertiesPrototype.
|
|
/// </summary>
|
|
internal class JobData
|
|
{
|
|
#region constants
|
|
/// <summary>
|
|
/// Mode the dialog has been launched
|
|
/// </summary>
|
|
internal enum ActionMode { Create, Edit, Unknown };
|
|
/// <summary>
|
|
/// If the job has been pushed from an MSX then it's category ID will always be 1
|
|
/// </summary>
|
|
public readonly int JobFromMsxId = 1;
|
|
#endregion
|
|
|
|
#region fields
|
|
/*
|
|
* fields that are always populated
|
|
*/
|
|
/// <summary>
|
|
/// Urn of the Job object we were launched against. This will be null if we are creating a new job.
|
|
/// </summary>
|
|
private Urn urn = null;
|
|
/// <summary>
|
|
/// Datacontainer that represents everything we need to know about the server.
|
|
/// </summary>
|
|
private CDataContainer context;
|
|
/// <summary>
|
|
/// Mode we are working in.
|
|
/// </summary>
|
|
private ActionMode mode;
|
|
|
|
/*
|
|
* fields that map to the SMO Job object
|
|
*/
|
|
|
|
/// <summary>
|
|
/// original name of the job
|
|
/// </summary>
|
|
private string originalName = null;
|
|
|
|
/// <summary>
|
|
/// Job GUID stored as string
|
|
/// </summary>
|
|
private string jobIdString = null;
|
|
|
|
#region general page
|
|
bool generalInfoLoaded = false;
|
|
/// <summary>
|
|
/// current name of the job
|
|
/// </summary>
|
|
private string currentName = null;
|
|
/// <summary>
|
|
/// job owner
|
|
/// </summary>
|
|
private string owner = null;
|
|
/// <summary>
|
|
/// category for this job
|
|
/// </summary>
|
|
private LocalizableCategory category = null;
|
|
/// <summary>
|
|
/// job description
|
|
/// </summary>
|
|
private string description = null;
|
|
/// <summary>
|
|
/// is the job enabled
|
|
/// </summary>
|
|
private bool enabled;
|
|
/// <summary>
|
|
/// source server for this job
|
|
/// </summary>
|
|
private string source = null;
|
|
/// <summary>
|
|
/// originating server
|
|
/// </summary>
|
|
private string originatingServer = null;
|
|
/// <summary>
|
|
/// Date and time this job was created
|
|
/// </summary>
|
|
private DateTime created;
|
|
/// <summary>
|
|
/// when was the job last modified
|
|
/// </summary>
|
|
private DateTime lastModified;
|
|
/// <summary>
|
|
/// when was the job last executed
|
|
/// </summary>
|
|
private DateTime lastExecution;
|
|
#endregion
|
|
|
|
#region notifications
|
|
bool notificationsLoaded = false;
|
|
/// <summary>
|
|
/// operator that will be emailed
|
|
/// </summary>
|
|
private string operatorToEmail = null;
|
|
/// <summary>
|
|
/// when the operator will be emailed
|
|
/// </summary>
|
|
private CompletionAction emailLevel;
|
|
/// <summary>
|
|
/// operator will be paged
|
|
/// </summary>
|
|
private string operatorToPage = null;
|
|
/// <summary>
|
|
/// when they will be paged
|
|
/// </summary>
|
|
private CompletionAction pageLevel;
|
|
/// <summary>
|
|
/// when will an entry be written to the event log
|
|
/// </summary>
|
|
private CompletionAction eventLogLevel;
|
|
/// <summary>
|
|
/// when will the job be deleted
|
|
/// </summary>
|
|
private CompletionAction deleteLevel;
|
|
#endregion
|
|
|
|
// msx / tsx information
|
|
private bool msaInformationLoaded = false;
|
|
private bool targetLocalServer = true;
|
|
private bool originallyTargetLocalServer = true;
|
|
private MsaJobTargetServer[] targetServers;
|
|
|
|
// cached server information
|
|
private UserRoles userRole = UserRoles.NotSet;
|
|
private string trueLogin = null;
|
|
private string[] owners = null;
|
|
private LocalizableCategory[] smoCategories = null;
|
|
private LocalizableCategory[] displayableCategories = null;
|
|
private string[] operators = null;
|
|
private Version version = null;
|
|
|
|
// cached proxy objects
|
|
private JobStepsData jobSteps = null;
|
|
private JobSchedulesData jobSchedules = null;
|
|
private JobAlertsData jobAlerts = null;
|
|
|
|
// other information
|
|
private string script = null;
|
|
private string scriptName = null;
|
|
private int startStepID = -1;
|
|
|
|
#endregion
|
|
|
|
#region public properties
|
|
public Version Version
|
|
{
|
|
get
|
|
{
|
|
if (this.version == null)
|
|
{
|
|
LoadVersion();
|
|
}
|
|
return this.version;
|
|
}
|
|
}
|
|
public UserRoles UserRole
|
|
{
|
|
get
|
|
{
|
|
if (this.userRole == UserRoles.NotSet)
|
|
{
|
|
LoadUserRoles();
|
|
}
|
|
return this.userRole;
|
|
}
|
|
}
|
|
public CDataContainer OriginalContext
|
|
{
|
|
get
|
|
{
|
|
return this.context;
|
|
}
|
|
}
|
|
public ActionMode Mode
|
|
{
|
|
get
|
|
{
|
|
return this.mode;
|
|
}
|
|
}
|
|
public string Name
|
|
{
|
|
get
|
|
{
|
|
CheckAndLoadGeneralData();
|
|
return this.currentName;
|
|
}
|
|
set
|
|
{
|
|
CheckAndLoadGeneralData();
|
|
this.currentName = value.Trim();
|
|
}
|
|
}
|
|
public string Owner
|
|
{
|
|
get
|
|
{
|
|
CheckAndLoadOwner();
|
|
return this.owner;
|
|
}
|
|
set
|
|
{
|
|
this.owner = value;
|
|
}
|
|
}
|
|
|
|
public string[] Owners
|
|
{
|
|
get
|
|
{
|
|
if (this.owners == null)
|
|
{
|
|
LoadLogins();
|
|
}
|
|
return this.owners;
|
|
}
|
|
}
|
|
|
|
public LocalizableCategory Category
|
|
{
|
|
get
|
|
{
|
|
CheckAndLoadCategory();
|
|
return this.category;
|
|
}
|
|
set
|
|
{
|
|
this.category = value;
|
|
}
|
|
}
|
|
|
|
public LocalizableCategory[] Categories
|
|
{
|
|
get
|
|
{
|
|
CheckAndLoadDisplayableCategories();
|
|
return this.displayableCategories;
|
|
}
|
|
}
|
|
|
|
private LocalizableCategory[] SmoCategories
|
|
{
|
|
get
|
|
{
|
|
CheckAndLoadSmoCategories();
|
|
return this.smoCategories;
|
|
}
|
|
}
|
|
|
|
public string Description
|
|
{
|
|
get
|
|
{
|
|
CheckAndLoadGeneralData();
|
|
return this.description;
|
|
}
|
|
set
|
|
{
|
|
CheckAndLoadGeneralData();
|
|
this.description = value;
|
|
}
|
|
}
|
|
public bool Enabled
|
|
{
|
|
get
|
|
{
|
|
CheckAndLoadGeneralData();
|
|
return this.enabled;
|
|
}
|
|
set
|
|
{
|
|
CheckAndLoadGeneralData();
|
|
this.enabled = value;
|
|
}
|
|
}
|
|
public string Source
|
|
{
|
|
get
|
|
{
|
|
CheckAndLoadGeneralData();
|
|
return this.source;
|
|
}
|
|
set
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
}
|
|
public DateTime DateCreated
|
|
{
|
|
get
|
|
{
|
|
CheckAndLoadGeneralData();
|
|
return this.created;
|
|
}
|
|
}
|
|
|
|
public DateTime DateLastModified
|
|
{
|
|
get
|
|
{
|
|
CheckAndLoadGeneralData();
|
|
return this.lastModified;
|
|
}
|
|
}
|
|
public DateTime LastRunDate
|
|
{
|
|
get
|
|
{
|
|
CheckAndLoadGeneralData();
|
|
return this.lastExecution;
|
|
}
|
|
}
|
|
public string OperatorToEmail
|
|
{
|
|
get
|
|
{
|
|
CheckAndLoadNotifications();
|
|
return this.operatorToEmail;
|
|
}
|
|
set
|
|
{
|
|
CheckAndLoadNotifications();
|
|
this.operatorToEmail = value;
|
|
}
|
|
}
|
|
public CompletionAction EmailLevel
|
|
{
|
|
get
|
|
{
|
|
CheckAndLoadNotifications();
|
|
return this.emailLevel;
|
|
}
|
|
set
|
|
{
|
|
CheckAndLoadNotifications();
|
|
this.emailLevel = value;
|
|
}
|
|
}
|
|
public string OperatorToPage
|
|
{
|
|
get
|
|
{
|
|
CheckAndLoadNotifications();
|
|
return this.operatorToPage;
|
|
}
|
|
set
|
|
{
|
|
CheckAndLoadNotifications();
|
|
this.operatorToPage = value;
|
|
}
|
|
}
|
|
public CompletionAction PageLevel
|
|
{
|
|
get
|
|
{
|
|
CheckAndLoadNotifications();
|
|
return this.pageLevel;
|
|
}
|
|
set
|
|
{
|
|
CheckAndLoadNotifications();
|
|
this.pageLevel = value;
|
|
}
|
|
}
|
|
|
|
public CompletionAction EventLogLevel
|
|
{
|
|
get
|
|
{
|
|
CheckAndLoadNotifications();
|
|
return this.eventLogLevel;
|
|
}
|
|
set
|
|
{
|
|
CheckAndLoadNotifications();
|
|
this.eventLogLevel = value;
|
|
}
|
|
}
|
|
public CompletionAction DeleteLevel
|
|
{
|
|
get
|
|
{
|
|
CheckAndLoadNotifications();
|
|
return this.deleteLevel;
|
|
}
|
|
set
|
|
{
|
|
CheckAndLoadNotifications();
|
|
this.deleteLevel = value;
|
|
}
|
|
}
|
|
|
|
public string[] Operators
|
|
{
|
|
get
|
|
{
|
|
if (this.operators == null)
|
|
{
|
|
LoadOperators();
|
|
}
|
|
return this.operators;
|
|
}
|
|
}
|
|
public JobStepsData JobSteps
|
|
{
|
|
get
|
|
{
|
|
CheckAndLoadJobSteps();
|
|
return this.jobSteps;
|
|
}
|
|
}
|
|
public JobSchedulesData JobSchedules
|
|
{
|
|
get
|
|
{
|
|
CheckAndLoadJobSchedules();
|
|
return this.jobSchedules;
|
|
}
|
|
}
|
|
public JobAlertsData JobAlerts
|
|
{
|
|
get
|
|
{
|
|
CheckAndLoadJobAlerts();
|
|
return this.jobAlerts;
|
|
}
|
|
}
|
|
public bool IsMsx
|
|
{
|
|
get
|
|
{
|
|
return this.AvailableTargetServers.Length > 0;
|
|
}
|
|
}
|
|
public MsaJobTargetServer[] AvailableTargetServers
|
|
{
|
|
get
|
|
{
|
|
if (this.targetServers == null)
|
|
{
|
|
LoadTargetServers();
|
|
}
|
|
return this.targetServers;
|
|
}
|
|
}
|
|
public bool IsLocalJob
|
|
{
|
|
get
|
|
{
|
|
return this.targetLocalServer;
|
|
}
|
|
}
|
|
public bool IsRemotelyOriginated
|
|
{
|
|
get
|
|
{
|
|
return (this.category == null) ? false : this.Category.SmoCategory.ID == JobFromMsxId;
|
|
}
|
|
}
|
|
public string OriginatingServer
|
|
{
|
|
get
|
|
{
|
|
return this.originatingServer;
|
|
}
|
|
}
|
|
public bool IsUserAgentAdmin
|
|
{
|
|
get
|
|
{
|
|
return (UserRole & UserRoles.SysAdmin) > 0;
|
|
}
|
|
}
|
|
public bool OriginallyTargetLocalServer
|
|
{
|
|
get
|
|
{
|
|
CheckAndLoadMsaInformation();
|
|
return this.originallyTargetLocalServer;
|
|
}
|
|
set
|
|
{
|
|
CheckAndLoadMsaInformation();
|
|
this.originallyTargetLocalServer = value;
|
|
}
|
|
}
|
|
public bool TargetLocalServer
|
|
{
|
|
get
|
|
{
|
|
CheckAndLoadMsaInformation();
|
|
return this.targetLocalServer;
|
|
}
|
|
set
|
|
{
|
|
CheckAndLoadMsaInformation();
|
|
// If a change in the targetLocalServer was detected, then fire the OnCategoriesChanged
|
|
// event so that the categories drop down list is properly populated.
|
|
if (this.targetLocalServer != value)
|
|
{
|
|
this.targetLocalServer = value;
|
|
this.displayableCategories = null;
|
|
CheckAndLoadDisplayableCategories();
|
|
OnCategoriesChanged();
|
|
this.owners = null;
|
|
OnOwnersChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// If we're looking at an existing Job and this.Job is not
|
|
/// null, then check if the job's category is local. We do
|
|
/// this for the case where the job exists but has no targets,
|
|
/// and therefore this.targetLocalServer is false even though
|
|
/// we're in a local job category.
|
|
public bool JobCategoryIsLocal
|
|
{
|
|
get
|
|
{
|
|
Job job = this.Job;
|
|
if (job == null)
|
|
{
|
|
return false;
|
|
}
|
|
return (CategoryType.LocalJob == (SMO.Agent.CategoryType)(job.CategoryType));
|
|
}
|
|
}
|
|
|
|
public bool IsReadOnly
|
|
{
|
|
get
|
|
{
|
|
return IsRemotelyOriginated
|
|
|| (this.mode == ActionMode.Edit && !IsUserAgentAdmin && this.TrueLogin != this.Owner);
|
|
}
|
|
}
|
|
|
|
public bool AllowEnableDisable
|
|
{
|
|
get
|
|
{
|
|
// Enable/disable for non-read only jobs should always be true
|
|
// for remotely originate jobs it will always be false
|
|
// if the user is an agent operator and they do not own the job then it will be true
|
|
return IsReadOnly
|
|
? (!IsRemotelyOriginated && ((UserRole & UserRoles.AgentOperator) > 0))
|
|
: true;
|
|
}
|
|
|
|
}
|
|
|
|
private void OnCategoriesChanged()
|
|
{
|
|
// Fire the categories changed event.
|
|
if (this.CategoriesChanged != null)
|
|
{
|
|
this.CategoriesChanged(this, EventArgs.Empty);
|
|
}
|
|
}
|
|
private void OnOwnersChanged()
|
|
{
|
|
// Fire the categories changed event.
|
|
if (this.OwnersChanged != null)
|
|
{
|
|
this.OwnersChanged(this, EventArgs.Empty);
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region events
|
|
public event EventHandler CategoriesChanged;
|
|
public event EventHandler OwnersChanged;
|
|
#endregion
|
|
|
|
#region construction
|
|
public JobData(CDataContainer data, AgentJobInfo jobInfo = null, ConfigAction configAction = ConfigAction.Create)
|
|
{
|
|
this.context = data;
|
|
|
|
// get the job information
|
|
STParameters parameters = new STParameters(this.context.Document);
|
|
|
|
parameters.GetParam("job", ref this.originalName);
|
|
parameters.GetParam("jobid", ref this.jobIdString);
|
|
parameters.GetParam("script", ref this.script);
|
|
parameters.GetParam("scriptName", ref this.scriptName);
|
|
|
|
// get the Urn
|
|
string urn = string.Empty;
|
|
|
|
parameters.GetParam("urn", ref urn);
|
|
|
|
if (urn != null && urn.Length > 0)
|
|
{
|
|
this.urn = new Urn(urn);
|
|
}
|
|
|
|
bool isMsxJob = false;
|
|
parameters.GetParam("msxjob", ref isMsxJob);
|
|
|
|
//If this is an MSX, initially set TargetLocalServers to false;
|
|
if (isMsxJob)
|
|
{
|
|
this.targetLocalServer = false;
|
|
}
|
|
// we are in properties mode.
|
|
if (configAction == ConfigAction.Update)
|
|
{
|
|
this.mode = ActionMode.Edit;
|
|
CheckAndLoadGeneralData();
|
|
}
|
|
else if (this.script.Length > 0)
|
|
{
|
|
// we are creating a new job, but prepopulating
|
|
// one step with the script passed to us
|
|
this.mode = ActionMode.Create;
|
|
this.Name = this.scriptName;
|
|
SetDefaults();
|
|
this.jobSteps = new JobStepsData(context, script, this);
|
|
}
|
|
// creating a new job
|
|
else
|
|
{
|
|
this.mode = ActionMode.Create;
|
|
// set defaults that do not involve going to the server to retrieve
|
|
SetDefaults();
|
|
}
|
|
|
|
// load AgentJobInfo data
|
|
if (jobInfo != null)
|
|
{
|
|
this.currentName = jobInfo.Name;
|
|
this.Owner = jobInfo.Owner;
|
|
this.Description = jobInfo.Description;
|
|
this.Enabled = jobInfo.Enabled;
|
|
this.startStepID = jobInfo.StartStepId;
|
|
this.Category = ConvertStringToCategory(jobInfo.Category);
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region properties - non public
|
|
internal Job Job
|
|
{
|
|
get
|
|
{
|
|
Job job = null;
|
|
// If JobID is passed in look up by jobID
|
|
if (!string.IsNullOrEmpty(this.jobIdString))
|
|
{
|
|
job = this.context.Server.JobServer.Jobs.ItemById(Guid.Parse(this.jobIdString));
|
|
|
|
// Job name might not be passed in context, let us fix job name and urn
|
|
this.originalName = job.Name;
|
|
this.urn = job.Urn;
|
|
}
|
|
else
|
|
{
|
|
// or use urn path to query job
|
|
job = this.context.Server.GetSmoObject(this.urn) as Job;
|
|
}
|
|
|
|
return job;
|
|
}
|
|
}
|
|
internal Urn Urn
|
|
{
|
|
get
|
|
{
|
|
return this.urn;
|
|
}
|
|
}
|
|
|
|
internal string TrueLogin
|
|
{
|
|
get
|
|
{
|
|
if (this.trueLogin == null)
|
|
{
|
|
LoadTrueLogin();
|
|
}
|
|
return trueLogin;
|
|
}
|
|
|
|
}
|
|
#endregion
|
|
|
|
#region data loading / initialization
|
|
|
|
private void CheckAndLoadMsaInformation()
|
|
{
|
|
if (msaInformationLoaded || this.mode == ActionMode.Create)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Job job = this.Job;
|
|
|
|
msaInformationLoaded = true;
|
|
|
|
DataTable table = job.EnumTargetServers();
|
|
string targetName;
|
|
if (table.Rows.Count == 0)
|
|
{
|
|
originallyTargetLocalServer = this.TargetLocalServer = false;
|
|
}
|
|
// if the server is an msx then see if this job is targetted at local
|
|
// servers
|
|
else if (this.IsMsx)
|
|
{
|
|
for (int i = 0; i < table.Rows.Count; ++i)
|
|
{
|
|
targetName = table.Rows[i]["ServerName"].ToString().ToLowerInvariant();
|
|
|
|
for (int ii = 0; ii < this.AvailableTargetServers.Length; ++ii)
|
|
{
|
|
if (targetName == this.AvailableTargetServers[ii].Name.ToLowerInvariant())
|
|
{
|
|
AvailableTargetServers[ii].IsJobAppliedToTarget = AvailableTargetServers[ii].WillJobBeAppliedToTarget = true;
|
|
originallyTargetLocalServer = this.TargetLocalServer = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void CheckAndLoadGeneralData()
|
|
{
|
|
if (this.generalInfoLoaded || this.mode == ActionMode.Create)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Job job = this.Job;
|
|
|
|
this.currentName = job.Name;
|
|
this.description = job.Description;
|
|
this.enabled = job.IsEnabled;
|
|
// this.source = job.Source;
|
|
this.created = job.DateCreated;
|
|
this.lastModified = job.DateLastModified;
|
|
this.lastExecution = job.LastRunDate;
|
|
|
|
this.originatingServer = job.OriginatingServer;
|
|
|
|
this.generalInfoLoaded = true;
|
|
CheckAndLoadMsaInformation();
|
|
}
|
|
|
|
private void CheckAndLoadOwner()
|
|
{
|
|
if (this.owner != null)
|
|
{
|
|
return;
|
|
}
|
|
if (this.Mode == ActionMode.Edit)
|
|
{
|
|
this.owner = this.Job.OwnerLoginName;
|
|
}
|
|
else
|
|
{
|
|
this.owner = this.context.ConnectionInfo.UserName.TrimEnd();
|
|
}
|
|
}
|
|
|
|
private void CheckAndLoadJobSteps()
|
|
{
|
|
if (this.jobSteps != null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
this.jobSteps = new JobStepsData(context, this);
|
|
}
|
|
|
|
private void CheckAndLoadJobSchedules()
|
|
{
|
|
if (this.jobSchedules != null)
|
|
{
|
|
return;
|
|
}
|
|
// load schedules
|
|
this.jobSchedules = new JobSchedulesData(context, this);
|
|
}
|
|
|
|
private void CheckAndLoadJobAlerts()
|
|
{
|
|
if (this.jobAlerts != null || (UserRole & UserRoles.SysAdmin) == 0)
|
|
{
|
|
return;
|
|
}
|
|
this.jobAlerts = new JobAlertsData(context, this);
|
|
}
|
|
|
|
// loads default values for a new job.
|
|
private void SetDefaults()
|
|
{
|
|
this.description = string.Empty;
|
|
this.enabled = true;
|
|
this.source = null;
|
|
|
|
// notifications
|
|
this.operatorToEmail = string.Empty;
|
|
this.emailLevel = CompletionAction.Never;
|
|
|
|
this.operatorToPage = string.Empty;
|
|
this.pageLevel = CompletionAction.Never;
|
|
this.eventLogLevel = CompletionAction.Never;
|
|
this.deleteLevel = CompletionAction.Never;
|
|
|
|
this.generalInfoLoaded = true;
|
|
this.notificationsLoaded = true;
|
|
this.msaInformationLoaded = true;
|
|
|
|
this.originatingServer = string.Empty;
|
|
}
|
|
|
|
private void CheckAndLoadNotifications()
|
|
{
|
|
if (this.notificationsLoaded || this.Mode == ActionMode.Create)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Job job = this.Job;
|
|
|
|
if (this.Version.Major >= 9 || ((UserRole & UserRoles.SysAdmin) > 0))
|
|
{
|
|
// notifications
|
|
this.operatorToEmail = job.OperatorToEmail;
|
|
this.emailLevel = job.EmailLevel;
|
|
|
|
this.operatorToPage = job.OperatorToPage;
|
|
this.pageLevel = job.PageLevel;
|
|
}
|
|
else
|
|
{
|
|
this.operatorToEmail = String.Empty;
|
|
this.emailLevel = CompletionAction.Never;
|
|
|
|
this.operatorToPage = String.Empty;
|
|
this.pageLevel = CompletionAction.Never;
|
|
}
|
|
|
|
this.eventLogLevel = job.EventLogLevel;
|
|
this.deleteLevel = job.DeleteLevel;
|
|
this.notificationsLoaded = true;
|
|
}
|
|
|
|
private void CheckAndLoadCategory()
|
|
{
|
|
if (this.category != null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (this.Mode == ActionMode.Edit)
|
|
{
|
|
this.category = ConvertStringToCategory(this.Job.Category);
|
|
}
|
|
else
|
|
{
|
|
this.category = Categories[0];
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Load all SMO categories.
|
|
/// </summary>
|
|
private void CheckAndLoadSmoCategories()
|
|
{
|
|
if (this.smoCategories != null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// force a preload of all of the category information. We cache this,
|
|
// and if we are not careful SMO will end up executing a batch per category
|
|
this.context.Server.SetDefaultInitFields(typeof(JobCategory), true);
|
|
|
|
JobServer jobServer = this.context.Server.JobServer;
|
|
List<LocalizableCategory> smoCategories = new List<LocalizableCategory>();
|
|
for (int i = 0; i < jobServer.JobCategories.Count; ++i)
|
|
{
|
|
smoCategories.Add(new LocalizableCategory(jobServer.JobCategories[i]));
|
|
}
|
|
this.smoCategories = smoCategories.ToArray();
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Load only those categories that will be displayed in the categories drop-down based on
|
|
/// TSX and MSX job status.
|
|
/// </summary>
|
|
private void CheckAndLoadDisplayableCategories()
|
|
{
|
|
if (this.displayableCategories != null)
|
|
{
|
|
return;
|
|
}
|
|
this.displayableCategories = null;
|
|
|
|
LocalizableCategory[] allCategories = this.SmoCategories;
|
|
|
|
List<LocalizableCategory> displayableCategories = new List<LocalizableCategory>();
|
|
|
|
bool targetsLocalServer = this.targetLocalServer || this.JobCategoryIsLocal;
|
|
|
|
|
|
// get all applicable categories
|
|
for (int i = 0; i < allCategories.Length; ++i)
|
|
{
|
|
bool validCategoryForThisContext = false;
|
|
CategoryType currentJobCategoryType = allCategories[i].SmoCategory.CategoryType;
|
|
|
|
if (targetsLocalServer)
|
|
{
|
|
// Only local jobs are valid, regardless of whether they are originated remotely or locally
|
|
// (Besides, it doesn't matter for TSX jobs as their category cannot be changed by TSX anyway)
|
|
validCategoryForThisContext = currentJobCategoryType == CategoryType.LocalJob;
|
|
}
|
|
else
|
|
{
|
|
if (this.IsRemotelyOriginated)
|
|
{
|
|
if (!this.IsMsx)
|
|
{
|
|
validCategoryForThisContext = (currentJobCategoryType == CategoryType.LocalJob);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (this.IsMsx)
|
|
{
|
|
validCategoryForThisContext = (currentJobCategoryType == CategoryType.MultiServerJob);
|
|
}
|
|
}
|
|
}
|
|
|
|
///See if this category can be added.
|
|
if (validCategoryForThisContext)
|
|
{
|
|
displayableCategories.Add(allCategories[i]);
|
|
}
|
|
|
|
}
|
|
this.displayableCategories = displayableCategories.ToArray();
|
|
}
|
|
|
|
private void LoadLogins()
|
|
{
|
|
// figure out what rights the user has.
|
|
SqlServer.Management.Smo.Server server = this.context.Server;
|
|
// see if the user is a sysadmin. At the moment sysadmins can assign
|
|
// job ownership to any user. Non sysadmins cannot. Operators can see jobs owned by anyone
|
|
if ((this.UserRole & UserRoles.SysAdmin) > 0 || (this.UserRole & UserRoles.AgentOperator) > 0)
|
|
{
|
|
System.Collections.Specialized.StringCollection validLoginNames = new System.Collections.Specialized.StringCollection();
|
|
|
|
foreach (SMO.Login login in server.Logins)
|
|
{
|
|
if (SMO.LoginType.WindowsGroup != login.LoginType)
|
|
{
|
|
//For Msx jobs, only add logins that are members of the sysadmin role.
|
|
if (!this.targetLocalServer)
|
|
{
|
|
if (login.IsMember("sysadmin"))
|
|
{
|
|
validLoginNames.Add(login.Name);
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
//Otherwise, if this is NOT an Msx jobs, just add it.
|
|
validLoginNames.Add(login.Name);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
//validLoginNames will not include the current connection's trusted user therefore
|
|
//add it to the owners string array. This will allow the value to be seen (and selected) in
|
|
//the job properties drop down.
|
|
//Only add the name if it doesn't already exist in the collection
|
|
if (!validLoginNames.Contains(TrueLogin))
|
|
{
|
|
validLoginNames.Add(TrueLogin);
|
|
}
|
|
|
|
this.owners = new string[validLoginNames.Count];
|
|
validLoginNames.CopyTo(this.owners, 0);
|
|
}
|
|
else
|
|
{
|
|
// the user is the only person allowed to own the job
|
|
this.owners = new string[1] { server.ConnectionContext.TrueLogin };
|
|
}
|
|
}
|
|
|
|
private void LoadTargetServers()
|
|
{
|
|
if ((UserRole & UserRoles.SysAdmin) > 0 || (UserRole & UserRoles.AgentOperator) > 0)
|
|
{
|
|
// load any target servers if this is an msx
|
|
this.targetServers = new MsaJobTargetServer[this.context.Server.JobServer.TargetServers.Count];
|
|
if (this.targetServers.Length > 0)
|
|
{
|
|
for (int i = 0; i < this.targetServers.Length; ++i)
|
|
{
|
|
this.targetServers[i] = new MsaJobTargetServer(this.context.Server.JobServer.TargetServers[i].Name);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
this.targetServers = new MsaJobTargetServer[0];
|
|
}
|
|
}
|
|
|
|
private void LoadOperators()
|
|
{
|
|
if (this.Version.Major >= 9 || ((UserRole & UserRoles.SysAdmin) > 0))
|
|
{
|
|
// load operators
|
|
int operatorCount = this.context.Server.JobServer.Operators.Count;
|
|
this.operators = new string[operatorCount];
|
|
for (int i = 0; i < operatorCount; i++)
|
|
{
|
|
this.operators[i] = this.context.Server.JobServer.Operators[i].Name;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
this.operators = new string[0];
|
|
}
|
|
}
|
|
|
|
private void LoadTrueLogin()
|
|
{
|
|
this.trueLogin = this.context.Server.ConnectionContext.TrueLogin;
|
|
}
|
|
|
|
private void LoadVersion()
|
|
{
|
|
this.version = this.context.Server.Information.Version;
|
|
}
|
|
|
|
private void LoadUserRoles()
|
|
{
|
|
SqlServer.Management.Smo.Server server = this.context.Server;
|
|
|
|
this.userRole = UserRoles.None;
|
|
|
|
if ((server.ConnectionContext.UserProfile & ServerUserProfiles.SALogin) > 0)
|
|
{
|
|
this.userRole |= UserRoles.SysAdmin;
|
|
}
|
|
|
|
if (this.Version.Major >= 9)
|
|
{
|
|
Database msdb = server.Databases["msdb"];
|
|
if (msdb != null)
|
|
{
|
|
if (msdb.IsMember("SQLAgentOperatorRole"))
|
|
{
|
|
this.userRole |= UserRoles.AgentOperator;
|
|
}
|
|
if (msdb.IsMember("SQLAgentReaderRole"))
|
|
{
|
|
this.userRole |= UserRoles.AgentReader;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.userRole |= UserRoles.AgentUser;
|
|
}
|
|
#endregion
|
|
|
|
#region saving
|
|
public void ApplyChanges(bool creating)
|
|
{
|
|
Job job = null;
|
|
bool scripting = this.context.Server.ConnectionContext.SqlExecutionModes == SqlExecutionModes.CaptureSql;
|
|
bool targetServerSelected = false;
|
|
|
|
this.mode = creating ? ActionMode.Create : ActionMode.Edit;
|
|
|
|
// Before any job posting if donem make sure that if this is an MSX job that the user has selected at
|
|
// least one Target Server.
|
|
if (!this.targetLocalServer)
|
|
{
|
|
for (int i = 0; i < this.AvailableTargetServers.Length; ++i)
|
|
{
|
|
if (this.AvailableTargetServers[i].WillJobBeAppliedToTarget)
|
|
{
|
|
targetServerSelected = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!targetServerSelected)
|
|
{
|
|
// Not target servers selected. Throw error.
|
|
throw new ApplicationException(SR.TargetServerNotSelected);
|
|
}
|
|
}
|
|
|
|
if (creating)
|
|
{
|
|
job = new Job(this.context.Server.JobServer, this.Name, this.Category.SmoCategory.ID);
|
|
}
|
|
else
|
|
{
|
|
// just lookup the original object
|
|
job = this.Job;
|
|
}
|
|
|
|
if (!this.IsReadOnly)
|
|
{
|
|
if (creating || job.OwnerLoginName != this.owner)
|
|
{
|
|
job.OwnerLoginName = this.owner;
|
|
}
|
|
|
|
if (creating || (this.category != null
|
|
&& this.category.SmoCategory.Name != job.Category))
|
|
{
|
|
job.Category = this.category.SmoCategory.Name;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(this.description) && (creating || this.description != job.Description))
|
|
{
|
|
job.Description = this.description;
|
|
}
|
|
|
|
SaveNotifications(job, creating);
|
|
}
|
|
|
|
if (this.AllowEnableDisable)
|
|
{
|
|
if (creating || this.enabled != job.IsEnabled)
|
|
{
|
|
job.IsEnabled = this.enabled;
|
|
}
|
|
}
|
|
|
|
// do the actual creation / alter
|
|
if (creating)
|
|
{
|
|
///Check to see if the job already exists
|
|
JobExists(job.Name);
|
|
job.Create();
|
|
if (!scripting)
|
|
{
|
|
this.urn = job.Urn;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
job.StartStepID = this.startStepID != -1 ? this.startStepID : job.StartStepID;
|
|
job.Alter();
|
|
}
|
|
|
|
if (!this.IsReadOnly && !scripting)
|
|
{
|
|
if (this.targetLocalServer)
|
|
{
|
|
if (!OriginallyTargetLocalServer || creating)
|
|
{
|
|
foreach (MsaJobTargetServer targetServer in this.AvailableTargetServers)
|
|
{
|
|
if (targetServer.IsJobAppliedToTarget)
|
|
{
|
|
job.RemoveFromTargetServer(targetServer.Name.ToUpperInvariant());
|
|
targetServer.IsJobAppliedToTarget = false;
|
|
}
|
|
}
|
|
|
|
OriginallyTargetLocalServer = true;
|
|
job.ApplyToTargetServer(this.context.Server.ConnectionContext.TrueName.ToUpperInvariant());
|
|
}
|
|
}
|
|
else if (this.IsMsx)
|
|
{
|
|
|
|
if (!creating && OriginallyTargetLocalServer)
|
|
{
|
|
// Remove from target server only if actually does target the local server
|
|
string thisServerName = this.context.Server.ConnectionContext.TrueName.ToUpperInvariant();
|
|
DataTable targetServers = job.EnumTargetServers();
|
|
foreach (DataRow row in targetServers.Rows)
|
|
{
|
|
string targetServerName = row["ServerName"] as string;
|
|
if (String.Compare(targetServerName, thisServerName, StringComparison.OrdinalIgnoreCase) == 0)
|
|
{
|
|
job.RemoveFromTargetServer(thisServerName);
|
|
break;
|
|
}
|
|
}
|
|
|
|
OriginallyTargetLocalServer = false;
|
|
}
|
|
|
|
// add and remove target servers
|
|
for (int i = 0; i < this.AvailableTargetServers.Length; ++i)
|
|
{
|
|
if (this.AvailableTargetServers[i].WillJobBeAppliedToTarget && !this.AvailableTargetServers[i].IsJobAppliedToTarget)
|
|
{
|
|
job.ApplyToTargetServer(this.AvailableTargetServers[i].Name.ToUpperInvariant());
|
|
this.AvailableTargetServers[i].IsJobAppliedToTarget = true;
|
|
}
|
|
else if (!this.AvailableTargetServers[i].WillJobBeAppliedToTarget && this.AvailableTargetServers[i].IsJobAppliedToTarget)
|
|
{
|
|
job.RemoveFromTargetServer(this.AvailableTargetServers[i].Name);
|
|
this.AvailableTargetServers[i].IsJobAppliedToTarget = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// Because some of the SMO methods above can update Job's CategoryID affecting the Urn
|
|
this.urn = job.Urn;
|
|
|
|
bool stepsChanged = false;
|
|
// save steps,schedules etc
|
|
if (this.jobSteps != null)
|
|
{
|
|
stepsChanged = this.jobSteps.ApplyChanges(job);
|
|
}
|
|
bool schedulesChanged = false;
|
|
if (this.jobSchedules != null)
|
|
{
|
|
schedulesChanged = this.jobSchedules.ApplyChanges(job);
|
|
}
|
|
|
|
if ((stepsChanged || schedulesChanged) && !this.TargetLocalServer && !creating)
|
|
{
|
|
// TODO: this seems wrong. Why do it here and not in SMO?
|
|
this.context.Server.ConnectionContext.ExecuteNonQuery(
|
|
string.Format(CultureInfo.InvariantCulture, "EXECUTE msdb.dbo.sp_post_msx_operation N'INSERT', N'JOB', @job_id = '{0}'"
|
|
, job.JobID));
|
|
}
|
|
|
|
// Do not attempt to save the job alert if we are in scripting mode, since the job id does not
|
|
// yet exists.
|
|
if (jobAlerts != null && !scripting)
|
|
{
|
|
this.jobAlerts.ApplyChanges(job);
|
|
}
|
|
|
|
// check the name if we are not creating
|
|
if (!creating && this.Name != job.Name )
|
|
{
|
|
// new name = rename
|
|
job.Rename(this.Name);
|
|
|
|
// get the new urn if we aren't scripting
|
|
if (!scripting)
|
|
{
|
|
this.urn = job.Urn;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void SaveNotifications(Job job, bool creating)
|
|
{
|
|
if (job == null)
|
|
{
|
|
throw new ArgumentNullException("job");
|
|
}
|
|
|
|
// nothing to do if this information has not been loaded
|
|
if (this.notificationsLoaded == false)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// notifications panel
|
|
if (this.emailLevel == CompletionAction.Never)
|
|
{
|
|
this.emailLevel = CompletionAction.OnFailure;
|
|
this.operatorToEmail = String.Empty;
|
|
}
|
|
|
|
if (creating || this.emailLevel != job.EmailLevel)
|
|
{
|
|
job.EmailLevel = this.emailLevel;
|
|
}
|
|
|
|
if (creating || this.operatorToEmail != job.OperatorToEmail)
|
|
{
|
|
job.OperatorToEmail = this.operatorToEmail;
|
|
}
|
|
|
|
if (this.pageLevel == CompletionAction.Never)
|
|
{
|
|
this.pageLevel = CompletionAction.OnFailure;
|
|
this.operatorToPage = String.Empty;
|
|
}
|
|
|
|
if (creating || this.pageLevel != job.PageLevel)
|
|
{
|
|
job.PageLevel = this.pageLevel;
|
|
}
|
|
|
|
if (creating || this.operatorToPage != job.OperatorToPage)
|
|
{
|
|
job.OperatorToPage = this.operatorToPage;
|
|
}
|
|
|
|
if (creating || this.eventLogLevel != job.EventLogLevel)
|
|
{
|
|
job.EventLogLevel = this.eventLogLevel;
|
|
}
|
|
|
|
if (creating || this.deleteLevel != job.DeleteLevel)
|
|
{
|
|
job.DeleteLevel = this.deleteLevel;
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region implementation
|
|
/// <summary>
|
|
/// Convert a string into a Localizable job category
|
|
/// </summary>
|
|
/// <param name="source"></param>
|
|
/// <returns></returns>
|
|
internal LocalizableCategory ConvertStringToCategory(string source)
|
|
{
|
|
if (this.SmoCategories == null || this.SmoCategories.Length == 0)
|
|
{
|
|
throw new InvalidOperationException();
|
|
}
|
|
|
|
LocalizableCategory category = null;
|
|
|
|
for (int i = 0; i < this.SmoCategories.Length; ++i)
|
|
{
|
|
if (source == this.SmoCategories[i].SmoCategory.Name)
|
|
{
|
|
category = this.SmoCategories[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
return category;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check SMO to see if job already exists.
|
|
/// </summary>
|
|
/// <param name="jobName"></param>
|
|
private void JobExists(string jobName)
|
|
{
|
|
Microsoft.SqlServer.Management.Smo.Agent.JobCollection smoJobCollection = this.context.Server.JobServer.Jobs;
|
|
try
|
|
{
|
|
if (smoJobCollection.Contains(jobName))
|
|
{
|
|
throw new ApplicationException(SR.JobAlreadyExists(jobName));
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
smoJobCollection = null;
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
}
|
|
}
|