/* 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();
}
}
}
}