/* 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.Drawing; using System.Linq; using System.Windows.Forms; using XenAdmin.Controls; using XenAdmin.Core; using XenAdmin.Actions; using XenAPI; using XenAdmin.Dialogs; using XenAdmin.Commands; namespace XenAdmin.TabPages { internal partial class SrStoragePage : BaseTabPage { private SR sr; private readonly DataGridViewColumn sizeColumn; private readonly DataGridViewColumn storageLinkVolumeColumn; private bool rebuildRequired; private readonly VDIsDataGridViewBuilder dataGridViewBuilder; public SrStoragePage() { InitializeComponent(); storageLinkVolumeColumn = ColumnVolume; sizeColumn = ColumnSize; for (int i = 0; i < 5; i++) { dataGridViewVDIs.Columns[i].SortMode = DataGridViewColumnSortMode.Automatic; } dataGridViewVDIs.SortCompare += DataGridViewObject_SortCompare; dataGridViewVDIs.SelectionChanged += dataGridViewVDIs_SelectedIndexChanged; dataGridViewVDIs.MouseUp += dataGridViewVDIs_MouseUp; dataGridViewVDIs.KeyUp += dataGridViewVDIs_KeyUp; ConnectionsManager.History.CollectionChanged += new CollectionChangeEventHandler(History_CollectionChanged); base.Text = Messages.VIRTUAL_DISKS; Properties.Settings.Default.PropertyChanged += Default_PropertyChanged; dataGridViewBuilder = new VDIsDataGridViewBuilder(this); } private bool disposed; /// /// Clean up any resources being used. /// /// true if managed resources should be disposed; otherwise, false. protected override void Dispose(bool disposing) { if (!disposed) { // Deregister listeners. SR = null; ConnectionsManager.History.CollectionChanged -= History_CollectionChanged; Properties.Settings.Default.PropertyChanged -= Default_PropertyChanged; if (disposing) { if (components != null) components.Dispose(); dataGridViewBuilder.Stop(); } disposed = true; } base.Dispose(disposing); } private void SetupDeprecationBanner() { if (sr != null && SR.IsIslOrIslLegacy(sr)) { Banner.AppliesToVersion = Messages.XENSERVER_6_5; Banner.BannerType = DeprecationBanner.Type.Removal; Banner.FeatureName = Messages.ISL_SR; Banner.LinkUri = new Uri(InvisibleMessages.ISL_DEPRECATION_URL); Banner.Visible = true; } else Banner.Visible = false; } public SR SR { set { Program.AssertOnEventThread(); if (sr != null) { sr.PropertyChanged -= sr_PropertyChanged; sr.Connection.Cache.DeregisterBatchCollectionChanged(VDI_BatchCollectionChanged); sr.Connection.XenObjectsUpdated -= Connection_XenObjectsUpdated; } sr = value; if (sr != null) { sr.PropertyChanged += sr_PropertyChanged; sr.Connection.Cache.RegisterBatchCollectionChanged(VDI_BatchCollectionChanged); addVirtualDiskButton.Visible = sr.SupportsVdiCreate(); sr.Connection.XenObjectsUpdated += Connection_XenObjectsUpdated; } BuildList(true); SetupDeprecationBanner(); } } private void RefreshDataGridView(VDIsData data) { dataGridViewVDIs.SuspendLayout(); try { storageLinkVolumeColumn.Visible = data.ShowStorageLink; // Update existing rows foreach (var vdiRow in data.VdiRowsToUpdate) { vdiRow.RefreshRowDetails(data.ShowStorageLink); } // Remove rows for deleted VDIs foreach (var vdiRow in data.VdiRowsToRemove) { dataGridViewVDIs.Rows.RemoveAt(vdiRow.Index); } // Add rows for new VDIs foreach (var vdi in data.VdisToAdd) { VDIRow newRow = new VDIRow(vdi, data.ShowStorageLink); dataGridViewVDIs.Rows.Add(newRow); } } finally { if (dataGridViewVDIs.SortedColumn != null && dataGridViewVDIs.SortOrder != SortOrder.None) dataGridViewVDIs.Sort(dataGridViewVDIs.SortedColumn, dataGridViewVDIs.SortOrder == SortOrder.Ascending ? ListSortDirection.Ascending : ListSortDirection.Descending); dataGridViewVDIs.ResumeLayout(); } RefreshButtons(); } private IEnumerable GetCurrentVDIRows() { return dataGridViewVDIs.Rows.OfType(); } private void BuildList(bool reset) { Program.AssertOnEventThread(); if (sr == null) return; dataGridViewBuilder.AddRequest(new RefreshGridRequest(sr, reset)); } private SelectedItemCollection SelectedVDIs { get { List vdis = new List(); foreach (DataGridViewRow r in dataGridViewVDIs.SelectedRows) { VDIRow row = r as VDIRow; if (row != null) vdis.Add(new SelectedItem(row.VDI)); } return new SelectedItemCollection(vdis); } } #region events void History_CollectionChanged(object sender, CollectionChangeEventArgs e) { Program.BeginInvoke(Program.MainWindow, () => { SrRefreshAction a = e.Element as SrRefreshAction; if (a == null) return; if (e.Action == CollectionChangeAction.Add) a.Completed += a_Completed; if (e.Action == CollectionChangeAction.Remove) a.Completed -= a_Completed; RefreshButtons(); }); } void a_Completed(ActionBase sender) { Program.Invoke(Program.MainWindow, RefreshButtons); } void Connection_XenObjectsUpdated(object sender, EventArgs e) { if (rebuildRequired) BuildList(false); rebuildRequired = false; } void sr_PropertyChanged(object sender1, PropertyChangedEventArgs e) { if (e.PropertyName == "VDIs") rebuildRequired = true; } private void VDI_BatchCollectionChanged(object sender, EventArgs e) { rebuildRequired = true; } private void Default_PropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName != "ShowHiddenVMs") return; Program.Invoke(this, () => BuildList(false)); } #endregion #region datagridviewevents void DataGridViewObject_SortCompare(object sender, DataGridViewSortCompareEventArgs e) { if (e.Column.Index == sizeColumn.Index) { VDI vdi1 = ((VDIRow)dataGridViewVDIs.Rows[e.RowIndex1]).VDI; VDI vdi2 = ((VDIRow)dataGridViewVDIs.Rows[e.RowIndex2]).VDI; long diff = vdi1.virtual_size - vdi2.virtual_size; e.SortResult = diff > 0 ? 1 : diff < 0 ? -1 : 0; e.Handled = true; } } private void dataGridViewVDIs_SelectedIndexChanged(object sender, EventArgs e) { RefreshButtons(); } private void dataGridViewVDIs_KeyUp(object sender, KeyEventArgs e) { if (e.KeyCode != Keys.Apps) return; if (dataGridViewVDIs.SelectedRows.Count <= 0) return; DataGridViewRow row = dataGridViewVDIs.SelectedRows[0]; contextMenuStrip1.Show(dataGridViewVDIs, 5, row.Height * (row.Index + 2)); } private void dataGridViewVDIs_MouseUp(object sender, MouseEventArgs e) { // Load context menus on right mouse click DataGridView.HitTestInfo hitTestInfo; if (e.Button == MouseButtons.Right) { hitTestInfo = dataGridViewVDIs.HitTest(e.X, e.Y); if (hitTestInfo.Type == DataGridViewHitTestType.Cell) { if (dataGridViewVDIs.Rows.Count >= 0) { if (!dataGridViewVDIs.Rows[hitTestInfo.RowIndex].Selected) { // Select the row that the user right clicked on (similiar to outlook) if it's not already in the selection // (avoids clearing a multiselect if you right click inside it) dataGridViewVDIs.CurrentCell = dataGridViewVDIs[hitTestInfo.ColumnIndex, hitTestInfo.RowIndex]; } // Show the menus. We will always have at least one (Add). contextMenuStrip1.Show(dataGridViewVDIs, new Point(e.X, e.Y)); } } } } #endregion #region Button and context menu population private void contextMenuStrip_Opening(object sender, CancelEventArgs e) { SelectedItemCollection vdis = SelectedVDIs; if (vdis.Count == 0) return; contextMenuStrip1.Items.Clear(); DeleteVirtualDiskCommand deleteCmd = new DeleteVirtualDiskCommand(Program.MainWindow, vdis); contextMenuStrip1.Items.Add(new CommandToolStripMenuItem(deleteCmd, true)); contextMenuStrip1.Items.Add(new CommandToolStripMenuItem(MoveMigrateCommand(vdis), true)); contextMenuStrip1.Items.Add(editVirtualDiskToolStripMenuItem); editVirtualDiskToolStripMenuItem.Enabled = vdis.Count == 1 && !vdis.AsXenObjects()[0].is_a_snapshot; } private Command MoveMigrateCommand(IEnumerable selection) { MoveVirtualDiskCommand moveCmd = new MoveVirtualDiskCommand(Program.MainWindow, selection); if (moveCmd.CanExecute()) return moveCmd; return new MigrateVirtualDiskCommand(Program.MainWindow, selection); } private void RefreshButtons() { bool srLocked = sr == null || sr.Locked; SelectedItemCollection vdis = SelectedVDIs; // Delete button DeleteVirtualDiskCommand deleteCmd = new DeleteVirtualDiskCommand(Program.MainWindow, vdis); // User has visibility that this disk is related to all it's VM. Allow deletion with multiple VBDs (non default behaviour), // but don't allow them to delete if a running vm is using the disk (default behaviour). deleteCmd.AllowMultipleVBDDelete = true; if (deleteCmd.CanExecute()) { RemoveButton.Enabled = true; RemoveButtonContainer.RemoveAll(); } else { RemoveButton.Enabled = false; RemoveButtonContainer.SetToolTip(deleteCmd.ToolTipText); } // Move button Command moveCmd = MoveMigrateCommand(vdis); if (moveCmd.CanExecute()) { buttonMove.Enabled = true; toolTipContainerMove.RemoveAll(); } else { buttonMove.Enabled = false; toolTipContainerMove.SetToolTip(moveCmd.ToolTipText); } // Rescan button if (srLocked) { buttonRefresh.Enabled = false; } else if (HelpersGUI.BeingScanned(sr)) { buttonRefresh.Enabled = false; toolTipContainerRescan.SetToolTip(Messages.SCAN_IN_PROGRESS_TOOLTIP); } else { buttonRefresh.Enabled = true; toolTipContainerRescan.RemoveAll(); } // Add VDI button addVirtualDiskButton.Enabled = !srLocked; // Properties button if (vdis.Count == 1) { VDI vdi = vdis.AsXenObjects()[0]; EditButton.Enabled = !srLocked && !vdi.is_a_snapshot && !vdi.Locked; } else EditButton.Enabled = false; } #endregion #region buttonHandlers private void removeVirtualDisk_Click(object sender, EventArgs e) { SelectedItemCollection vdis = SelectedVDIs; DeleteVirtualDiskCommand cmd = new DeleteVirtualDiskCommand(Program.MainWindow, vdis); cmd.AllowMultipleVBDDelete = true; if (cmd.CanExecute()) cmd.Execute(); } private void editVirtualDiskToolStripMenuItem_Click(object sender, EventArgs e) { SelectedItemCollection vdis = SelectedVDIs; if (vdis.Count != 1) return; VDI vdi = vdis.AsXenObjects()[0]; if (vdi.is_a_snapshot) return; new PropertiesDialog(vdi).ShowDialog(this); } private void addVirtualDiskButton_Click(object sender, EventArgs e) { if (sr != null) Program.MainWindow.ShowPerConnectionWizard(sr.Connection, new Dialogs.NewDiskDialog(sr.Connection, sr)); } private void RemoveButton_Click(object sender, EventArgs e) { removeVirtualDisk_Click(sender, e); } private void EditButton_Click(object sender, EventArgs e) { editVirtualDiskToolStripMenuItem_Click(sender, e); } private void buttonRefresh_Click(object sender, EventArgs e) { SrRefreshAction a = new SrRefreshAction(sr); a.RunAsync(); } private void moveVirtualDiskToolStripMenuItem_Click(object sender, EventArgs e) { SelectedItemCollection vdis = SelectedVDIs; Command cmd = MoveMigrateCommand(vdis); if (cmd.CanExecute()) cmd.Execute(); } private void buttonMove_Click(object sender, EventArgs e) { moveVirtualDiskToolStripMenuItem_Click(sender, e); } #endregion private void dataGridViewVDIs_CellMouseUp(object sender, DataGridViewCellMouseEventArgs e) { // Select the row that the user right clicked on if it's not already in the selection if (e.Button == MouseButtons.Right && e.RowIndex >= 0) { if (!dataGridViewVDIs.Rows[e.RowIndex].Selected) { // Check if the CurrentCell is the cell the user right clicked on (but the row is not Selected) [CA-64954] // This happens when the grid is initially shown: the current cell is the first cell in the first column, but the row is not selected if (dataGridViewVDIs.CurrentCell == dataGridViewVDIs[e.ColumnIndex, e.RowIndex]) dataGridViewVDIs.Rows[e.RowIndex].Selected = true; else dataGridViewVDIs.CurrentCell = dataGridViewVDIs[e.ColumnIndex, e.RowIndex]; } } } public class VDIRow : DataGridViewRow { public VDI VDI { get; private set; } private bool showStorageLink = false; public VDIRow(VDI vdi, bool showStorageLink) { this.showStorageLink = showStorageLink; VDI = vdi; for (int i = 0; i < 5; i++) { Cells.Add(new DataGridViewTextBoxCell()); Cells[i].Value = GetCellText(i); } } private string GetCellText(int cellIndex) { switch (cellIndex) { case 0: return VDI.Name; case 1: string name; return VDI.sm_config.TryGetValue("displayname", out name) ? name : ""; case 2: return VDI.Description; case 3: return VDI.SizeText; case 4: return VDI.VMsOfVDI; default: return ""; } } public void RefreshRowDetails(bool showSL) { showStorageLink = showSL; for (int i = 0; i < 5; i++) { Cells[i].Value = GetCellText(i); } } } public struct VDIsData { public List VdiRowsToUpdate { get; private set; } public List VdiRowsToRemove { get; private set; } public List VdisToAdd { get; private set; } public bool ShowStorageLink { get; private set; } public VDIsData(List vdiRowsToUpdate, List vdiRowsToRemove, List vdisToAdd, bool showStorageLink) : this() { VdiRowsToUpdate = vdiRowsToUpdate; VdiRowsToRemove = vdiRowsToRemove; VdisToAdd = vdisToAdd; ShowStorageLink = showStorageLink; } } public class RefreshGridRequest { public SR SR { get; private set; } public bool Reset { get; private set; } public RefreshGridRequest(SR sr, bool reset) { SR = sr; Reset = reset; } } public class VDIsDataGridViewBuilder { private readonly Control owner; private readonly object _locker = new object(); private BackgroundWorker worker; private Queue queue = new Queue(); public VDIsDataGridViewBuilder(Control owner) { this.owner = owner; worker = new BackgroundWorker {WorkerSupportsCancellation = true}; worker.DoWork += DoWork; worker.RunWorkerCompleted += (RunWorkerCompleted); } private void RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { if (e.Cancelled || e.Error != null) return; lock (_locker) { if (queue.Count > 0 && !worker.IsBusy) worker.RunWorkerAsync(); } } private VDIsData GetCurrentData(SR sr, IEnumerable currentVDIRows) { List vdis = sr.Connection.ResolveAll(sr.VDIs).Where( vdi => vdi.Show(Properties.Settings.Default.ShowHiddenVMs) && !vdi.IsAnIntermediateStorageMotionSnapshot) .ToList(); bool showStorageLink = vdis.Find(v => v.sm_config.ContainsKey("SVID")) != null; var vdiRowsToRemove = currentVDIRows.Where(vdiRow => !vdis.Contains(vdiRow.VDI)).OrderByDescending(row => row.Index).ToList(); var vdiRowsToUpdate = currentVDIRows.Except(vdiRowsToRemove).ToList(); var vdisToAdd = vdis.Except(vdiRowsToUpdate.ConvertAll(vdiRow => vdiRow.VDI)).ToList(); return new VDIsData(vdiRowsToUpdate, vdiRowsToRemove, vdisToAdd, showStorageLink); } private void DoWork(object sender, DoWorkEventArgs e) { RefreshGridRequest refreshRequest; lock (_locker) { refreshRequest = queue.Count > 0 ? queue.Dequeue() : null; } if (worker.CancellationPending) return; if (refreshRequest == null || refreshRequest.SR == null) return; SrStoragePage page = owner as SrStoragePage; if (page == null) return; if (refreshRequest.Reset) Program.Invoke(owner, page.dataGridViewVDIs.Rows.Clear); Program.Invoke(owner, page.RefreshButtons); IEnumerable currentVDIRows = Enumerable.Empty(); Program.Invoke(owner, () => currentVDIRows = page.GetCurrentVDIRows()); VDIsData data = GetCurrentData(refreshRequest.SR, currentVDIRows); if (worker.CancellationPending) return; Program.Invoke(owner, () => ((SrStoragePage)owner).RefreshDataGridView(data)); } public void AddRequest(RefreshGridRequest refreshGridRequest) { lock (_locker) { if (refreshGridRequest.Reset) queue.Clear(); queue.Enqueue(refreshGridRequest); if (!worker.IsBusy) worker.RunWorkerAsync(); } } public void Stop() { if (!worker.CancellationPending) worker.CancelAsync(); } } } }