/* 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 proxys = new Dictionary(); private static readonly object dicLock = new object(); public readonly Db db; public readonly List eventsList = new List(); 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> callVersions = new Dictionary>(); public bool IsSuperUser { get; private set; } public void SetIsSuperUser(bool isSuperUser) { IsSuperUser = isSuperUser; } public static void RemoveAll() { lock (dicLock) { foreach (KeyValuePair kvp in proxys) { kvp.Value.db.Dispose(); kvp.Value.RunInEventLoop = delegate() { throw new Failure(Failure.SESSION_INVALID); }; } proxys.Clear(); } } /// /// Initializes the class. /// 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()); foreach (XmlNode n in node.SelectNodes("call")) { callVersions[ver].Add(n.Attributes["name"].Value); } } */ } /// /// Generates a Proxy instance which implements for the specified and XML document. /// /// The connection. /// Name of the file. /// 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]; } } /// /// Initializes a new instance of the class. /// /// The connection. /// The http url of filename to the XenServer status report XML document to be used. 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 response = (Response)pmi.FakeMethod.Invoke(obj, methodArgs); if (response.Status == ResponseTypes.SUCCESS) { return new Response(CreateTask(DbProxy.TaskStatus.success, response.Value)); } else { return new Response(CreateTask(DbProxy.TaskStatus.failure, response.ErrorDescription)); } } return new Response(true, new String[] { Failure.INTERNAL_ERROR }); } public event EventHandler 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(""); } 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(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(""); } 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(""); } 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(""); } case "add_to_gui_config": AddToDictionary(pmi.TypeName, (string)args[1], "gui_config", args[2], args[3]); return new Response(""); case "remove_from_gui_config": RemoveFromDictionary(pmi.TypeName, (string)args[1], "gui_config", args[2]); return new Response(""); case "add_tags": AddToArray(pmi.TypeName, (string)args[1], "tags", args[2]); return new Response(""); case "remove_tags": RemoveFromArray(pmi.TypeName, (string)args[1], "tags", args[2]); return new Response(""); 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(subjectInfo); // For VBDEditPage.CalculateDevicePositions() case "get_allowed_vbd_devices": return new Response(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(tbl); case "get_uncooperative_resident_vms": return new Response(new string[0]); // For HAWizard_Pages.AssignPriorities case "ha_compute_hypothetical_max_host_failures_to_tolerate": return new Response("1"); case "get_ha_host_failures_to_tolerate": return new Response("1"); // For EvacuateHostDialog and RollingUpgradeWizard case "get_vms_which_prevent_evacuation": return new Response(new Hashtable()); // For RollingUpgradeWizard case "get_live": return new Response(true); case "get_allowed_operations": return new Response(new string[0]); case "get_health_check_config": string uuid1 = (string)args[1]; return new Response(get_health_check_config(pmi.TypeName, uuid1, true)); } return new Response(true, new String[] { Failure.INTERNAL_ERROR }); } #endregion /// /// Determine if the given URL represents a simulator db proxy /// /// /// 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 tasks = new Dictionary(); 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 list = new List(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(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)); } } /// /// Verifies that the specified call can be made with this version of server. /// 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()); } } }