plugins/hypervisors/hyperv/DotNet/ServerResource/HypervResource/HypervResourceController.cs (2,092 lines of code) (raw):

// Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. using Amazon; using Amazon.S3; using Amazon.S3.Model; using log4net; using Microsoft.CSharp.RuntimeBinder; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.Collections; using System.Collections.Specialized; using System.Collections.Generic; using System.Configuration; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Text; using System.Security.Cryptography; using System.Security.Principal; using System.Web.Http; using CloudStack.Plugin.WmiWrappers.ROOT.VIRTUALIZATION.V2; namespace HypervResource { public struct HypervResourceControllerConfig { private string privateIpAddress; private static ILog logger = LogManager.GetLogger(typeof(HypervResourceControllerConfig)); public string PrivateIpAddress { get { return privateIpAddress; } set { ValidateIpAddress(value); privateIpAddress = value; System.Net.NetworkInformation.NetworkInterface nic = HypervResourceController.GetNicInfoFromIpAddress(privateIpAddress, out PrivateNetmask); PrivateMacAddress = nic.GetPhysicalAddress().ToString(); } } private static void ValidateIpAddress(string value) { // Convert to IP address IPAddress ipAddress; if (!IPAddress.TryParse(value, out ipAddress)) { String errMsg = "Invalid PrivateIpAddress: " + value; logger.Error(errMsg); throw new ArgumentException(errMsg); } } public string GatewayIpAddress; public string PrivateMacAddress; public string PrivateNetmask; public string StorageNetmask; public string StorageMacAddress; public string StorageIpAddress; public long RootDeviceReservedSpaceBytes; public string RootDeviceName; public ulong ParentPartitionMinMemoryMb; public string LocalSecondaryStoragePath; private string getPrimaryKey(string id) { return "primary_storage_" + id; } public string getPrimaryStorage(string id) { NameValueCollection settings = ConfigurationManager.AppSettings; return settings.Get(getPrimaryKey(id)); } public void setPrimaryStorage(string id, string path) { Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None); KeyValueConfigurationCollection settings = config.AppSettings.Settings; string key = getPrimaryKey(id); if (settings[key] != null) { settings.Remove(key); } settings.Add(key, path); config.Save(ConfigurationSaveMode.Modified); ConfigurationManager.RefreshSection("appSettings"); } public List<string> getAllPrimaryStorages() { List<string> poolPaths = new List<string>(); Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None); KeyValueConfigurationCollection settings = config.AppSettings.Settings; foreach (string key in settings.AllKeys) { if (key.Contains("primary_storage_")) { poolPaths.Add(settings[key].Value); } } return poolPaths; } } /// <summary> /// Supports one HTTP GET and multiple HTTP POST URIs /// </summary> /// <remarks> /// <para> /// POST takes dynamic to allow it to receive JSON without concern for what is the underlying object. /// E.g. http://stackoverflow.com/questions/14071715/passing-dynamic-json-object-to-web-api-newtonsoft-example /// and http://stackoverflow.com/questions/3142495/deserialize-json-into-c-sharp-dynamic-object /// Use ActionName attribute to allow multiple POST URLs, one for each supported command /// E.g. http://stackoverflow.com/a/12703423/939250 /// Strictly speaking, this goes against the purpose of an ApiController, which is to provide one GET/POST/PUT/DELETE, etc. /// However, it reduces the amount of code by removing the need for a switch according to the incoming command type. /// http://weblogs.asp.net/fredriknormen/archive/2012/06/11/asp-net-web-api-exception-handling.aspx /// </para> /// <para> /// Exceptions handled on command by command basis rather than globally to allow details of the command /// to be reflected in the response. Default error handling is in the catch for Exception, but /// other exception types may be caught where the feedback would be different. /// NB: global alternatives discussed at /// http://weblogs.asp.net/fredriknormen/archive/2012/06/11/asp-net-web-api-exception-handling.aspx /// </para> /// </remarks> public class HypervResourceController : ApiController { public static void Configure(HypervResourceControllerConfig config) { HypervResourceController.config = config; wmiCallsV2 = new WmiCallsV2(); } public static HypervResourceControllerConfig config = new HypervResourceControllerConfig(); private static ILog logger = LogManager.GetLogger(typeof(HypervResourceController)); Dictionary<String, String> contextMap = new Dictionary<String, String>(); public static void Initialize() { } public static IWmiCallsV2 wmiCallsV2 { get; set;} // GET api/HypervResource public string Get() { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { return "HypervResource controller running, use POST to send JSON encoded RPCs"; ; } } /// <summary> /// NOP - placeholder for future setup, e.g. delete existing VMs or Network ports /// POST api/HypervResource/SetupCommand /// </summary> /// <param name="cmd"></param> /// <returns></returns> /// TODO: produce test [HttpPost] [ActionName(CloudStackTypes.SetupCommand)] public JContainer SetupCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.SetupCommand + Utils.CleanString(cmd.ToString())); string details = null; bool result = false; try { result = true; } catch (Exception sysEx) { details = CloudStackTypes.SetupCommand + " failed due to " + sysEx.Message; logger.Error(details, sysEx); } object ansContent = new { result = result, details = "success - NOP", _reconnect = false, contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.SetupAnswer); } } // POST api/HypervResource/AttachCommand [HttpPost] [ActionName(CloudStackTypes.AttachCommand)] public JContainer AttachCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.AttachCommand + Utils.CleanString(cmd.ToString())); string details = null; bool result = false; try { string vmName = (string)cmd.vmName; DiskTO disk = DiskTO.ParseJson(cmd.disk); if (disk.type.Equals("ISO")) { TemplateObjectTO dataStore = disk.templateObjectTO; NFSTO share = dataStore.nfsDataStoreTO; string diskPath = Utils.NormalizePath(Path.Combine(share.UncPath, dataStore.path)); wmiCallsV2.AttachIso(vmName, diskPath); result = true; } else if (disk.type.Equals("DATADISK")) { VolumeObjectTO volume = disk.volumeObjectTO; string diskPath = Utils.NormalizePath(volume.FullFileName); wmiCallsV2.AttachDisk(vmName, diskPath, disk.diskSequence); result = true; } else { details = "Invalid disk type to be attached to vm " + vmName; } } catch (Exception sysEx) { details = CloudStackTypes.AttachCommand + " failed due to " + sysEx.Message; logger.Error(details, sysEx); } object ansContent = new { result = result, details = details, disk = cmd.disk, contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.AttachAnswer); } } // POST api/HypervResource/DetachCommand [HttpPost] [ActionName(CloudStackTypes.DettachCommand)] public JContainer DetachCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.DettachCommand + Utils.CleanString(cmd.ToString())); string details = null; bool result = false; try { string vmName = (string)cmd.vmName; DiskTO disk = DiskTO.ParseJson(cmd.disk); if (disk.type.Equals("ISO")) { TemplateObjectTO dataStore = disk.templateObjectTO; NFSTO share = dataStore.nfsDataStoreTO; string diskPath = Utils.NormalizePath(Path.Combine(share.UncPath, dataStore.path)); wmiCallsV2.DetachDisk(vmName, diskPath); result = true; } else if (disk.type.Equals("DATADISK")) { VolumeObjectTO volume = disk.volumeObjectTO; string diskPath = Utils.NormalizePath(volume.FullFileName); wmiCallsV2.DetachDisk(vmName, diskPath); result = true; } else { details = "Invalid disk type to be dettached from vm " + vmName; } } catch (Exception sysEx) { details = CloudStackTypes.DettachCommand + " failed due to " + sysEx.Message; logger.Error(details, sysEx); } object ansContent = new { result = result, details = details, contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.DettachAnswer); } } // POST api/HypervResource/RebootCommand [HttpPost] [ActionName(CloudStackTypes.RebootCommand)] public JContainer RebootCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.RebootCommand + Utils.CleanString(cmd.ToString())); string details = null; bool result = false; try { string vmName = (string)cmd.vmName; var sys = wmiCallsV2.GetComputerSystem(vmName); if (sys == null) { details = CloudStackTypes.RebootCommand + " requested unknown VM " + vmName; logger.Error(details); } else { wmiCallsV2.SetState(sys, RequiredState.Reset); result = true; } } catch (Exception sysEx) { details = CloudStackTypes.RebootCommand + " failed due to " + sysEx.Message; logger.Error(details, sysEx); } object ansContent = new { result = result, details = details, contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.RebootAnswer); } } // POST api/HypervResource/DestroyCommand [HttpPost] [ActionName(CloudStackTypes.DestroyCommand)] public JContainer DestroyCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.DestroyCommand + Utils.CleanString(cmd.ToString())); string details = null; bool result = false; try { // Assert String errMsg = "No 'volume' details in " + CloudStackTypes.DestroyCommand + " " + Utils.CleanString(cmd.ToString()); if (cmd.volume == null) { logger.Error(errMsg); throw new ArgumentException(errMsg); } // Assert errMsg = "No valid path in DestroyCommand in " + CloudStackTypes.DestroyCommand + " " + (String)cmd.ToString(); if (cmd.volume.path == null) { logger.Error(errMsg); throw new ArgumentException(errMsg); } String path = (string)cmd.volume.path; if (!File.Exists(path)) { logger.Info(CloudStackTypes.DestroyCommand + ", but volume at pass already deleted " + path); } string vmName = (string)cmd.vmName; if (!string.IsNullOrEmpty(vmName) && File.Exists(path)) { // Make sure that this resource is removed from the VM wmiCallsV2.DetachDisk(vmName, path); } File.Delete(path); result = true; } catch (Exception sysEx) { details = CloudStackTypes.DestroyCommand + " failed due to " + sysEx.Message; logger.Error(details, sysEx); } object ansContent = new { result = result, details = details, contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.Answer); } } // POST api/HypervResource/DeleteCommand [HttpPost] [ActionName(CloudStackTypes.DeleteCommand)] public JContainer DeleteCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.DestroyCommand + Utils.CleanString(cmd.ToString())); string details = null; bool result = false; try { // Assert String errMsg = "No 'volume' details in " + CloudStackTypes.DestroyCommand + " " + Utils.CleanString(cmd.ToString()); VolumeObjectTO destVolumeObjectTO = VolumeObjectTO.ParseJson(cmd.data); if (destVolumeObjectTO.name == null) { logger.Error(errMsg); throw new ArgumentException(errMsg); } String path = destVolumeObjectTO.FullFileName; if (!File.Exists(path)) { logger.Info(CloudStackTypes.DestroyCommand + ", but volume at pass already deleted " + path); } string vmName = (string)cmd.vmName; if (!string.IsNullOrEmpty(vmName) && File.Exists(path)) { // Make sure that this resource is removed from the VM wmiCallsV2.DetachDisk(vmName, path); } File.Delete(path); result = true; } catch (Exception sysEx) { details = CloudStackTypes.DestroyCommand + " failed due to " + sysEx.Message; logger.Error(details, sysEx); } object ansContent = new { result = result, details = details, contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.Answer); } } private static JArray ReturnCloudStackTypedJArray(object ansContent, string ansType) { JObject ansObj = Utils.CreateCloudStackObject(ansType, ansContent); JArray answer = new JArray(ansObj); logger.Info(Utils.CleanString(ansObj.ToString())); return answer; } // POST api/HypervResource/CreateCommand [HttpPost] [ActionName(CloudStackTypes.CreateCommand)] public JContainer CreateCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.CreateCommand + Utils.CleanString(cmd.ToString())); string details = null; bool result = false; VolumeInfo volume = new VolumeInfo(); try { string diskType = cmd.diskCharacteristics.type; ulong disksize = cmd.diskCharacteristics.size; string templateUri = cmd.templateUrl; // assert: valid storagepool? string poolTypeStr = cmd.pool.type; string poolLocalPath = cmd.pool.path; string poolUuid = cmd.pool.uuid; string newVolPath = null; long volId = cmd.volId; string newVolName = null; if (ValidStoragePool(poolTypeStr, poolLocalPath, poolUuid, ref details)) { // No template URI? Its a blank disk. if (string.IsNullOrEmpty(templateUri)) { // assert VolumeType volType; if (!Enum.TryParse<VolumeType>(diskType, out volType) && volType != VolumeType.DATADISK) { details = "Cannot create volumes of type " + (string.IsNullOrEmpty(diskType) ? "NULL" : diskType); } else { newVolName = cmd.diskCharacteristics.name; newVolPath = Path.Combine(poolLocalPath, newVolName, diskType.ToLower()); // TODO: make volume format and block size configurable wmiCallsV2.CreateDynamicVirtualHardDisk(disksize, newVolPath); if (File.Exists(newVolPath)) { result = true; } else { details = "Failed to create DATADISK with name " + newVolName; } } } else { // TODO: Does this always work, or do I need to download template at times? if (templateUri.Contains("/") || templateUri.Contains("\\")) { details = "Problem with templateURL " + templateUri + " the URL should be volume UUID in primary storage created by previous PrimaryStorageDownloadCommand"; logger.Error(details); } else { logger.Debug("Template's name in primary store should be " + templateUri); // HypervPhysicalDisk BaseVol = primaryPool.getPhysicalDisk(tmplturl); FileInfo srcFileInfo = new FileInfo(templateUri); newVolName = Guid.NewGuid() + srcFileInfo.Extension; newVolPath = Path.Combine(poolLocalPath, newVolName); logger.Debug("New volume will be at " + newVolPath); string oldVolPath = Path.Combine(poolLocalPath, templateUri); File.Copy(oldVolPath, newVolPath); if (File.Exists(newVolPath)) { result = true; } else { details = "Failed to create DATADISK with name " + newVolName; } } volume = new VolumeInfo( volId, diskType, poolTypeStr, poolUuid, newVolName, newVolPath, newVolPath, (long)disksize, null); } } } catch (Exception sysEx) { // TODO: consider this as model for error processing in all commands details = CloudStackTypes.CreateCommand + " failed due to " + sysEx.Message; logger.Error(details, sysEx); } object ansContent = new { result = result, details = details, volume = volume, contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.CreateAnswer); } } // POST api/HypervResource/PrimaryStorageDownloadCommand [HttpPost] [ActionName(CloudStackTypes.PrimaryStorageDownloadCommand)] public JContainer PrimaryStorageDownloadCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.PrimaryStorageDownloadCommand + Utils.CleanString(cmd.ToString())); string details = null; bool result = false; long size = 0; string newCopyFileName = null; string poolLocalPath = cmd.localPath; if (!Directory.Exists(poolLocalPath)) { details = "None existent local path " + poolLocalPath; } else { // Compose name for downloaded file. string sourceUrl = cmd.url; if (sourceUrl.ToLower().EndsWith(".vhd")) { newCopyFileName = Guid.NewGuid() + ".vhd"; } if (sourceUrl.ToLower().EndsWith(".vhdx")) { newCopyFileName = Guid.NewGuid() + ".vhdx"; } // assert if (newCopyFileName == null) { details = CloudStackTypes.PrimaryStorageDownloadCommand + " Invalid file extension for hypervisor type in source URL " + sourceUrl; logger.Error(details); } else { try { FileInfo newFile; if (CopyURI(sourceUrl, newCopyFileName, poolLocalPath, out newFile, ref details)) { size = newFile.Length; result = true; } } catch (System.Exception ex) { details = CloudStackTypes.PrimaryStorageDownloadCommand + " Cannot download source URL " + sourceUrl + " due to " + ex.Message; logger.Error(details, ex); } } } object ansContent = new { result = result, details = details, templateSize = size, installPath = newCopyFileName, contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.PrimaryStorageDownloadAnswer); } } private static bool ValidStoragePool(string poolTypeStr, string poolLocalPath, string poolUuid, ref string details) { StoragePoolType poolType; if (!Enum.TryParse<StoragePoolType>(poolTypeStr, out poolType) || poolType != StoragePoolType.Filesystem) { details = "Primary storage pool " + poolUuid + " type " + poolType + " local path " + poolLocalPath + " has invalid StoragePoolType"; logger.Error(details); return false; } else if (!Directory.Exists(poolLocalPath)) { details = "Primary storage pool " + poolUuid + " type " + poolType + " local path " + poolLocalPath + " has invalid local path"; logger.Error(details); return false; } return true; } /// <summary> /// Exceptions to watch out for: /// Exceptions related to URI creation /// System.SystemException /// +-System.ArgumentNullException /// +-System.FormatException /// +-System.UriFormatException /// /// Exceptions related to NFS URIs /// System.SystemException /// +-System.NotSupportedException /// +-System.ArgumentException /// +-System.ArgumentNullException /// +-System.Security.SecurityException; /// +-System.UnauthorizedAccessException /// +-System.IO.IOException /// +-System.IO.PathTooLongException /// /// Exceptions related to HTTP URIs /// System.SystemException /// +-System.InvalidOperationException /// +-System.Net.WebException /// +-System.NotSupportedException /// +-System.ArgumentNullException /// </summary> /// <param name="sourceUri"></param> /// <param name="newCopyFileName"></param> /// <param name="poolLocalPath"></param> /// <returns></returns> private bool CopyURI(string sourceUri, string newCopyFileName, string poolLocalPath, out FileInfo newFile, ref string details) { Uri source = new Uri(sourceUri); String destFilePath = Path.Combine(poolLocalPath, newCopyFileName); string[] pathSegments = source.Segments; String templateUUIDandExtension = pathSegments[pathSegments.Length - 1]; newFile = new FileInfo(destFilePath); // NFS URI assumed to already be mounted locally. Mount location given by settings. if (source.Scheme.ToLower().Equals("nfs")) { String srcDiskPath = Path.Combine(HypervResourceController.config.LocalSecondaryStoragePath, templateUUIDandExtension); String taskMsg = "Copy NFS url in " + sourceUri + " at " + srcDiskPath + " to pool " + poolLocalPath; logger.Debug(taskMsg); File.Copy(srcDiskPath, destFilePath); } else if (source.Scheme.ToLower().Equals("http") || source.Scheme.ToLower().Equals("https")) { System.Net.WebClient webclient = new WebClient(); webclient.DownloadFile(source, destFilePath); } else { details = "Unsupported URI scheme " + source.Scheme.ToLower() + " in source URI " + sourceUri; logger.Error(details); return false; } if (!File.Exists(destFilePath)) { details = "Filed to copy " + sourceUri + " to primary pool destination " + destFilePath; logger.Error(details); return false; } return true; } // POST api/HypervResource/CheckHealthCommand [HttpPost] [ActionName(CloudStackTypes.CheckHealthCommand)] public JContainer CheckHealthCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.CheckHealthCommand + Utils.CleanString(cmd.ToString())); object ansContent = new { result = true, details = "resource is alive", contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.CheckHealthAnswer); } } // POST api/HypervResource/CheckOnHostCommand [HttpPost] [ActionName(CloudStackTypes.CheckOnHostCommand)] public JContainer CheckOnHostCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.CheckOnHostCommand + Utils.CleanString(cmd.ToString())); string details = "host is not alive"; bool result = true; try { foreach (string poolPath in config.getAllPrimaryStorages()) { if (IsHostAlive(poolPath, (string)cmd.host.privateNetwork.ip)) { result = false; details = "host is alive"; break; } } } catch (Exception e) { logger.Error("Error Occurred in " + CloudStackTypes.CheckOnHostCommand + " : " + e.Message); } object ansContent = new { result = result, details = details, contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.CheckOnHostAnswer); } } private bool IsHostAlive(string poolPath, string privateIp) { bool hostAlive = false; try { string hbFile = Path.Combine(poolPath, "hb-" + privateIp); FileInfo file = new FileInfo(hbFile); using (StreamReader sr = file.OpenText()) { string epoch = sr.ReadLine(); string[] dateTime = epoch.Split('@'); string[] date = dateTime[0].Split('-'); string[] time = dateTime[1].Split(':'); DateTime epochTime = new DateTime(Convert.ToInt32(date[0]), Convert.ToInt32(date[1]), Convert.ToInt32(date[2]), Convert.ToInt32(time[0]), Convert.ToInt32(time[1]), Convert.ToInt32(time[2]), DateTimeKind.Utc); DateTime currentTime = DateTime.UtcNow; DateTime ThreeMinuteLaterEpoch = epochTime.AddMinutes(3); if (currentTime.CompareTo(ThreeMinuteLaterEpoch) < 0) { hostAlive = true; } sr.Close(); } } catch (Exception e) { logger.Info("Exception occurred in verifying host " + e.Message); } return hostAlive; } // POST api/HypervResource/CheckSshCommand // TODO: create test [HttpPost] [ActionName(CloudStackTypes.CheckSshCommand)] public JContainer CheckSshCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.CheckSshCommand + Utils.CleanString(cmd.ToString())); object ansContent = new { result = true, details = "NOP, TODO: implement properly", contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.CheckSshAnswer); } } // POST api/HypervResource/CheckVirtualMachineCommand [HttpPost] [ActionName(CloudStackTypes.CheckVirtualMachineCommand)] public JContainer CheckVirtualMachineCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.CheckVirtualMachineCommand + Utils.CleanString(cmd.ToString())); string details = null; bool result = false; string vmName = cmd.vmName; string powerState = null; // TODO: Look up the VM, convert Hyper-V state to CloudStack version. var sys = wmiCallsV2.GetComputerSystem(vmName); if (sys == null) { details = CloudStackTypes.CheckVirtualMachineCommand + " requested unknown VM " + vmName; logger.Error(details); } else { powerState = EnabledState.ToCloudStackPowerState(sys.EnabledState); result = true; } object ansContent = new { result = result, details = details, powerstate = powerState, contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.CheckVirtualMachineAnswer); } } // POST api/HypervResource/DeleteStoragePoolCommand [HttpPost] [ActionName(CloudStackTypes.DeleteStoragePoolCommand)] public JContainer DeleteStoragePoolCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.DeleteStoragePoolCommand + Utils.CleanString(cmd.ToString())); object ansContent = new { result = true, details = "Current implementation does not delete local path corresponding to storage pool!", contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.Answer); } } /// <summary> /// NOP - legacy command - /// POST api/HypervResource/CreateStoragePoolCommand /// </summary> /// <param name="cmd"></param> /// <returns></returns> [HttpPost] [ActionName(CloudStackTypes.CreateStoragePoolCommand)] public JContainer CreateStoragePoolCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.CreateStoragePoolCommand + Utils.CleanString(cmd.ToString())); object ansContent = new { result = true, details = "success - NOP", contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.Answer); } } // POST api/HypervResource/ModifyStoragePoolCommand [HttpPost] [ActionName(CloudStackTypes.ModifyStoragePoolCommand)] public JContainer ModifyStoragePoolCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.ModifyStoragePoolCommand + Utils.CleanString(cmd.ToString())); string details = null; string localPath; StoragePoolType poolType; long capacityBytes = 0; long availableBytes = 0; string hostPath = null; bool result = false; var tInfo = new Dictionary<string, string>(); object ansContent; try { result = ValidateStoragePoolCommand(cmd, out localPath, out poolType, ref details); if (!result) { ansContent = new { result = result, details = details, contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.Answer); } if (poolType == StoragePoolType.Filesystem) { hostPath = localPath; GetCapacityForPath(hostPath, out capacityBytes, out availableBytes); } else if (poolType == StoragePoolType.NetworkFilesystem || poolType == StoragePoolType.SMB) { NFSTO share = new NFSTO(); String uriStr = "cifs://" + (string)cmd.pool.host + (string)cmd.pool.path; share.uri = new Uri(uriStr); hostPath = Utils.NormalizePath(share.UncPath); // Check access to share. Utils.GetShareDetails(hostPath, out capacityBytes, out availableBytes); config.setPrimaryStorage((string)cmd.pool.uuid, hostPath); } else { result = false; } } catch { result = false; details = String.Format("Failed to add storage pool {0}, please verify your pool details", (string)cmd.pool.uuid); logger.Error(details); } String uuid = null; var poolInfo = new { uuid = uuid, host = cmd.pool.host, hostPath = cmd.pool.path, localPath = hostPath, poolType = cmd.pool.type, capacityBytes = capacityBytes, availableBytes = availableBytes }; ansContent = new { result = result, details = details, localPath = hostPath, templateInfo = tInfo, poolInfo = poolInfo, contextMap = contextMap }; if (result) { try { if ((bool)cmd.add) { logger.Info("Adding HeartBeat Task to task scheduler for pool " + (string)cmd.pool.uuid); Utils.AddHeartBeatTask((string)cmd.pool.uuid, hostPath, config.PrivateIpAddress); } else { logger.Info("Deleting HeartBeat Task from task scheduler for pool " + (string)cmd.pool.uuid); Utils.RemoveHeartBeatTask(cmd.pool.uuid); } } catch (Exception e) { logger.Error("Error occurred in adding/delete HeartBeat Task to/from Task Scheduler : " + e.Message); } } return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.ModifyStoragePoolAnswer); } } private bool ValidateStoragePoolCommand(dynamic cmd, out string localPath, out StoragePoolType poolType, ref string details) { dynamic pool = cmd.pool; string poolTypeStr = pool.type; localPath = cmd.localPath; if (!Enum.TryParse<StoragePoolType>(poolTypeStr, out poolType)) { details = "Request to create / modify unsupported pool type: " + (poolTypeStr == null ? "NULL" : poolTypeStr) + "in cmd " + JsonConvert.SerializeObject(cmd); logger.Error(details); return false; } if (poolType != StoragePoolType.Filesystem && poolType != StoragePoolType.NetworkFilesystem && poolType != StoragePoolType.SMB) { details = "Request to create / modify unsupported pool type: " + (poolTypeStr == null ? "NULL" : poolTypeStr) + "in cmd " + JsonConvert.SerializeObject(cmd); logger.Error(details); return false; } return true; } // POST api/HypervResource/PlugNicCommand [HttpPost] [ActionName(CloudStackTypes.PlugNicCommand)] public JContainer PlugNicCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.PlugNicCommand + Utils.CleanString(cmd.ToString())); object ansContent = new { result = true, details = "Hot Nic plug not supported, change any empty virtual network adapter network settings", contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.PlugNicAnswer); } } // POST api/HypervResource/CleanupNetworkRulesCmd [HttpPost] [ActionName(CloudStackTypes.CleanupNetworkRulesCmd)] public JContainer CleanupNetworkRulesCmd([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.CleanupNetworkRulesCmd + Utils.CleanString(cmd.ToString())); object ansContent = new { result = false, details = "nothing to cleanup in our current implementation", contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.Answer); } } // POST api/HypervResource/CheckNetworkCommand [HttpPost] [ActionName(CloudStackTypes.CheckNetworkCommand)] public JContainer CheckNetworkCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.CheckNetworkCommand + Utils.CleanString(cmd.ToString())); object ansContent = new { result = true, details = (string)null, contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.CheckNetworkAnswer); } } // POST api/HypervResource/ReadyCommand [HttpPost] [ActionName(CloudStackTypes.ReadyCommand)] public JContainer ReadyCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.ReadyCommand + Utils.CleanString(cmd.ToString())); object ansContent = new { result = true, details = (string)null, contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.ReadyAnswer); } } // POST api/HypervResource/StartCommand [HttpPost] [ActionName(CloudStackTypes.StartCommand)] public JContainer StartCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.StartCommand + Utils.CleanString(cmd.ToString())); string details = null; bool result = false; try { string systemVmIsoPath = null; String uriStr = (String)cmd.secondaryStorage; if (!String.IsNullOrEmpty(uriStr)) { NFSTO share = new NFSTO(); share.uri = new Uri(uriStr); string defaultDataPath = wmiCallsV2.GetDefaultDataRoot(); string secondaryPath = Utils.NormalizePath(Path.Combine(share.UncPath, "systemvm")); string[] choices = Directory.GetFiles(secondaryPath, "systemvm*.iso"); if (choices.Length != 1) { String errMsg = "Couldn't locate the systemvm iso on " + secondaryPath; logger.Error(errMsg); } else { systemVmIsoPath = choices[0]; } } wmiCallsV2.DeployVirtualMachine(cmd, systemVmIsoPath); result = true; } catch (Exception wmiEx) { details = CloudStackTypes.StartCommand + " fail on exception" + wmiEx.Message; logger.Error(details, wmiEx); } object ansContent = new { result = result, details = details, vm = cmd.vm, contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.StartAnswer); } } // POST api/HypervResource/StopCommand [HttpPost] [ActionName(CloudStackTypes.StopCommand)] public JContainer StopCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.StopCommand + Utils.CleanString(cmd.ToString())); string details = null; bool result = false; bool checkBeforeCleanup = cmd.checkBeforeCleanup; String vmName = cmd.vmName; if (checkBeforeCleanup == true) { ComputerSystem vm = wmiCallsV2.GetComputerSystem(vmName); if (vm == null || vm.EnabledState == 2) { // VM is not available or vm is not in running state return ReturnCloudStackTypedJArray(new { result = false, details = "VM is not available or vm is not running on host, bailing out", vm = vmName, contextMap = contextMap }, CloudStackTypes.StopAnswer); } } try { wmiCallsV2.DestroyVm(cmd); result = true; } catch (Exception wmiEx) { details = CloudStackTypes.StopCommand + " fail on exception" + wmiEx.Message; logger.Error(details, wmiEx); } object ansContent = new { result = result, details = details, vm = cmd.vm, contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.StopAnswer); } } // POST api/HypervResource/CreateObjectCommand [HttpPost] [ActionName(CloudStackTypes.CreateObjectCommand)] public JContainer CreateObjectCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.CreateObjectCommand + Utils.CleanString(cmd.ToString())); bool result = false; string details = null; object newData = null; try { VolumeObjectTO volume = VolumeObjectTO.ParseJson(cmd.data); PrimaryDataStoreTO primary = volume.primaryDataStore; ulong volumeSize = volume.size; string volumeName = volume.uuid + ".vhdx"; string volumePath = null; if (primary.isLocal) { volumePath = Path.Combine(primary.Path, volumeName); } else { volumePath = @"\\" + primary.uri.Host + primary.uri.LocalPath + @"\" + volumeName; volumePath = Utils.NormalizePath(volumePath); } volume.path = volume.uuid; wmiCallsV2.CreateDynamicVirtualHardDisk(volumeSize, volumePath); if (File.Exists(volumePath)) { result = true; JObject ansObj = Utils.CreateCloudStackObject(CloudStackTypes.VolumeObjectTO, volume); newData = ansObj; } else { details = "Failed to create disk with name " + volumePath; } } catch (Exception ex) { // Test by providing wrong key details = CloudStackTypes.CreateObjectCommand + " failed on exception, " + ex.Message; logger.Error(details, ex); } object ansContent = new { result = result, details = details, data = newData, contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.CreateObjectAnswer); } } // POST api/HypervResource/MaintainCommand // TODO: should this be a NOP? [HttpPost] [ActionName(CloudStackTypes.MaintainCommand)] public JContainer MaintainCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.MaintainCommand + Utils.CleanString(cmd.ToString())); object ansContent = new { result = true, willMigrate = true, details = "success - NOP for MaintainCommand", _reconnect = false, contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.MaintainAnswer); } } // POST api/HypervResource/PingRoutingCommand // TODO: should this be a NOP? [HttpPost] [ActionName(CloudStackTypes.PingRoutingCommand)] public JContainer PingRoutingCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.PingRoutingCommand + Utils.CleanString(cmd.ToString())); object ansContent = new { result = true, details = "success - NOP for PingRoutingCommand", _reconnect = false, contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.Answer); } } // POST api/HypervResource/PingCommand // TODO: should this be a NOP? [HttpPost] [ActionName(CloudStackTypes.PingCommand)] public JContainer PingCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.PingCommand + Utils.CleanString(cmd.ToString())); object ansContent = new { result = true, details = "success - NOP for PingCommand", _reconnect = false, contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.Answer); } } // POST api/HypervResource/ModifyVmVnicVlanCommand [HttpPost] [ActionName(CloudStackTypes.ModifyVmNicConfigCommand)] public JContainer ModifyVmNicConfigCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.ModifyVmNicConfigCommand + Utils.CleanString(cmd.ToString())); bool result = false; String vmName = cmd.vmName; String vlan = cmd.vlan; string macAddress = cmd.macAddress; uint pos = cmd.index; bool enable = cmd.enable; string switchLableName = cmd.switchLableName; if (macAddress != null) { wmiCallsV2.ModifyVmVLan(vmName, vlan, macAddress); result = true; } else if (pos >= 1) { wmiCallsV2.ModifyVmVLan(vmName, vlan, pos, enable, switchLableName); result = true; } object ansContent = new { vmName = vmName, result = result, contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.ModifyVmNicConfigAnswer); } } // POST api/HypervResource/GetVmConfigCommand [HttpPost] [ActionName(CloudStackTypes.GetVmConfigCommand)] public JContainer GetVmConfigCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.GetVmConfigCommand + Utils.CleanString(cmd.ToString())); bool result = false; String vmName = cmd.vmName; ComputerSystem vm = wmiCallsV2.GetComputerSystem(vmName); List<NicDetails> nicDetails = new List<NicDetails>(); var nicSettingsViaVm = wmiCallsV2.GetEthernetPortSettings(vm); NicDetails nic = null; int index = 0; int[] nicStates = new int[8]; int[] nicVlan = new int[8]; int vlanid = 1; var ethernetConnections = wmiCallsV2.GetEthernetConnections(vm); foreach (EthernetPortAllocationSettingData item in ethernetConnections) { EthernetSwitchPortVlanSettingData vlanSettings = wmiCallsV2.GetVlanSettings(item); if (vlanSettings == null) { vlanid = -1; } else { vlanid = vlanSettings.AccessVlanId; } nicStates[index] = (Int32)(item.EnabledState); nicVlan[index] = vlanid; index++; } index = 0; foreach (SyntheticEthernetPortSettingData item in nicSettingsViaVm) { nic = new NicDetails(item.Address, nicVlan[index], nicStates[index]); index++; nicDetails.Add(nic); } result = true; object ansContent = new { vmName = vmName, nics = nicDetails, result = result, contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.GetVmConfigAnswer); } } // POST api/HypervResource/GetVmStatsCommand [HttpPost] [ActionName(CloudStackTypes.GetVmStatsCommand)] public JContainer GetVmStatsCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.GetVmStatsCommand + Utils.CleanString(cmd.ToString())); bool result = false; JArray vmNamesJson = cmd.vmNames; string[] vmNames = vmNamesJson.ToObject<string[]>(); Dictionary<string, VmStatsEntry> vmProcessorInfo = new Dictionary<string, VmStatsEntry>(vmNames.Length); var vmsToInspect = new List<System.Management.ManagementPath>(); foreach (var vmName in vmNames) { var sys = wmiCallsV2.GetComputerSystem(vmName); if (sys == null) { logger.InfoFormat("GetVmStatsCommand requested unknown VM {0}", vmNames); continue; } var sysInfo = wmiCallsV2.GetVmSettings(sys); vmsToInspect.Add(sysInfo.Path); } wmiCallsV2.GetSummaryInfo(vmProcessorInfo, vmsToInspect); // TODO: Network usage comes from Performance Counter API; however it is only available in kb/s, and not in total terms. // Curious about these? Use perfmon to inspect them, e.g. http://msdn.microsoft.com/en-us/library/xhcx5a20%28v=vs.100%29.aspx // Recent post on these counter at http://blogs.technet.com/b/cedward/archive/2011/07/19/hyper-v-networking-optimizations-part-6-of-6-monitoring-hyper-v-network-consumption.aspx result = true; object ansContent = new { vmStatsMap = vmProcessorInfo, result = result, contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.GetVmStatsAnswer); } } // POST api/HypervResource/CopyCommand [HttpPost] [ActionName(CloudStackTypes.CopyCommand)] public JContainer CopyCommand(dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { // Log command *after* we've removed security details from the command. logger.Info(CloudStackTypes.CopyCommand + Utils.CleanString(cmd.ToString())); bool result = false; string details = null; object newData = null; TemplateObjectTO destTemplateObjectTO = null; VolumeObjectTO destVolumeObjectTO = null; VolumeObjectTO srcVolumeObjectTO = null; TemplateObjectTO srcTemplateObjectTO = null; try { srcTemplateObjectTO = TemplateObjectTO.ParseJson(cmd.srcTO); destTemplateObjectTO = TemplateObjectTO.ParseJson(cmd.destTO); srcVolumeObjectTO = VolumeObjectTO.ParseJson(cmd.srcTO); destVolumeObjectTO = VolumeObjectTO.ParseJson(cmd.destTO); string destFile = null; if (destTemplateObjectTO != null) { if (destTemplateObjectTO.path == null) { destTemplateObjectTO.path = System.Guid.NewGuid().ToString(); } if (destTemplateObjectTO.primaryDataStore != null) { destFile = destTemplateObjectTO.FullFileName; } else if (destTemplateObjectTO.nfsDataStoreTO != null) { destFile = destTemplateObjectTO.FullFileName; } } // Create local copy of a template? if (srcTemplateObjectTO != null && destTemplateObjectTO != null) { // S3 download to primary storage? // NFS provider download to primary storage? if ((srcTemplateObjectTO.s3DataStoreTO != null || srcTemplateObjectTO.nfsDataStoreTO != null) && destTemplateObjectTO.primaryDataStore != null) { if (File.Exists(destFile)) { logger.Info("Deleting existing file " + destFile); File.Delete(destFile); } if (srcTemplateObjectTO.s3DataStoreTO != null) { // Download from S3 to destination data storage DownloadS3ObjectToFile(srcTemplateObjectTO.path, srcTemplateObjectTO.s3DataStoreTO, destFile); } else if (srcTemplateObjectTO.nfsDataStoreTO != null) { // Download from S3 to destination data storage Utils.DownloadCifsFileToLocalFile(srcTemplateObjectTO.path, srcTemplateObjectTO.nfsDataStoreTO, destFile); } // Uncompress, as required if (srcTemplateObjectTO.path.EndsWith(".bz2")) { String uncompressedFile = destFile + ".tmp"; String compressedFile = destFile; using (var uncompressedOutStrm = new FileStream(uncompressedFile, FileMode.CreateNew, FileAccess.Write)) { using (var compressedInStrm = new FileStream(destFile, FileMode.Open, FileAccess.Read)) { using (var bz2UncompressorStrm = new Ionic.BZip2.BZip2InputStream(compressedInStrm, true) /* outer 'using' statement will close FileStream*/ ) { int count = 0; int bufsize = 1024 * 1024; byte[] buf = new byte[bufsize]; // EOF returns -1, see http://dotnetzip.codeplex.com/workitem/16069 while (0 < (count = bz2UncompressorStrm.Read(buf, 0, bufsize))) { uncompressedOutStrm.Write(buf, 0, count); } } } } File.Delete(compressedFile); File.Move(uncompressedFile, compressedFile); if (File.Exists(uncompressedFile)) { String errMsg = "Extra file left around called " + uncompressedFile + " when creating " + destFile; logger.Error(errMsg); throw new IOException(errMsg); } } // assert if (!File.Exists(destFile)) { String errMsg = "Failed to create " + destFile + " , because the file is missing"; logger.Error(errMsg); throw new IOException(errMsg); } FileInfo destFileInfo = new FileInfo(destFile); destTemplateObjectTO.size = destFileInfo.Length.ToString(); JObject ansObj = Utils.CreateCloudStackObject(CloudStackTypes.TemplateObjectTO, destTemplateObjectTO); newData = ansObj; result = true; } else { details = "Data store combination not supported"; } } // Create volume from a template? else if (srcTemplateObjectTO != null && destVolumeObjectTO != null) { // VolumeObjectTO guesses file extension based on existing files // this can be wrong if the previous file had a different file type var guessedDestFile = destVolumeObjectTO.FullFileName; if (File.Exists(guessedDestFile)) { logger.Info("Deleting existing file " + guessedDestFile); File.Delete(guessedDestFile); } destVolumeObjectTO.format = srcTemplateObjectTO.format; destFile = destVolumeObjectTO.FullFileName; if (File.Exists(destFile)) { logger.Info("Deleting existing file " + destFile); File.Delete(destFile); } string srcFile = srcTemplateObjectTO.FullFileName; if (!File.Exists(srcFile)) { details = "Local template file missing from " + srcFile; } else { // TODO: thin provision instead of copying the full file. File.Copy(srcFile, destFile); destVolumeObjectTO.path = destVolumeObjectTO.uuid; JObject ansObj = Utils.CreateCloudStackObject(CloudStackTypes.VolumeObjectTO, destVolumeObjectTO); newData = ansObj; result = true; } } else if (srcVolumeObjectTO != null && destVolumeObjectTO != null) { var guessedDestFile = destVolumeObjectTO.FullFileName; if (File.Exists(guessedDestFile)) { logger.Info("Deleting existing file " + guessedDestFile); File.Delete(guessedDestFile); } destVolumeObjectTO.format = srcVolumeObjectTO.format; destFile = destVolumeObjectTO.FullFileName; if (File.Exists(destFile)) { logger.Info("Deleting existing file " + destFile); File.Delete(destFile); } string srcFile = srcVolumeObjectTO.FullFileName; if (!File.Exists(srcFile)) { details = "Local template file missing from " + srcFile; } else { // Create the directory before copying the files. CreateDirectory // doesn't do anything if the directory is already present. Directory.CreateDirectory(Path.GetDirectoryName(destFile)); File.Copy(srcFile, destFile); if (srcVolumeObjectTO.nfsDataStore != null && srcVolumeObjectTO.primaryDataStore == null) { logger.Info("Copied volume from secondary data store to primary. Path: " + destVolumeObjectTO.path); } else if (srcVolumeObjectTO.primaryDataStore != null && srcVolumeObjectTO.nfsDataStore == null) { destVolumeObjectTO.path = destVolumeObjectTO.path + "/" + destVolumeObjectTO.uuid; if (destVolumeObjectTO.format != null) { destVolumeObjectTO.path += "." + destVolumeObjectTO.format.ToLower(); } } else { logger.Error("Destination volume path wasn't set. Unsupported source volume data store."); } // Create volumeto object deserialize and send it JObject ansObj = Utils.CreateCloudStackObject(CloudStackTypes.VolumeObjectTO, destVolumeObjectTO); newData = ansObj; result = true; } } else if (srcVolumeObjectTO != null && destTemplateObjectTO != null) { var guessedDestFile = destTemplateObjectTO.FullFileName; if (File.Exists(guessedDestFile)) { logger.Info("Deleting existing file " + guessedDestFile); File.Delete(guessedDestFile); } destTemplateObjectTO.format = srcVolumeObjectTO.format; destFile = destTemplateObjectTO.FullFileName; if (File.Exists(destFile)) { logger.Info("Deleting existing file " + destFile); File.Delete(destFile); } string srcFile = srcVolumeObjectTO.FullFileName; if (!File.Exists(srcFile)) { details = "Local template file missing from " + srcFile; } else { // Create the directory before copying the files. CreateDirectory // doesn't do anything if the directory is already present. Directory.CreateDirectory(Path.GetDirectoryName(destFile)); File.Copy(srcFile, destFile); FileInfo destFileInfo = new FileInfo(destFile); // Write the template.properties file PostCreateTemplate(Path.GetDirectoryName(destFile), destTemplateObjectTO.id, destTemplateObjectTO.name, destFileInfo.Length.ToString(), srcVolumeObjectTO.size.ToString(), destTemplateObjectTO.format); TemplateObjectTO destTemplateObject = new TemplateObjectTO(); destTemplateObject.size = srcVolumeObjectTO.size.ToString(); destTemplateObject.format = srcVolumeObjectTO.format; destTemplateObject.path = destTemplateObjectTO.path + "/" + destTemplateObjectTO.uuid; if (destTemplateObject.format != null) { destTemplateObject.path += "." + destTemplateObject.format.ToLower(); } destTemplateObject.nfsDataStoreTO = destTemplateObjectTO.nfsDataStoreTO; destTemplateObject.checksum = destTemplateObjectTO.checksum; JObject ansObj = Utils.CreateCloudStackObject(CloudStackTypes.TemplateObjectTO, destTemplateObject); newData = ansObj; result = true; } } else { details = "Data store combination not supported"; } } catch (Exception ex) { // Test by providing wrong key details = CloudStackTypes.CopyCommand + " failed on exception, " + ex.Message; logger.Error(details, ex); } object ansContent = new { result = result, details = details, newData = newData, contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.CopyCmdAnswer); } } private static void PostCreateTemplate(string path, string templateId, string templateUuid, string physicalSize, string virtualSize, string format) { string templatePropFile = Path.Combine(path, "template.properties"); using (StreamWriter sw = new StreamWriter(File.Open(templatePropFile, FileMode.Create), Encoding.GetEncoding("iso-8859-1"))) { if (format != null) { format = format.ToLower(); } sw.NewLine = "\n"; sw.WriteLine("id=" + templateId); sw.WriteLine("filename=" + templateUuid + "." + format); sw.WriteLine(format + ".filename=" + templateUuid + "." + format); sw.WriteLine("uniquename=" + templateUuid); sw.WriteLine(format + "=true"); sw.WriteLine("virtualsize=" + virtualSize); sw.WriteLine(format + ".virtualsize=" + virtualSize); sw.WriteLine("size=" + physicalSize); sw.WriteLine(format + ".size=" + physicalSize); sw.WriteLine("public=false"); } } private static bool VerifyChecksum(string destFile, string checksum) { string localChecksum = BitConverter.ToString(CalcFileChecksum(destFile)).Replace("-", "").ToLower(); logger.Debug("Checksum of " + destFile + " is " + checksum); if (checksum.Equals(localChecksum)) { return true; } return false; } /// <summary> /// Match implmentation of DownloadManagerImpl.computeCheckSum /// </summary> /// <param name="destFile"></param> /// <returns></returns> private static byte[] CalcFileChecksum(string destFile) { // TODO: Add unit test to verify that checksum algorithm has not changed. using (MD5 md5 = MD5.Create()) { using (FileStream stream = File.OpenRead(destFile)) { return md5.ComputeHash(stream); } } } private static void DownloadS3ObjectToFile(string srcObjectKey, S3TO srcS3TO, string destFile) { AmazonS3Config S3Config = new AmazonS3Config { ServiceURL = srcS3TO.endpoint, CommunicationProtocol = Amazon.S3.Model.Protocol.HTTP }; if (srcS3TO.httpsFlag) { S3Config.CommunicationProtocol = Protocol.HTTPS; } try { using (AmazonS3 client = Amazon.AWSClientFactory.CreateAmazonS3Client(srcS3TO.accessKey, srcS3TO.secretKey, S3Config)) { GetObjectRequest getObjectRequest = new GetObjectRequest().WithBucketName(srcS3TO.bucketName).WithKey(srcObjectKey); using (S3Response getObjectResponse = client.GetObject(getObjectRequest)) { using (Stream s = getObjectResponse.ResponseStream) { using (FileStream fs = new FileStream(destFile, FileMode.Create, FileAccess.Write)) { byte[] data = new byte[524288]; int bytesRead = 0; do { bytesRead = s.Read(data, 0, data.Length); fs.Write(data, 0, bytesRead); } while (bytesRead > 0); fs.Flush(); } } } } } catch (Exception ex) { string errMsg = "Download from S3 url" + srcS3TO.endpoint + " said: " + ex.Message; logger.Error(errMsg, ex); throw new Exception(errMsg, ex); } } // POST api/HypervResource/GetStorageStatsCommand [HttpPost] [ActionName(CloudStackTypes.GetStorageStatsCommand)] public JContainer GetStorageStatsCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.GetStorageStatsCommand + Utils.CleanString(cmd.ToString())); bool result = false; string details = null; long capacity = 0; long available = 0; long used = 0; try { StoragePoolType poolType; string hostPath = null; if (!Enum.TryParse<StoragePoolType>((string)cmd.pooltype, out poolType)) { details = "Request to get unsupported pool type: " + ((string)cmd.pooltype == null ? "NULL" : (string)cmd.pooltype) + "in cmd " + JsonConvert.SerializeObject(cmd); logger.Error(details); } else if (poolType == StoragePoolType.Filesystem) { hostPath = (string)cmd.localPath; GetCapacityForPath(hostPath, out capacity, out available); used = capacity - available; result = true; } else if (poolType == StoragePoolType.NetworkFilesystem || poolType == StoragePoolType.SMB) { string sharePath = config.getPrimaryStorage((string)cmd.id); if (sharePath != null) { hostPath = sharePath; Utils.GetShareDetails(sharePath, out capacity, out available); used = capacity - available; result = true; } } else { result = false; } if (result) { logger.Debug(CloudStackTypes.GetStorageStatsCommand + " set used bytes for " + hostPath + " to " + used); } } catch (Exception ex) { details = CloudStackTypes.GetStorageStatsCommand + " failed on exception" + ex.Message; logger.Error(details, ex); } object ansContent = new { result = result, details = details, capacity = capacity, used = used, contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.GetStorageStatsAnswer); } } // POST api/HypervResource/GetHostStatsCommand [HttpPost] [ActionName(CloudStackTypes.GetHostStatsCommand)] public JContainer GetHostStatsCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.GetHostStatsCommand + Utils.CleanString(cmd.ToString())); bool result = false; string details = null; object hostStats = null; var entityType = "host"; ulong totalMemoryKBs; ulong freeMemoryKBs; double networkReadKBs; double networkWriteKBs; double cpuUtilization; try { long hostId = (long)cmd.hostId; wmiCallsV2.GetMemoryResources(out totalMemoryKBs, out freeMemoryKBs); wmiCallsV2.GetProcessorUsageInfo(out cpuUtilization); // TODO: can we assume that the host has only one adaptor? string tmp; var privateNic = GetNicInfoFromIpAddress(config.PrivateIpAddress, out tmp); var nicStats = privateNic.GetIPv4Statistics(); //TODO: add IPV6 support, currentl networkReadKBs = nicStats.BytesReceived; networkWriteKBs = nicStats.BytesSent; // Generate GetHostStatsAnswer hostStats = new { hostId = hostId, entityType = entityType, cpuUtilization = cpuUtilization, networkReadKBs = networkReadKBs, networkWriteKBs = networkWriteKBs, totalMemoryKBs = (double)totalMemoryKBs, freeMemoryKBs = (double)freeMemoryKBs }; result = true; } catch (Exception ex) { details = CloudStackTypes.GetHostStatsCommand + " failed on exception" + ex.Message; logger.Error(details, ex); } object ansContent = new { result = result, hostStats = hostStats, details = details, contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.GetHostStatsAnswer); } } // POST api/HypervResource/PrepareForMigrationCommand [HttpPost] [ActionName(CloudStackTypes.PrepareForMigrationCommand)] public JContainer PrepareForMigrationCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.PrepareForMigrationCommand + Utils.CleanString(cmd.ToString())); string details = null; bool result = true; try { details = "NOP - success"; } catch (Exception sysEx) { result = false; details = CloudStackTypes.PrepareForMigrationCommand + " failed due to " + sysEx.Message; logger.Error(details, sysEx); } object ansContent = new { result = result, details = details, contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.PrepareForMigrationAnswer); } } // POST api/HypervResource/MigrateCommand [HttpPost] [ActionName(CloudStackTypes.MigrateCommand)] public JContainer MigrateCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.MigrateCommand + Utils.CleanString(cmd.ToString())); string details = null; bool result = false; try { string vm = (string)cmd.vmName; string destination = (string)cmd.destIp; wmiCallsV2.MigrateVm(vm, destination); result = true; } catch (Exception sysEx) { details = CloudStackTypes.MigrateCommand + " failed due to " + sysEx.Message; logger.Error(details, sysEx); } object ansContent = new { result = result, details = details, contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.MigrateAnswer); } } // POST api/HypervResource/MigrateVolumeCommand [HttpPost] [ActionName(CloudStackTypes.MigrateVolumeCommand)] public JContainer MigrateVolumeCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.MigrateVolumeCommand + Utils.CleanString(cmd.ToString())); string details = null; bool result = false; try { string vm = (string)cmd.attachedVmName; string volume = (string)cmd.volumePath; wmiCallsV2.MigrateVolume(vm, volume, GetStoragePoolPath(cmd.pool)); result = true; } catch (Exception sysEx) { details = CloudStackTypes.MigrateVolumeCommand + " failed due to " + sysEx.Message; logger.Error(details, sysEx); } object ansContent = new { result = result, volumePath = (string)cmd.volumePath, details = details, contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.MigrateVolumeAnswer); } } // POST api/HypervResource/MigrateWithStorageCommand [HttpPost] [ActionName(CloudStackTypes.MigrateWithStorageCommand)] public JContainer MigrateWithStorageCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.MigrateWithStorageCommand + Utils.CleanString(cmd.ToString())); string details = null; bool result = false; List<dynamic> volumeTos = new List<dynamic>(); try { string vm = (string)cmd.vm.name; string destination = (string)cmd.tgtHost; var volumeToPoolList = cmd.volumeToFilerAsList; var volumeToPool = new Dictionary<string, string>(); foreach (var item in volumeToPoolList) { volumeTos.Add(item.t); string poolPath = GetStoragePoolPath(item.u); volumeToPool.Add((string)item.t.path, poolPath); } wmiCallsV2.MigrateVmWithVolume(vm, destination, volumeToPool); result = true; } catch (Exception sysEx) { details = CloudStackTypes.MigrateWithStorageCommand + " failed due to " + sysEx.Message; logger.Error(details, sysEx); } object ansContent = new { result = result, volumeTos = JArray.FromObject(volumeTos), details = details, contextMap = contextMap }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.MigrateWithStorageAnswer); } } private string GetStoragePoolPath(dynamic pool) { string poolTypeStr = pool.type; StoragePoolType poolType; if (!Enum.TryParse<StoragePoolType>(poolTypeStr, out poolType)) { throw new ArgumentException("Invalid pool type " + poolTypeStr); } else if (poolType == StoragePoolType.SMB) { NFSTO share = new NFSTO(); String uriStr = "cifs://" + (string)pool.host + (string)pool.path; share.uri = new Uri(uriStr); return Utils.NormalizePath(share.UncPath); } else if (poolType == StoragePoolType.Filesystem) { return pool.path; } throw new ArgumentException("Couldn't parse path for pool type " + poolTypeStr); } // POST api/HypervResource/StartupCommand [HttpPost] [ActionName(CloudStackTypes.StartupCommand)] public JContainer StartupCommand([FromBody]dynamic cmdArray) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(cmdArray.ToString()); // Log agent configuration logger.Info("Agent StartupRoutingCommand received " + cmdArray.ToString()); dynamic strtRouteCmd = cmdArray[0][CloudStackTypes.StartupRoutingCommand]; // Insert networking details string privateIpAddress = strtRouteCmd.privateIpAddress; config.PrivateIpAddress = privateIpAddress; string subnet; System.Net.NetworkInformation.NetworkInterface privateNic = GetNicInfoFromIpAddress(privateIpAddress, out subnet); strtRouteCmd.privateIpAddress = privateIpAddress; strtRouteCmd.privateNetmask = subnet; strtRouteCmd.privateMacAddress = privateNic.GetPhysicalAddress().ToString(); string storageip = strtRouteCmd.storageIpAddress; System.Net.NetworkInformation.NetworkInterface storageNic = GetNicInfoFromIpAddress(storageip, out subnet); strtRouteCmd.storageIpAddress = storageip; strtRouteCmd.storageNetmask = subnet; strtRouteCmd.storageMacAddress = storageNic.GetPhysicalAddress().ToString(); strtRouteCmd.gatewayIpAddress = storageNic.GetPhysicalAddress().ToString(); strtRouteCmd.hypervisorVersion = System.Environment.OSVersion.Version.Major.ToString() + "." + System.Environment.OSVersion.Version.Minor.ToString(); strtRouteCmd.caps = "hvm"; dynamic details = strtRouteCmd.hostDetails; if (details != null) { string productVersion = System.Environment.OSVersion.Version.Major.ToString() + "." + System.Environment.OSVersion.Version.Minor.ToString(); details.Add("product_version", productVersion); details.Add("rdp.server.port", 2179); } // Detect CPUs, speed, memory uint cores; uint mhz; uint sockets; wmiCallsV2.GetProcessorResources(out sockets, out cores, out mhz); strtRouteCmd.cpus = cores; strtRouteCmd.speed = mhz; strtRouteCmd.cpuSockets = sockets; ulong memoryKBs; ulong freeMemoryKBs; wmiCallsV2.GetMemoryResources(out memoryKBs, out freeMemoryKBs); strtRouteCmd.memory = memoryKBs * 1024; // Convert to bytes // Need 2 Gig for DOM0, see http://technet.microsoft.com/en-us/magazine/hh750394.aspx strtRouteCmd.dom0MinMemory = config.ParentPartitionMinMemoryMb * 1024 * 1024; // Convert to bytes // Insert storage pool details. // // Read the localStoragePath for virtual disks from the Hyper-V configuration // See http://blogs.msdn.com/b/virtual_pc_guy/archive/2010/05/06/managing-the-default-virtual-machine-location-with-hyper-v.aspx // for discussion of Hyper-V file locations paths. string virtualDiskFolderPath = wmiCallsV2.GetDefaultVirtualDiskFolder(); if (virtualDiskFolderPath != null) { // GUID arbitrary. Host agents deals with storage pool in terms of localStoragePath. // We use HOST guid. string poolGuid = strtRouteCmd.guid; if (poolGuid == null) { poolGuid = Guid.NewGuid().ToString(); logger.InfoFormat("Setting Startup StoragePool GUID to " + poolGuid); } else { logger.InfoFormat("Setting Startup StoragePool GUID same as HOST, i.e. " + poolGuid); } long capacity; long available; GetCapacityForPath(virtualDiskFolderPath, out capacity, out available); logger.Debug(CloudStackTypes.StartupStorageCommand + " set available bytes to " + available); string ipAddr = strtRouteCmd.privateIpAddress; var vmStates = wmiCallsV2.GetVmSync(config.PrivateIpAddress); strtRouteCmd.vms = Utils.CreateCloudStackMapObject(vmStates); StoragePoolInfo pi = new StoragePoolInfo( poolGuid.ToString(), ipAddr, virtualDiskFolderPath, virtualDiskFolderPath, StoragePoolType.Filesystem.ToString(), capacity, available); // Build StartupStorageCommand using an anonymous type // See http://stackoverflow.com/a/6029228/939250 object ansContent = new { poolInfo = pi, guid = pi.uuid, dataCenter = strtRouteCmd.dataCenter, resourceType = StorageResourceType.STORAGE_POOL.ToString(), // TODO: check encoding contextMap = contextMap }; JObject ansObj = Utils.CreateCloudStackObject(CloudStackTypes.StartupStorageCommand, ansContent); cmdArray.Add(ansObj); } // Convert result to array for type correctness? logger.Info(CloudStackTypes.StartupCommand + " result is " + cmdArray.ToString()); return cmdArray; } } // POST api/HypervResource/GetVncPortCommand [HttpPost] [ActionName(CloudStackTypes.GetVncPortCommand)] public JContainer GetVncPortCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.GetVncPortCommand + Utils.CleanString(cmd.ToString())); string details = null; bool result = false; string address = null; int port = -9; try { string vmName = (string)cmd.name; var sys = wmiCallsV2.GetComputerSystem(vmName); address = "instanceId=" + sys.Name ; result = true; } catch (Exception sysEx) { details = CloudStackTypes.GetVncPortAnswer + " failed due to " + sysEx.Message; logger.Error(details, sysEx); } object ansContent = new { result = result, details = details, address = address, port = port }; return ReturnCloudStackTypedJArray(ansContent, CloudStackTypes.GetVncPortAnswer); } } // POST api/HypervResource/HostVmStateReportCommand [HttpPost] [ActionName(CloudStackTypes.HostVmStateReportCommand)] public JContainer HostVmStateReportCommand([FromBody]dynamic cmd) { using (log4net.NDC.Push(Guid.NewGuid().ToString())) { logger.Info(CloudStackTypes.HostVmStateReportCommand + Utils.CleanString(cmd.ToString())); string details = null; List<Dictionary<string, string>> hostVmStateReport = new List<Dictionary<string, string>>(); try { var vmCollection = wmiCallsV2.GetComputerSystemCollection(); foreach (ComputerSystem vm in vmCollection) { if (EnabledState.ToCloudStackPowerState(vm.EnabledState).Equals("PowerOn")) { var dict = new Dictionary<string, string>(); dict.Add(vm.ElementName, EnabledState.ToCloudStackPowerState(vm.EnabledState)); hostVmStateReport.Add(dict); } if (EnabledState.ToCloudStackPowerState(vm.EnabledState).Equals("PowerOff")) { string note = wmiCallsV2.GetVmNote((wmiCallsV2.GetVmSettings(vm)).Path); if (note != null && note.Contains("CloudStack")) { if (!note.Contains("creating")) { try { wmiCallsV2.DestroyVm(vm.ElementName); } catch (Exception ex) { logger.Error("Failed to delete stopped VMs due to " + ex.Message); } } else { var dict = new Dictionary<string, string>(); dict.Add(vm.ElementName, "PowerOn"); hostVmStateReport.Add(dict); } } } } } catch (Exception sysEx) { details = CloudStackTypes.HostVmStateReportCommand + " failed due to " + sysEx.Message; logger.Error(details, sysEx); } var answer = JArray.FromObject(hostVmStateReport); logger.Info(String.Format("{0}: {1}",CloudStackTypes.HostVmStateReportCommand, answer.ToString())); return answer; } } public static System.Net.NetworkInformation.NetworkInterface GetNicInfoFromIpAddress(string ipAddress, out string subnet) { System.Net.NetworkInformation.NetworkInterface[] nics = System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces(); System.Net.NetworkInformation.NetworkInterface defaultnic = null; foreach (var nic in nics) { subnet = null; defaultnic = nic; // TODO: use to remove NETMASK and MAC from the config file, and to validate the IPAddress. var nicProps = nic.GetIPProperties(); bool found = false; foreach (var addr in nicProps.UnicastAddresses) { if (addr.Address.Equals(IPAddress.Parse(ipAddress))) { subnet = addr.IPv4Mask.ToString(); found = true; } } if (!found) { continue; } return nic; } var defaultSubnet = defaultnic.GetIPProperties().UnicastAddresses[0]; subnet = defaultSubnet.IPv4Mask.ToString(); return defaultnic; } public static void GetCapacityForLocalPath(string localStoragePath, out long capacityBytes, out long availableBytes) { // NB: DriveInfo does not work for remote folders (http://stackoverflow.com/q/1799984/939250) // DriveInfo requires a driver letter... string fullPath = Path.GetFullPath(localStoragePath); System.IO.DriveInfo poolInfo = new System.IO.DriveInfo(fullPath); capacityBytes = poolInfo.TotalSize; availableBytes = poolInfo.AvailableFreeSpace; // Don't allow all of the Root Device to be used for virtual disks if (poolInfo.RootDirectory.Name.ToLower().Equals(config.RootDeviceName)) { availableBytes -= config.RootDeviceReservedSpaceBytes; availableBytes = availableBytes > 0 ? availableBytes : 0; capacityBytes -= config.RootDeviceReservedSpaceBytes; capacityBytes = capacityBytes > 0 ? capacityBytes : 0; } } public static void GetCapacityForPath(String hostPath, out long capacityBytes, out long availableBytes) { if (hostPath.Substring(0, 2) == "\\\\") { Utils.GetShareDetails(hostPath, out capacityBytes, out availableBytes); } else { GetCapacityForLocalPath(hostPath, out capacityBytes, out availableBytes); } } } }