/* Copyright (c) Citrix Systems, Inc. * All rights reserved. * * Redistribution and use in source and binary forms, * with or without modification, are permitted provided * that the following conditions are met: * * * Redistributions of source code must retain the above * copyright notice, this list of conditions and the * following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the * following disclaimer in the documentation and/or other * materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Drawing; using System.Linq; using System.Windows.Forms; using XenAdmin.Controls; using XenAdmin.Core; using XenAdmin.Network; using XenAdmin.Properties; using XenAPI; using XenCenterLib; namespace XenAdmin.Wizards.HAWizard_Pages { public partial class AssignPriorities : XenTabPage { private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); private IXenConnection connection; private readonly CollectionChangeEventHandler VM_CollectionChangedWithInvoke; private readonly QueuedBackgroundWorker m_worker; /// /// May not be set to null. /// public new IXenConnection Connection { set { if (value == null) throw new ArgumentNullException(); if (connection != null) DeregisterEvents(); this.connection = value; RegisterEvents(); UpdateMenuItems(); PopulateVMs(); haNtolIndicator.Connection = value; haNtolIndicator.Settings = getCurrentSettings(); } } private void UpdateMenuItems() { List restartPriorities = VM.GetAvailableRestartPriorities(connection); //When this line: m_dropDownButtonRestartPriority.ContextMenuStrip = this.contextMenuStrip //was called in the designer a "dummy" item was added to the contextMenuStrip by the m_dropDownButtonRestartPriority, //however here it is cleared so no need to remove it explicitly later when contextMenuStrip.Items is called contextMenuStrip.Items.Clear(); foreach (var restartPriority in restartPriorities) { var menuItem = contextMenuStrip.Items.Add(Helpers.RestartPriorityI18n(restartPriority)); menuItem.Tag = restartPriority; menuItem.Click += priority_Click; } } /// /// Called when the current IXenConnection's VM dictionary changes. /// private void VM_CollectionChanged(object sender, CollectionChangeEventArgs e) { Program.AssertOnEventThread(); VM vm = (VM)e.Element; switch (e.Action) { case CollectionChangeAction.Add: vm.PropertyChanged -= vm_PropertyChanged; vm.PropertyChanged += vm_PropertyChanged; AddVmRow(vm); UpdateVMsAgility(new List {vm}); break; case CollectionChangeAction.Remove: vm.PropertyChanged -= vm_PropertyChanged; RemoveVmRow(vm); break; } } /// /// The current (uncommitted) VM restart priorities. /// public Dictionary CurrentSettings { get { return haNtolIndicator.Settings; } } /// /// The Ntol from the HaNtolIndicator control contained within. /// public long Ntol { get { return haNtolIndicator.Ntol; } } /// /// Whether the user has made any changes to VM restart priorities on the server. /// public bool ChangesMade { get; private set; } public AssignPriorities() { InitializeComponent(); VM_CollectionChangedWithInvoke = Program.ProgramInvokeHandler(VM_CollectionChanged); haNtolIndicator.UpdateInProgressChanged += haNtolIndicator_UpdateInProgressChanged; nudStartDelay.Maximum = long.MaxValue; nudOrder.Maximum = long.MaxValue; m_worker = new QueuedBackgroundWorker(); } /// /// Clean up any resources being used. /// /// true if managed resources should be disposed; otherwise, false. protected override void Dispose(bool disposing) { DeregisterEvents(); if (disposing && (components != null)) { haNtolIndicator.UpdateInProgressChanged -= haNtolIndicator_UpdateInProgressChanged; components.Dispose(); } StopNtolUpdate(); base.Dispose(disposing); } private void RegisterEvents() { // Add listeners connection.Cache.RegisterCollectionChanged(VM_CollectionChangedWithInvoke); foreach (VM vm in connection.Cache.VMs) { vm.PropertyChanged -= vm_PropertyChanged; vm.PropertyChanged += vm_PropertyChanged; } } private void DeregisterEvents() { // Remove listeners connection.Cache.DeregisterCollectionChanged(VM_CollectionChangedWithInvoke); foreach (VM vm in connection.Cache.VMs) vm.PropertyChanged -= vm_PropertyChanged; } /// /// Sets all agile VMs to state 'Protected' and all non-agile VMs to 'Restart if possible'. /// Important: call before setting the Connection property. /// internal bool ProtectVmsByDefault { get; set; } internal void StartNtolUpdate() { haNtolIndicator.StartNtolUpdate(); } internal void StopNtolUpdate() { haNtolIndicator.StopNtolUpdate(); } /// /// Must be called on the event thread. /// private void PopulateVMs() { Program.AssertOnEventThread(); try { dataGridViewVms.SuspendLayout(); dataGridViewVms.Rows.Clear(); var newRows = new List(); var vms = connection.Cache.VMs.Where(v => v.HaCanProtect(Properties.Settings.Default.ShowHiddenVMs)); // see if HA is being activated for the first time. bool firstTime = IsHaActivatedFirstTime(vms); foreach (VM vm in connection.Cache.VMs) { if (!vm.HaCanProtect(Properties.Settings.Default.ShowHiddenVMs)) continue; // Create a new row for this VM. // The priority for this row is either initially null (which means 'fill in the restart priority with // Protected/Restart if possible when we've determined if the VM is agile'), or its current restart priority. // The first case is for the HA wizard when priorities are being configured for the first time, // the second is for the edit dialog, when HA is already enabled. VM.HA_Restart_Priority? priority = firstTime ? (VM.HA_Restart_Priority?)null : vm.HARestartPriority(); var row = new VmWithSettingsRow(vm, priority); newRows.Add(row); } dataGridViewVms.Rows.AddRange(newRows.ToArray()); var addedVms = from row in dataGridViewVms.Rows.Cast() select row.Vm; UpdateVMsAgility(addedVms); } finally { dataGridViewVms.ResumeLayout(); } } /// /// Starts a new background thread that updates the displayed agility status for each VM. /// private void UpdateVMsAgility(IEnumerable vms) { Debug.Assert(connection != null, "Connection property must have been set to non-null before calling this function"); //worker starts on UI (main) thread m_worker.RunWorkerAsync((sender, arg) => worker_DoWork(null, vms), worker_RunWorkerCompleted); } private object worker_DoWork(object sender, object arg) { var vms = arg as IEnumerable; Debug.Assert(vms != null); Session session = connection.DuplicateSession(); var results = new Dictionary(); foreach (VM vm in vms) { try { VM.assert_agile(session, vm.opaque_ref); results[vm] = null; } catch (Failure failure)//The VM wasn't agile { results[vm] = failure.ErrorDescription[0]; } } return results; } private void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { if (e.Error != null) { log.Error(e.Error); return; } var result = e.Result as Dictionary; Debug.Assert(result != null); foreach (var pair in result) { VM vm = pair.Key; string nonAgileReason = pair.Value; bool isNowAgile = nonAgileReason == null; //worker started on main thread => this event handler will be on //main thread too, so no need to invoke VmWithSettingsRow row = findItemFromVM(vm); if (row == null) return; // We previously had no restart priority assigned => protectVmsByDefault. // Now we know whether the VM is agile, so we can assign it the highest protection available. if (row.RestartPriority == null) { Debug.Assert(ProtectVmsByDefault); var priority = isNowAgile ? VM.HaHighestProtectionAvailable(connection) : VM.HA_Restart_Priority.BestEffort; row.UpdateRestartPriority(priority); haNtolIndicator.Settings = getCurrentSettings(); } else if (!isNowAgile && row.RestartPriority != VM.HA_Restart_Priority.BestEffort && row.RestartPriority != VM.HA_Restart_Priority.DoNotRestart) { row.UpdateRestartPriority(VM.HA_Restart_Priority.BestEffort); haNtolIndicator.Settings = getCurrentSettings(); } row.UpdateAgile(isNowAgile); row.FriendlyNonAgileReason = nonAgileReason; } updateButtons(); } private bool IsHaActivatedFirstTime(IEnumerable vms) { return ProtectVmsByDefault && vms.All(v => string.IsNullOrEmpty(v.ha_restart_priority)); } private void AddVmRow(VM vm) { Program.AssertOnEventThread(); if (!vm.HaCanProtect(Properties.Settings.Default.ShowHiddenVMs)) return; // see if HA is being activated for the first time var vms = connection.Cache.VMs.Where(v => v.HaCanProtect(Properties.Settings.Default.ShowHiddenVMs)); bool firstTime = IsHaActivatedFirstTime(vms); VM.HA_Restart_Priority? priority = firstTime ? (VM.HA_Restart_Priority?)null : vm.HARestartPriority(); var row = new VmWithSettingsRow(vm, priority); dataGridViewVms.Rows.Add(row); } private void RemoveVmRow(VM vm) { Program.AssertOnEventThread(); var row = findItemFromVM(vm); if (row != null) dataGridViewVms.Rows.Remove(row); } /// /// Finds the ListViewItem for the given VM. Will return null if no corresponding item could be found. /// Must be called on the event thread. /// private VmWithSettingsRow findItemFromVM(VM vm) { Program.AssertOnEventThread(); return dataGridViewVms.Rows.Cast().FirstOrDefault(r => r.Vm == vm); } private void vm_PropertyChanged(object sender, PropertyChangedEventArgs e) { Program.Invoke(this, () => { VM vm = (VM)sender; if (vm == null) return; // Find row for VM var row = findItemFromVM(vm); try { dataGridViewVms.SuspendLayout(); if (row == null) AddVmRow(vm); else { row.UpdateVm(vm); row.SetAgileCalculating(); } UpdateVMsAgility(new List {vm}); } finally { dataGridViewVms.ResumeLayout(); } }); } private void haNtolIndicator_UpdateInProgressChanged(object sender, EventArgs e) { // Signal to anyone who cares that this page is good to continue, or not. OnPageUpdated(); if (!haNtolIndicator.UpdateInProgress) { if (haNtolIndicator.Ntol == -1) { labelHaStatus.Text = Messages.HA_UNABLE_TO_CALCULATE_MESSAGE; labelHaStatus.ForeColor = Color.Red; pictureBoxStatus.Image = Resources._000_Alert2_h32bit_16; return; } if (haNtolIndicator.Overcommitted || haNtolIndicator.Ntol == 0) { labelHaStatus.Text = Messages.HA_OVERCOMMIT_MESSAGE; labelHaStatus.ForeColor = Color.Red; pictureBoxStatus.Image = Resources._000_Alert2_h32bit_16; return; } labelHaStatus.Text = string.Format(Messages.HA_GUARANTEED_MESSAGE, haNtolIndicator.NtolMax); labelHaStatus.ForeColor = SystemColors.ControlText; pictureBoxStatus.Image = Resources._000_Tick_h32bit_16; } } private void dataGridViewVms_KeyDown(object sender, KeyEventArgs e) { if (e.Control && e.KeyCode == Keys.A) { try { dataGridViewVms.SuspendLayout(); dataGridViewVms.SelectAll(); } finally { dataGridViewVms.ResumeLayout(); } } } /// /// Called when the user clicks one of the restart priorities in the context menu. /// private void priority_Click(object sender, EventArgs e) { var menuitem = (ToolStripMenuItem)sender; VM.HA_Restart_Priority pri = (VM.HA_Restart_Priority)menuitem.Tag; bool changesMade = false; foreach (var row in dataGridViewVms.SelectedRows.Cast()) { if (row.RestartPriority != pri) { changesMade = true; row.UpdateRestartPriority(pri); } } if (changesMade) { ChangesMade = true; haNtolIndicator.Settings = getCurrentSettings(); // Will trigger a ntol update under this revised plan updateButtons(); } } /// /// Gets the current (uncommitted) VM restart priorities. Must be called on the GUI thread. /// private Dictionary getCurrentSettings() { Program.AssertOnEventThread(); Dictionary result = new Dictionary(); foreach (var row in dataGridViewVms.Rows.Cast()) { // If the restart priority is null, it means we don't know if the VM is agile yet, and we have // protectVmsByDefault == true. result[row.Vm] = row.RestartPriority ?? VM.HA_Restart_Priority.BestEffort; } return result; } private void dataGridViewVms_SelectionChanged(object sender, EventArgs e) { updateButtons(); } private void m_dropDownButtonRestartPriority_Click(object sender, EventArgs e) { var selectedRows = dataGridViewVms.SelectedRows.Cast(); foreach (ToolStripMenuItem item in contextMenuStrip.Items) { var itemRestartPriority = ((VM.HA_Restart_Priority)item.Tag); item.Checked = selectedRows.Count() > 0 && selectedRows.All(s => s.RestartPriority == itemRestartPriority); } } private void updateButtons() { Program.AssertOnEventThread(); var selectedRows = dataGridViewVms.SelectedRows.Cast(); if (dataGridViewVms.SelectedRows.Count == 0) { m_dropDownButtonRestartPriority.Enabled = false; m_dropDownButtonRestartPriority.Text = ""; nudOrder.Enabled = nudStartDelay.Enabled = false; nudOrder.Text = nudStartDelay.Text = ""; return; } // if there is a VM with null priority in the selection disable all buttons. We are waiting on the background thread // to see if the VM is agile before giving it a starting value if (selectedRows.Any(r => r.RestartPriority == null)) { m_dropDownButtonRestartPriority.Enabled = true; m_dropDownButtonRestartPriority.Text = ""; nudOrder.Enabled = nudStartDelay.Enabled = true; nudOrder.Text = nudStartDelay.Text = ""; return; } m_dropDownButtonRestartPriority.Enabled = true; //now set the drop down button text bool allSamePriority = false; foreach (ToolStripMenuItem item in contextMenuStrip.Items) { VM.HA_Restart_Priority itemRestartPriority = (VM.HA_Restart_Priority)item.Tag; if (selectedRows.All(r => r.RestartPriority == itemRestartPriority)) { allSamePriority = true; m_dropDownButtonRestartPriority.Text = Helpers.RestartPriorityI18n(itemRestartPriority); break; } } if (!allSamePriority && dataGridViewVms.SelectedRows.Count > 1) { m_dropDownButtonRestartPriority.Text = Messages.HA_ASSIGN_PRIORITIES_MIXED_PROTECTION_LEVELS; } // set the order and delay NUDs nudOrder.Enabled = nudStartDelay.Enabled = true; var orderDistList = (from row in selectedRows select row.StartOrder).Distinct(); nudOrder.Text = orderDistList.Count() == 1 ? orderDistList.ElementAt(0).ToString() : ""; var delayDistList = (from row in selectedRows select row.StartDelay).Distinct(); nudStartDelay.Text = delayDistList.Count() == 1 ? delayDistList.ElementAt(0).ToString() : ""; // check that all the VMs selected in the list are agile and make sure the protect button is disabled with the relevant reason VmWithSettingsRow nonAgileRow = selectedRows.FirstOrDefault(r => !r.IsAgile); // Now set the buttons and tooltips) foreach (ToolStripMenuItem menuItem in contextMenuStrip.Items) { var priority = (VM.HA_Restart_Priority)menuItem.Tag; if (VM.HaPriorityIsRestart(connection, priority)) { menuItem.Enabled = (nonAgileRow == null); menuItem.ToolTipText = (nonAgileRow == null) ? "" : nonAgileRow.FriendlyNonAgileReason; } else { menuItem.Enabled = true; } } } private void nudOrder_ValueChanged(object sender, EventArgs e) { long newValue = (long)nudOrder.Value; bool changesMade = false; foreach (var row in dataGridViewVms.SelectedRows.Cast()) { if (row.StartOrder != newValue) { changesMade = true; row.UpdateStartOrder(newValue); } } if (changesMade) { ChangesMade = true; updateButtons(); } } private void nudStartDelay_ValueChanged(object sender, EventArgs e) { long newValue = (long)nudStartDelay.Value; bool changesMade = false; foreach (var row in dataGridViewVms.SelectedRows.Cast()) { if (row.StartDelay != newValue) { changesMade = true; row.UpdateStartDelay(newValue); } } if (changesMade) { ChangesMade = true; updateButtons(); } } /// /// Gets the changed (uncommitted) VM startup options. Must be called on the GUI thread. /// /// public Dictionary GetChangedStartupOptions() { Program.AssertOnEventThread(); Dictionary result = new Dictionary(); foreach (var curRow in dataGridViewVms.Rows.Cast()) { if (curRow.HasChanged()) result[curRow.Vm] = new VMStartupOptions(curRow.StartOrder, curRow.StartDelay, curRow.RestartPriority ?? VM.HA_Restart_Priority.BestEffort); } return result; } private void linkLabelTellMeMore_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) { Help.HelpManager.Launch("HaNtolZero"); } #region XentabPage overrides public override string Text { get { return Messages.HAWIZARD_ASSIGNPRIORITIESPAGE_TEXT; } } public override string PageTitle { get { return Messages.HAWIZARD_ASSIGNPRIORITIESPAGE_TITLE; } } public override bool EnableNext() { return !haNtolIndicator.UpdateInProgress && Ntol >= 0 && !haNtolIndicator.Overcommitted; } public override void PageCancelled() { StopNtolUpdate(); } public override void PageLoaded(PageLoadedDirection direction) { base.PageLoaded(direction); StartNtolUpdate(); } public override void SelectDefaultControl() { dataGridViewVms.Select(); } public override void PageLeave(PageLoadedDirection direction, ref bool cancel) { StopNtolUpdate(); base.PageLeave(direction, ref cancel); } #endregion #region Nested classes private class VmWithSettingsRow : DataGridViewRow { private readonly DataGridViewImageCell cellImage; private readonly DataGridViewTextBoxCell cellVm; private readonly DataGridViewTextBoxCell cellRestartPriority; private readonly DataGridViewTextBoxCell cellStartOrder; private readonly DataGridViewTextBoxCell cellDelay; private readonly DataGridViewTextBoxCell cellAgile; private string _nonAgileReason; public VM Vm { get; private set; } public long StartDelay { get; private set; } public long StartOrder { get; private set; } public VM.HA_Restart_Priority? RestartPriority { get; private set; } public bool IsAgile { get; private set; } /// /// Returns a short version of the agility violation for the error type stored in NonAgileReason. /// Returns empty string if there is no error. /// For a more detailed, technical description use the error translation in FriendlyErrorNames.resx /// public string FriendlyNonAgileReason { get { switch (_nonAgileReason) { case null: return ""; case "HA_CONSTRAINT_VIOLATION_SR_NOT_SHARED": return Messages.NOT_AGILE_SR_NOT_SHARED; case "HA_CONSTRAINT_VIOLATION_NETWORK_NOT_SHARED": return Messages.NOT_AGILE_NETWORK_NOT_SHARED; case "VM_HAS_VGPU": return Messages.NOT_AGILE_VM_HAS_VGPU; default: // We shouldn't really be here unless we have not iterated all the return errors from vm.assert_agile return Messages.NOT_AGILE_UNKOWN; } } set { _nonAgileReason = value; } } public VmWithSettingsRow(VM vm, VM.HA_Restart_Priority? priority) { cellImage = new DataGridViewImageCell {ValueType = typeof(Image)}; cellVm = new DataGridViewTextBoxCell(); cellRestartPriority = new DataGridViewTextBoxCell(); cellStartOrder = new DataGridViewTextBoxCell(); cellDelay = new DataGridViewTextBoxCell(); cellAgile = new DataGridViewTextBoxCell(); Cells.AddRange(cellImage, cellVm, cellRestartPriority, cellStartOrder, cellDelay, cellAgile); UpdateVm(vm); UpdateRestartPriority(priority); UpdateStartOrder(vm.order); UpdateStartDelay(vm.start_delay); SetAgileCalculating(); } public void UpdateVm(VM vm) { Vm = vm; cellImage.Value = Images.GetImage16For(vm); cellVm.Value = vm.Name(); } public void UpdateStartDelay(long startDelay) { StartDelay = startDelay; cellDelay.Value = string.Format(Messages.TIME_SECONDS, startDelay); } public void UpdateStartOrder(long startOrder) { StartOrder = startOrder; cellStartOrder.Value = startOrder.ToString(); } public void UpdateRestartPriority(VM.HA_Restart_Priority? restartPriority) { RestartPriority = restartPriority; cellRestartPriority.Value = Helpers.RestartPriorityI18n(restartPriority); } public void UpdateAgile(bool isAgile) { IsAgile = isAgile; cellAgile.Value = isAgile.ToYesNoStringI18n(); } public void SetAgileCalculating() { cellAgile.Value = Messages.HA_CALCULATING_AGILITY; } public bool HasChanged() { return Vm.order != StartOrder || Vm.start_delay != StartDelay || Vm.HARestartPriority() != RestartPriority; } } #endregion } }