xenadmin/XenModel/ServerDBs/DbProxy.cs
Konstantina Chremmou ada6f7dac7 CA-296490: Upload single update to multiple pools in parallel. (#2338)
* Simplification: use auto-properties.

* Removed unnecessary null check. Converted private methods to linq method chain.

* Removed unused file. Added null check.

* Corrected property names as it was difficult to distinguish between the alert,
the patch and the filepath.

* Removed duplicate lock.

* CA-296490: Rewrote the logic uploading single updates so as to facilitate uploading to multiple pools in parallel.

* Corrections as per code review.

* Account for the case of uploading from disk legacy updates that do not correspond to an update alert.
Removed unfriendly error messages (the failure is written out in the logs anyway).

* Wizard test corrections and some modernising:
- Only call cancel on the last page if the test allows us to.
- Do finish the UpdatesWizard test.
- Finish the RunWizardKeyboardTests using key strokes instead of button clicks.
- Ensure the Cancel/Previous buttons do not have focus when sending an Enter stroke.
- Use optional parameters in the WizardTest constructor and string interpolation.

* Renamed badly named property.

* Improved code cloning VMs by assigning new opaque_refs to cloned VBDs.
Compacted test object provider: use optional parameters instead of method overloads.

* Modifications to prevent multiple update of enabled state and ensure the wizard
buttons are updated last.

Signed-off-by: Konstantina Chremmou <konstantina.chremmou@citrix.com>
2019-01-10 13:24:42 +00:00

708 lines
27 KiB
C#

/* 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;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using XenAPI;
using XenAdmin.Network;
using XenAdmin.ServerDBs.FakeAPI;
using XenAdmin.Core;
namespace XenAdmin.ServerDBs
{
public class DbProxy : IInvocationHandler
{
private static readonly ClassGenerator generator = new ClassGenerator(typeof(Proxy));
private static readonly Type proxyType = generator.Generate();
public static readonly Proxy_Event dummyEvent = new Proxy_Event();
public static readonly Dictionary<IXenConnection, DbProxy> proxys = new Dictionary<IXenConnection, DbProxy>();
private static readonly object dicLock = new object();
public readonly Db db;
public readonly List<Proxy_Event> eventsList = new List<Proxy_Event>();
public readonly object eventsListLock = new object();
private readonly string url;
public readonly IXenConnection connection;
public volatile bool MarkToDisconnect;
/// Set this delegate to run inside the event loop.
/// Throwing an specific exception type or Failure will simulate the termination
/// of the event loop mid-loop
public Action RunInEventLoop { get; private set; }
private static readonly Dictionary<string, List<string>> callVersions = new Dictionary<string, List<string>>();
public bool IsSuperUser { get; private set; }
public void SetIsSuperUser(bool isSuperUser)
{
IsSuperUser = isSuperUser;
}
public static void RemoveAll()
{
lock (dicLock)
{
foreach (KeyValuePair<IXenConnection, DbProxy> kvp in proxys)
{
kvp.Value.db.Dispose();
kvp.Value.RunInEventLoop = delegate()
{
throw new Failure(Failure.SESSION_INVALID);
};
}
proxys.Clear();
}
}
/// <summary>
/// Initializes the <see cref="DbProxy"/> class.
/// </summary>
static DbProxy()
{
dummyEvent.class_ = "vtpm"; // This will cause XenCenter to ignore the event;
// load the api-version.xml file to detect whether a call is made for the wrong version of the server being simulated.
/*XmlDocument doc = new XmlDocument();
doc.Load(Assembly.GetExecutingAssembly().GetManifestResourceStream("XenAdmin.TestResources.api-version.xml"));
foreach (XmlNode node in doc.SelectNodes("ApiVersionTool/version"))
{
string ver = node.Attributes["name"].Value;
callVersions.Add(ver, new List<string>());
foreach (XmlNode n in node.SelectNodes("call"))
{
callVersions[ver].Add(n.Attributes["name"].Value);
}
}
*/
}
/// <summary>
/// Generates a Proxy instance which implements <see cref="Proxy"/> for the specified <see cref="IXenConnection"/> and XML document.
/// </summary>
/// <param name="connection">The connection.</param>
/// <param name="fileName">Name of the file.</param>
/// <returns></returns>
public static Proxy GetProxy(IXenConnection connection, string fileName)
{
return (Proxy)generator.CreateProxyInstance(proxyType, ProxyInstance(connection, fileName));
}
private static IInvocationHandler ProxyInstance(IXenConnection connection, string fileName)
{
lock (dicLock)
{
if (!proxys.ContainsKey(connection))
{
proxys[connection] = new DbProxy(connection, fileName);
}
return proxys[connection];
}
}
/// <summary>
/// Initializes a new instance of the <see cref="DbProxy"/> class.
/// </summary>
/// <param name="connection">The connection.</param>
/// <param name="url">The http url of filename to the XenServer status report XML document to be used.</param>
public DbProxy(IXenConnection connection, string url)
{
Util.ThrowIfParameterNull(connection, "connection");
Util.ThrowIfStringParameterNullOrEmpty(url, "url");
this.url = url;
this.connection = connection;
db = new Db(connection, url);
}
public object ExecuteMethod(ProxyMethodInfo pmi, object[] args)
{
object obj;
if (pmi.FakeType == typeof(fakeUnknown))
{
obj = new fakeUnknown(pmi.TypeName, this);
}
else
{
obj = Activator.CreateInstance(pmi.FakeType, this);
}
object[] methodArgs = new object[pmi.FakeMethod.GetParameters().Length];
for (int i = 0; i < methodArgs.Length; i++)
{
methodArgs[i] = args[i];
}
switch (pmi.FakeMethodType)
{
case MethodType.Sync:
return pmi.FakeMethod.Invoke(obj, methodArgs);
case MethodType.Async:
Response<string> response = (Response<string>)pmi.FakeMethod.Invoke(obj, methodArgs);
if (response.Status == ResponseTypes.SUCCESS)
{
return new Response<string>(CreateTask(DbProxy.TaskStatus.success, response.Value));
}
else
{
return new Response<string>(CreateTask(DbProxy.TaskStatus.failure, response.ErrorDescription));
}
}
return new Response<String>(true, new String[] { Failure.INTERNAL_ERROR });
}
public event EventHandler<DbProxyInvokingEventArgs> Invoking;
protected virtual void OnInvoking(DbProxyInvokingEventArgs e)
{
var handler = Invoking;
if (handler != null)
{
handler(this, e);
}
}
#region IInvocationHandler Members
public object Invoke(string proxyMethodName, string returnType, params object[] args)
{
VerifyVersion(proxyMethodName);
ProxyMethodInfo pmi = ProxyMethodNameParser.Parse(proxyMethodName);
OnInvoking(new DbProxyInvokingEventArgs(pmi));
if (pmi.FakeType != null)
{
return ExecuteMethod(pmi, args);
}
if (pmi.MethodName.StartsWith("set_"))
{
string fieldname = pmi.MethodName.Substring(4);
// Special case: set_memory_limits is not just editing a "memory_limits" field
if (fieldname == "memory_limits")
{
EditObject(pmi.TypeName, (string)args[1], "memory_static_min", args[2]);
EditObject(pmi.TypeName, (string)args[1], "memory_static_max", args[3]);
EditObject(pmi.TypeName, (string)args[1], "memory_dynamic_min", args[4]);
EditObject(pmi.TypeName, (string)args[1], "memory_dynamic_max", args[5]);
}
else
{
EditObject(pmi.TypeName, (string)args[1], fieldname, args[2]);
}
return new Response<string>("");
}
switch (pmi.MethodName)
{
case "Url":
return url;// "http://XenCenter.Simulator/";
case "get_record":
{
string uuid = (string)args[1];
if (uuid.StartsWith("task")) // tasks are kept track of separately
{
return new Response<Proxy_Task>(task_get_record(uuid));
}
return get_record(pmi.TypeName, uuid, true);
}
case "destroy":
{
string uuid = (string)args[1];
if (uuid.StartsWith("task"))
{
task_destroy(uuid);
return new Response<string>("");
}
break;
}
case "add_to_other_config":
{
string uuid = (string)args[1];
if (!uuid.StartsWith("task")) // ignore other_config for tasks (used to keep track of meddling actions)
{
AddToDictionary(pmi.TypeName, (string)args[1], "other_config", args[2], args[3]);
}
return new Response<string>("");
}
case "remove_from_other_config":
{
string uuid = (string)args[1];
if (!uuid.StartsWith("task")) // ignore other_config for tasks (used to keep track of meddling actions)
{
RemoveFromDictionary(pmi.TypeName, (string)args[1], "other_config", args[2]);
}
return new Response<string>("");
}
case "add_to_gui_config":
AddToDictionary(pmi.TypeName, (string)args[1], "gui_config", args[2], args[3]);
return new Response<string>("");
case "remove_from_gui_config":
RemoveFromDictionary(pmi.TypeName, (string)args[1], "gui_config", args[2]);
return new Response<string>("");
case "add_tags":
AddToArray(pmi.TypeName, (string)args[1], "tags", args[2]);
return new Response<string>("");
case "remove_tags":
RemoveFromArray(pmi.TypeName, (string)args[1], "tags", args[2]);
return new Response<string>("");
case "get_subject_information_from_identifier":
Hashtable subjectInfo = new Hashtable();
if ((string)args[1] == "SID1")
{
subjectInfo["subject-name"] = @"citrix\tu_one";
subjectInfo["subject-displayname"] = "Test User 1";
subjectInfo["subject-is-group"] = "false";
}
else if ((string)args[1] == "SID2")
{
subjectInfo["subject-name"] = @"citrix\tu_two";
subjectInfo["subject-displayname"] = "Test User 2";
subjectInfo["subject-is-group"] = "false";
}
else if ((string)args[1] == "SID3")
{
subjectInfo["subject-name"] = @"citrix\tg";
subjectInfo["subject-displayname"] = "Test Group";
subjectInfo["subject-is-group"] = "true";
}
return new Response<Object>(subjectInfo);
// For VBDEditPage.CalculateDevicePositions()
case "get_allowed_vbd_devices":
return new Response<String[]>(new String[] { "0", "1", "2", "3", "4", "5", "6", "7" });
case "retrieve_wlb_recommendations":
case "retrieve_wlb_evacuate_recommendations":
case "retrieve_wlb_configuration":
Hashtable tbl = new Hashtable();
return new Response<object>(tbl);
case "get_uncooperative_resident_vms":
return new Response<string[]>(new string[0]);
// For HAWizard_Pages.AssignPriorities
case "ha_compute_hypothetical_max_host_failures_to_tolerate":
return new Response<string>("1");
case "get_ha_host_failures_to_tolerate":
return new Response<string>("1");
// For EvacuateHostDialog and RollingUpgradeWizard
case "get_vms_which_prevent_evacuation":
return new Response<object>(new Hashtable());
// For RollingUpgradeWizard
case "get_live":
return new Response<bool>(true);
case "get_allowed_operations":
return new Response<string[]>(new string[0]);
case "get_health_check_config":
string uuid1 = (string)args[1];
return new Response<object>(get_health_check_config(pmi.TypeName, uuid1, true));
}
return new Response<String>(true, new String[] { Failure.INTERNAL_ERROR });
}
#endregion
/// <summary>
/// Determine if the given URL represents a simulator db proxy
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
public static bool IsSimulatorUrl(string url)
{
return url.EndsWith(".db") || url.EndsWith(".xml") || url.EndsWith(".tmp");
}
// Code to create fake asynchronous tasks that just return a known value
private static Dictionary<string, Proxy_Task> tasks = new Dictionary<string, Proxy_Task>();
private readonly Random rand = new Random();
public enum TaskStatus { success, failure, pending }
// If success, use the next argument as the result;
// if !success, use all the subsequent arguments as the error_info.
public string CreateTask(TaskStatus status, params string[] result)
{
Proxy_Task task = new Proxy_Task();
task.uuid = String.Format("task{0}", rand.Next());
task.name_label = task.uuid;
task.name_description = "";
task.created = DateTime.Now;
task.status = status.ToString();
if (status != TaskStatus.pending)
{
task.finished = DateTime.Now;
task.progress = 100.0;
if (status == TaskStatus.success)
{
task.result = result[0];
}
else
{
task.error_info = result;
}
}
tasks.Add(task.uuid, task);
return task.uuid;
}
public Proxy_Task GetTask(string opaque_ref)
{
return tasks[opaque_ref];
}
private static Proxy_Task task_get_record(string uuid)
{
Proxy_Task task;
if (tasks.TryGetValue(uuid, out task))
{
return task;
}
return null;
}
private static bool task_destroy(string uuid)
{
return tasks.Remove(uuid);
}
public enum EditTypes { Replace, AddToDict, RemoveFromDict, AddToArray, RemoveFromArray }
public void EditObject_(EditTypes editType, string typeName, string opaqueRef, string field, params object[] args)
{
// Edit the XML to have the new value
edit_record(editType, typeName, opaqueRef, field, args);
// Make a Proxy object from the new XML
Type proxyT = TypeCache.GetProxyType(typeName);
object proxy = get_record(typeName, opaqueRef, false);
// Make a Proxy_Event representing this edit, and add it to the events queue
lock (eventsListLock)
{
eventsList.Add(MakeProxyEvent(typeName, opaqueRef, "mod", proxyT, proxy));
}
}
private void EditObject(string typeName, string opaqueRef, string field, object new_value)
{
EditObject_(EditTypes.Replace, typeName, opaqueRef, field, new_value);
}
private void AddToDictionary(string typeName, string opaqueRef, string field, object key, object new_value)
{
EditObject_(EditTypes.AddToDict, typeName, opaqueRef, field, key, new_value);
}
private void RemoveFromDictionary(string typeName, string opaqueRef, string field, object key)
{
EditObject_(EditTypes.RemoveFromDict, typeName, opaqueRef, field, key);
}
private void AddToArray(string typeName, string opaqueRef, string field, object value)
{
EditObject_(EditTypes.AddToArray, typeName, opaqueRef, field, value);
}
private void RemoveFromArray(string typeName, string opaqueRef, string field, object value)
{
EditObject_(EditTypes.RemoveFromArray, typeName, opaqueRef, field, value);
}
public static Proxy_Event MakeProxyEvent(string typeName, string opaqueRef, string operation, Type ProxyT, object proxy)
{
Proxy_Event ev = new Proxy_Event();
ev.class_ = typeName;
ev.opaqueRef = opaqueRef;
ev.operation = operation;
ev.snapshot = ProxyToHashtable(ProxyT, proxy);
return ev;
}
public static Hashtable ProxyToHashtable(Type proxyT, object proxy)
{
Hashtable table = new Hashtable();
FieldInfo[] fields = proxyT.GetFields();
foreach (FieldInfo field in fields)
{
table[field.Name] = field.GetValue(proxy);
}
return table;
}
public Object get_record(String clazz, String opaqueRef, bool makeResponse)
{
Db.Table table = db.Tables[clazz];
Type type = table.XapiType;
if (!table.Rows.ContainsKey(opaqueRef))
{
if (makeResponse)
{
return typeof(Response<>).MakeGenericType(type).GetConstructor(new Type[] { typeof(bool), typeof(String[]) })
.Invoke(new Object[] { true, new String[] { Failure.OBJECT_NO_LONGER_EXISTS, opaqueRef } });
}
throw new Exception(Failure.OBJECT_NO_LONGER_EXISTS);
}
Db.Row row = table.Rows[opaqueRef];
Object result = Activator.CreateInstance(type);
foreach (string propName in row.Props.Keys)
{
FieldInfo info = type.GetField(propName, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
info.SetValue(result, row.Props[propName].XapiObjectValue);
}
if (makeResponse)
{
return typeof(Response<>).MakeGenericType(type).GetConstructor(new Type[] { type }).Invoke(new Object[] { result });
}
return result;
}
public Object get_health_check_config(String clazz, String opaqueRef, bool makeResponse)
{
Db.Table table = db.Tables[clazz];
if (!table.Rows.ContainsKey(opaqueRef))
{
if (makeResponse)
return new object();
throw new Exception(Failure.OBJECT_NO_LONGER_EXISTS);
}
return table.Rows[opaqueRef].Props["health_check_config"].XapiObjectValue;
}
private void edit_record(EditTypes editType, string clazz, string opaqueRef, string field, params object[] args)
{
Db.Row row = db.Tables[clazz].Rows[opaqueRef];
Db.Prop prop;
if (row.Props.TryGetValue(field, out prop))
{
prop.XapiObjectValue = NewValue(editType, prop.XapiObjectValue, args);
}
else
{
prop = new Db.Prop(row, field, NewValue(editType, null, args));
row.Props[field] = prop;
}
}
private object NewValue(EditTypes editType, object fieldBefore, object[] args)
{
switch (editType)
{
case EditTypes.Replace:
return args[0];
case EditTypes.AddToDict:
Hashtable ht = fieldBefore == null ? new Hashtable() : (Hashtable)fieldBefore;
ht[args[0]] = args[1];
return ht;
case EditTypes.RemoveFromDict:
if (fieldBefore == null)
{
return new Hashtable();
}
else
{
ht = (Hashtable)fieldBefore;
ht.Remove(args[0]);
return ht;
}
case EditTypes.AddToArray:
// We only support string[] at the moment
string[] arr = fieldBefore == null ? new string[0] : (string[])fieldBefore;
List<string> list = new List<string>(arr);
if (!list.Contains((string)args[0]))
{
list.Add((string)args[0]);
}
return list.ToArray();
case EditTypes.RemoveFromArray:
if (fieldBefore == null)
{
return new string[0];
}
else
{
arr = (string[])fieldBefore;
list = new List<string>(arr);
while (list.Contains((string)args[0]))
{
list.Remove((string)args[0]);
}
return list.ToArray();
}
default:
System.Diagnostics.Trace.Assert(false);
return null;
}
}
public string GetRandomRef(string clazz)
{
string[] refs = new string[db.Tables[clazz].Rows.Keys.Count];
db.Tables[clazz].Rows.Keys.CopyTo(refs, 0);
return refs[rand.Next(0, refs.Length)];
}
public string CopyObject(string clazz, string opaque_ref)
{
if (!db.Tables[clazz].Rows.ContainsKey(opaque_ref))
return Helper.NullOpaqueRef;
Db.Row r = db.Tables[clazz].Rows[opaque_ref].CopyOf();
string new_opaque_ref = CreateOpaqueRef();
r.Props["uuid"].XapiObjectValue = Guid.NewGuid().ToString(); // this object needs a new uuid & ref
if (r.Props.TryGetValue("VBDs", out Db.Prop vbdVal))
{
if (vbdVal.XapiObjectValue is string[] vbds)
r.Props["VBDs"].XapiObjectValue =
vbds.Select(v => $"OpaqueRef:{Guid.NewGuid().ToString()}").ToArray();
}
db.Tables[clazz].Rows.Add(new_opaque_ref, r);
return new_opaque_ref;
}
public void SendCreateObject(string typeName, string opaqueRef)
{
// Make a Proxy object from the new XML
Type proxyT = TypeCache.GetProxyType(typeName);
object proxy = get_record(typeName, opaqueRef, false);
// Make a Proxy_Event representing this edit, and add it to the events queue
lock (eventsListLock)
{
eventsList.Add(MakeProxyEvent(typeName, opaqueRef, "add", proxyT, proxy));
}
}
public void SendDestroyObject(string typeName, string opaqueRef)
{
// Make a Proxy object from the new XML
Type proxyT = TypeCache.GetProxyType(typeName);
// Make a Proxy_Event representing this edit, and add it to the events queue
lock (eventsListLock)
{
eventsList.Add(MakeProxyEvent(typeName, opaqueRef, "del", proxyT, Activator.CreateInstance(proxyT)));
}
}
public void SendModObject(string typeName, string opaqueRef)
{
// Make a Proxy object from the new XML
Type proxyT = TypeCache.GetProxyType(typeName);
object proxy = get_record(typeName, opaqueRef, false);
// Make a Proxy_Event representing this edit, and add it to the events queue
lock (eventsListLock)
{
eventsList.Add(MakeProxyEvent(typeName, opaqueRef, "mod", proxyT, proxy));
}
}
/// <summary>
/// Verifies that the specified call can be made with this version of server.
/// </summary>
private void VerifyVersion(string call)
{
string version = null;
foreach (string key in callVersions.Keys)
{
if (callVersions[key].Contains(call))
{
version = key;
break;
}
}
}
public string CreateOpaqueRef()
{
return string.Format("OpaqueRef:{0}", Guid.NewGuid());
}
}
}