/* Copyright (c) Cloud Software Group, Inc. * * 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; using XenCenterLib; namespace XenAdmin.TabPages { internal partial class SrStoragePage : BaseTabPage { private SR sr; private bool rebuildRequired; private readonly VDIsDataGridViewBuilder dataGridViewBuilder; public SrStoragePage() { InitializeComponent(); for (int i = 0; i < 5; i++) { dataGridViewVDIs.Columns[i].SortMode = DataGridViewColumnSortMode.Automatic; } ConnectionsManager.History.CollectionChanged += History_CollectionChanged; base.Text = Messages.VIRTUAL_DISKS; Properties.Settings.Default.PropertyChanged += Default_PropertyChanged; dataGridViewBuilder = new VDIsDataGridViewBuilder(this); } public override string HelpID => "TabPageStorage"; 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() { 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 { ColumnVolume.Visible = data.ShowStorageLink; var showCbtColumn = !Helpers.FeatureForbidden(sr.Connection, Host.RestrictChangedBlockTracking); ColumnCBT.Visible = showCbtColumn; // Update existing rows foreach (var vdiRow in data.VdiRowsToUpdate) { vdiRow.RefreshRowDetails(showCbtColumn); } // 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) { dataGridViewVDIs.Rows.Add(new VDIRow(vdi, showCbtColumn)); } } 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 void UnregisterHandlers() { ConnectionsManager.History.CollectionChanged -= History_CollectionChanged; Properties.Settings.Default.PropertyChanged -= Default_PropertyChanged; if (sr != null) { sr.PropertyChanged -= sr_PropertyChanged; sr.Connection.Cache.DeregisterBatchCollectionChanged(VDI_BatchCollectionChanged); sr.Connection.XenObjectsUpdated -= Connection_XenObjectsUpdated; } } public override void PageHidden() { UnregisterHandlers(); } #region events void History_CollectionChanged(object sender, CollectionChangeEventArgs e) { Program.BeginInvoke(Program.MainWindow, () => { SrRefreshAction a = e.Element as SrRefreshAction; if (e.Action == CollectionChangeAction.Add) { if (a != null) a.Completed += a_Completed; else return; } if (e.Action == CollectionChangeAction.Remove) { if (a != null) { a.Completed -= a_Completed; } else { var range = e.Element as List; if (range != null) { foreach (var sra in range) sra.Completed -= a_Completed; } else { return; } } } 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 datagridvie wevents private void DataGridViewObject_SortCompare(object sender, DataGridViewSortCompareEventArgs e) { if (e.Column.Index == ColumnName.Index) { var vdi1 = ((VDIRow) dataGridViewVDIs.Rows[e.RowIndex1]).VDI; var vdi2 = ((VDIRow) dataGridViewVDIs.Rows[e.RowIndex2]).VDI; e.SortResult = vdi1.CompareTo(vdi2); e.Handled = true; return; } if (e.Column.Index == ColumnDesc.Index) { var vdi1 = ((VDIRow)dataGridViewVDIs.Rows[e.RowIndex1]).VDI; var vdi2 = ((VDIRow)dataGridViewVDIs.Rows[e.RowIndex2]).VDI; var descCompare = StringUtility.NaturalCompare(vdi1.Description(), vdi2.Description()); if (descCompare != 0) { e.SortResult = descCompare; } else { var refCompare = string.Compare(vdi1.opaque_ref, vdi2.opaque_ref, StringComparison.Ordinal); e.SortResult = refCompare; } e.Handled = true; return; } if (e.Column.Index == ColumnSize.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) { // 3 is the defaul control margin contextMenuStrip1.Show(dataGridViewVDIs, 3, dataGridViewVDIs.ColumnHeadersHeight + 3); } else { DataGridViewRow row = dataGridViewVDIs.SelectedRows[0]; contextMenuStrip1.Show(dataGridViewVDIs, 3, row.Height * (row.Index + 2)); } } private void dataGridViewVDIs_MouseUp(object sender, MouseEventArgs e) { DataGridView.HitTestInfo hitTestInfo = dataGridViewVDIs.HitTest(e.X, e.Y); if (hitTestInfo.Type == DataGridViewHitTestType.None) { dataGridViewVDIs.ClearSelection(); } else if (hitTestInfo.Type == DataGridViewHitTestType.Cell && e.Button == MouseButtons.Right && 0 <= hitTestInfo.RowIndex && hitTestInfo.RowIndex < dataGridViewVDIs.Rows.Count && !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) // 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[hitTestInfo.ColumnIndex, hitTestInfo.RowIndex]) dataGridViewVDIs.Rows[hitTestInfo.RowIndex].Selected = true; else dataGridViewVDIs.CurrentCell = dataGridViewVDIs[hitTestInfo.ColumnIndex, hitTestInfo.RowIndex]; } if ((hitTestInfo.Type == DataGridViewHitTestType.None || hitTestInfo.Type == DataGridViewHitTestType.Cell) && e.Button == MouseButtons.Right) { contextMenuStrip1.Show(dataGridViewVDIs, new Point(e.X, e.Y)); } } #endregion #region Button and context menu population private void contextMenuStrip_Opening(object sender, CancelEventArgs e) { bool rescan = buttonRescan.Enabled; bool add = addVirtualDiskButton.Enabled; bool move = buttonMove.Enabled; bool delete = RemoveButton.Enabled; bool edit = EditButton.Enabled; if (!(rescan || add || move || delete || edit)) { e.Cancel = true; return; } rescanToolStripMenuItem.Visible = rescan; addToolStripMenuItem.Visible = add; moveVirtualDiskToolStripMenuItem.Visible = move; deleteVirtualDiskToolStripMenuItem.Visible = delete; editVirtualDiskToolStripMenuItem.Visible = edit; toolStripSeparator1.Visible = (rescan || add || move || delete) && edit; } private void RefreshButtons() { var vdis = dataGridViewVDIs.SelectedRows.Cast().Select(r => new SelectedItem(r.VDI)).ToList(); // Delete button // The user can see that this disk is attached to more than one VMs. Allow deletion of multiple VBDs (non default behaviour), // but don't allow them to be deleted if a running vm is using the disk (default behaviour). RemoveButton.Command = new DeleteVirtualDiskCommand(Program.MainWindow, vdis) {AllowMultipleVBDDelete = true}; // Move button buttonMove.Command = MoveVirtualDiskDialog.MoveMigrateCommand(Program.MainWindow, new SelectedItemCollection(vdis)); // Rescan button if (sr == null || sr.Locked) { buttonRescan.Enabled = false; } else if (sr.IsDetached()) { buttonRescan.Enabled = false; toolTipContainerRescan.SetToolTip(Messages.SR_DETACHED); } else if (HelpersGUI.BeingScanned(sr, out _)) { buttonRescan.Enabled = false; toolTipContainerRescan.SetToolTip(Messages.SCAN_IN_PROGRESS_TOOLTIP); } else { buttonRescan.Enabled = true; toolTipContainerRescan.RemoveAll(); } // Add VDI button addVirtualDiskButton.Enabled = sr != null && !sr.Locked; // Properties button EditButton.Enabled = vdis.Count == 1 && vdis[0].XenObject is VDI vdi && sr != null && !sr.Locked && !vdi.is_a_snapshot && !vdi.Locked; EditButton.Tag = EditButton.Enabled ? vdis[0].XenObject as VDI : null; } #endregion #region Actions on Vdis private void Rescan() { SrRefreshAction a = new SrRefreshAction(sr); a.RunAsync(); } private void AddVdi() { if (sr != null) Program.MainWindow.ShowPerConnectionWizard(sr.Connection, new NewDiskDialog(sr.Connection, sr)); } private void EditSelectedVdis() { if (EditButton.Tag is VDI vdi) using (var dlg = new PropertiesDialog(vdi)) dlg.ShowDialog(this); } #endregion #region Button and ToolStripMenuItem handlers private void addVirtualDiskButton_Click(object sender, EventArgs e) { AddVdi(); } private void EditButton_Click(object sender, EventArgs e) { EditSelectedVdis(); } private void buttonRescan_Click(object sender, EventArgs e) { Rescan(); } private void rescanToolStripMenuItem_Click(object sender, EventArgs e) { Rescan(); } private void addToolStripMenuItem_Click(object sender, EventArgs e) { AddVdi(); } private void editVirtualDiskToolStripMenuItem_Click(object sender, EventArgs e) { EditSelectedVdis(); } private void moveVirtualDiskToolStripMenuItem_Click(object sender, EventArgs e) { if (buttonMove.Enabled) buttonMove.PerformClick(); } private void deleteVirtualDiskToolStripMenuItem_Click(object sender, EventArgs e) { if (RemoveButton.Enabled) RemoveButton.PerformClick(); } #endregion public class VDIRow : DataGridViewRow { public VDI VDI { get; private set; } public VDIRow(VDI vdi, bool show_cbt) { VDI = vdi; for (int i = 0; i < 5; i++) { Cells.Add(new DataGridViewTextBoxCell()); Cells[i].Value = GetCellText(i); } if (show_cbt) { Cells.Add(new DataGridViewTextBoxCell()); Cells[5].Value = GetCellText(5); } } 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(); case 5: return VDI.cbt_enabled ? Messages.ENABLED : Messages.DISABLED; default: return ""; } } public void RefreshRowDetails(bool show_cbt) { for (int i = 0; i < 5; i++) { Cells[i].Value = GetCellText(i); } if (show_cbt) Cells[5].Value = GetCellText(5); } } 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(); } } } }