diff --git a/XenModel/XenAPI-Extensions/Pool.cs b/XenModel/XenAPI-Extensions/Pool.cs index e222e0cf4..40e325e88 100644 --- a/XenModel/XenAPI-Extensions/Pool.cs +++ b/XenModel/XenAPI-Extensions/Pool.cs @@ -462,6 +462,7 @@ namespace XenAPI public const string NEW_UPLOAD_REQUEST = "NewUploadRequest"; public const string HEALTH_CHECK_PIPE = "HealthCheckServicePipe"; public const string HEALTH_CHECK_PIPE_END_MESSAGE = "HealthCheckServicePipe"; + public const string UPLOAD_UUID = "UploadUuid"; public CallHomeSettings(CallHomeStatus status, int intervalInDays, DayOfWeek dayOfWeek, int timeOfDay, int retryInterval) { diff --git a/XenServerHealthCheck/XenServerHealthCheck.csproj b/XenServerHealthCheck/XenServerHealthCheck.csproj index 397c1e807..3844b0173 100755 --- a/XenServerHealthCheck/XenServerHealthCheck.csproj +++ b/XenServerHealthCheck/XenServerHealthCheck.csproj @@ -48,6 +48,7 @@ + @@ -55,14 +56,16 @@ - + True True Settings.settings - + + + Component diff --git a/XenServerHealthCheck/XenServerHealthCheckBundleUpload.cs b/XenServerHealthCheck/XenServerHealthCheckBundleUpload.cs new file mode 100644 index 000000000..b2f18bd79 --- /dev/null +++ b/XenServerHealthCheck/XenServerHealthCheckBundleUpload.cs @@ -0,0 +1,210 @@ +/* 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.Generic; +using System.IO; +using System.Threading; +using XenAdmin.Core; +using XenAdmin.Network; +using XenAPI; + + +namespace XenServerHealthCheck +{ + public class XenServerHealthCheckBundleUpload + { + public XenServerHealthCheckBundleUpload(IXenConnection _connection) + { + connection = _connection; + } + + private IXenConnection connection; + + private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); + public const int TIMEOUT = 24 * 60 * 60 * 1000; + public const int VERBOSITY_LEVEL = 2; + + public void runUpload() + { + DateTime startTime = DateTime.UtcNow; + string uploadToken = ""; + Session session = new Session(connection.Hostname, 80); + session.APIVersion = API_Version.LATEST; + + try + { + session.login_with_password(connection.Username, connection.Password); + connection.LoadCache(session); + var pool = Helpers.GetPoolOfOne(connection); + if (pool != null) + { + uploadToken = pool.CallHomeSettings.GetSecretyInfo(connection, CallHomeSettings.UPLOAD_TOKEN_SECRET); + } + + if (string.IsNullOrEmpty(uploadToken)) + { + if (session != null) + session.logout(); + session = null; + log.ErrorFormat("The upload token is not retrieved for {0}", connection.Hostname); + updateCallHomeSettings(false, startTime); + return; + } + + } + catch (Exception e) + { + if (session != null) + session.logout(); + session = null; + log.Error(e, e); + updateCallHomeSettings(false, startTime); + return; + } + + try + { + CancellationTokenSource cts = new CancellationTokenSource(); + Func upload = delegate() + { + try + { + return bundleUpload(connection, session, uploadToken, cts.Token); + } + catch (OperationCanceledException) + { + return ""; + } + }; + System.Threading.Tasks.Task task = new System.Threading.Tasks.Task(upload); + task.Start(); + if (!task.Wait(TIMEOUT)) + { + cts.Cancel(); + updateCallHomeSettings(false, startTime); + task.Wait(); + return; + } + + if (task.Status == System.Threading.Tasks.TaskStatus.RanToCompletion) + { + string upload_uuid = task.Result; + if(!string.IsNullOrEmpty(upload_uuid)) + updateCallHomeSettings(true, startTime, upload_uuid); + else + updateCallHomeSettings(false, startTime); + } + else + updateCallHomeSettings(false, startTime); + } + catch (Exception e) + { + if (session != null) + session.logout(); + session = null; + log.Error(e, e); + updateCallHomeSettings(false, startTime); + } + + } + + public void updateCallHomeSettings(bool success, DateTime time, string uploadUuid = "") + { + Session session = new Session(connection.Hostname, 80); + session.login_with_password(connection.Username, connection.Password); + connection.LoadCache(session); + + // Round-trip format time + DateTime rtime = DateTime.SpecifyKind(time, DateTimeKind.Utc); + string stime = rtime.ToString("o"); + + // record upload_uuid, + // release the lock, + // set the time of LAST_SUCCESSFUL_UPLOAD or LAST_FAILED_UPLOAD + Dictionary config = Pool.get_health_check_config(session, connection.Cache.Pools[0].opaque_ref); + config[CallHomeSettings.UPLOAD_LOCK] = ""; + if (success) + { + config[CallHomeSettings.LAST_SUCCESSFUL_UPLOAD] = stime; + config[CallHomeSettings.UPLOAD_UUID] = uploadUuid; + } + else + config[CallHomeSettings.LAST_FAILED_UPLOAD] = stime; + Pool.set_health_check_config(session, connection.Cache.Pools[0].opaque_ref, config); + + if (session != null) + session.logout(); + session = null; + } + + public string bundleUpload(IXenConnection connection, Session session, string uploadToken, System.Threading.CancellationToken cancel) + { + // Collect the server status report and generate zip file to upload. + XenServerHealthCheckBugTool bugTool = new XenServerHealthCheckBugTool(); + try + { + bugTool.RunBugtool(connection, session); + } + catch (Exception e) + { + if (session != null) + session.logout(); + session = null; + log.Error(e, e); + return ""; + } + + string bundleToUpload = bugTool.outputFile; + if(string.IsNullOrEmpty(bundleToUpload) || !File.Exists(bundleToUpload)) + { + log.ErrorFormat("Server Status Report is NOT collected"); + return ""; + } + + // Upload the zip file to CIS uploading server. + XenServerHealthCheckUpload upload = new XenServerHealthCheckUpload(uploadToken, VERBOSITY_LEVEL); + string upload_uuid = upload.UploadZip(bundleToUpload, cancel); + if (File.Exists(bundleToUpload)) + File.Delete(bundleToUpload); + + // Return the uuid of upload. + if(string.IsNullOrEmpty(upload_uuid)) + { + // Fail to upload the zip to CIS server. + log.ErrorFormat("Fail to upload the Server Status Report {0} to CIS server", bundleToUpload); + return ""; + } + + return upload_uuid; + } + } +} diff --git a/XenServerHealthCheck/XenServerHealthCheckService.cs b/XenServerHealthCheck/XenServerHealthCheckService.cs index 1fb891d16..efd0e6fad 100755 --- a/XenServerHealthCheck/XenServerHealthCheckService.cs +++ b/XenServerHealthCheck/XenServerHealthCheckService.cs @@ -132,9 +132,17 @@ namespace XenServerHealthCheck session.login_with_password(server.UserName, server.Password); connectionInfo.LoadCache(session); if (RequestUploadTask.Request(connectionInfo, session) || RequestUploadTask.OnDemandRequest(connectionInfo, session)) - { - //Create thread to do log uploading - log.InfoFormat("Will upload report for XenServer {0}", connectionInfo.Hostname); + { + // Create a task to collect server status report and upload to CIS server + log.InfoFormat("Start to upload server status report for XenServer {0}", connectionInfo.Hostname); + + XenServerHealthCheckBundleUpload upload = new XenServerHealthCheckBundleUpload(connectionInfo); + Action uploadAction = delegate() + { + upload.runUpload(); + }; + System.Threading.Tasks.Task task = new System.Threading.Tasks.Task(uploadAction); + task.Start(); } session.logout(); session = null; diff --git a/XenServerHealthCheck/XenServerHealthCheckUpload.cs b/XenServerHealthCheck/XenServerHealthCheckUpload.cs new file mode 100644 index 000000000..66e755e6a --- /dev/null +++ b/XenServerHealthCheck/XenServerHealthCheckUpload.cs @@ -0,0 +1,216 @@ +/* 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.Generic; +using System.Linq; +using System.Text; +using System.IO; +using System.Net; +using System.Web.Script.Serialization; + + +namespace XenServerHealthCheck +{ + public class XenServerHealthCheckUpload + { + private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); + public const string UPLOAD_URL = "https://rttf-staging.citrix.com/feeds/api/"; + public const int CHUNK_SIZE = 1 * 1024 * 1024; + private JavaScriptSerializer serializer; + private int verbosityLevel; + + private string uploadToken; + + public XenServerHealthCheckUpload(string token, int verbosity) + { + uploadToken = token; + verbosityLevel = verbosity; + serializer = new JavaScriptSerializer(); + serializer.MaxJsonLength = int.MaxValue; + } + + // Request an upload and fetch the uploading id from CIS. + public string InitiateUpload(long size) + { + // Request a new bundle upload to CIS server. + Uri uri = new Uri(UPLOAD_URL + "bundle/?size=" + size.ToString()); + HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri); + if (uploadToken != null) + { + request.Headers.Add("Authorization", "BT " + uploadToken); + } + + request.Method = "POST"; + request.ContentType = "application/json"; + request.ServicePoint.Expect100Continue = false; + + using (var streamWriter = new StreamWriter(request.GetRequestStream())) + { + // Add the log verbosity level in json cotent. + string verbosity = "{\"verbosity\":\"2\"}"; + streamWriter.Write(verbosity); + } + + Dictionary res = new Dictionary(); + try + { + HttpWebResponse response = (HttpWebResponse)request.GetResponse(); + using (StreamReader reader = new StreamReader(response.GetResponseStream())) + { + string respString = reader.ReadToEnd(); + res = (Dictionary)serializer.DeserializeObject(respString); + } + response.Close(); + } + catch (WebException e) + { + log.ErrorFormat("Exception while requesting a new CIS uploading: {0}", e); + return ""; + } + + if (res.ContainsKey("id")) + { + // Get the uuid of uploading + return (string)res["id"]; + } + else + { + // Fail to initialize the upload request + return ""; + } + } + + // Upload a chunk. + public bool UploadChunk(string address, string filePath, long offset, long thisChunkSize, string uploadToken) + { + Uri uri = new Uri(address); + HttpWebRequest req = (HttpWebRequest)WebRequest.Create(uri); + req.Method = "POST"; + req.ContentType = "application/octet-stream"; + req.Headers.Add("Authorization", "BT " + uploadToken); + + using (Stream destination = req.GetRequestStream()) + { + using (FileStream source = File.Open(filePath, FileMode.Open)) + { + source.Position = offset; + int bytes = Convert.ToInt32(thisChunkSize); + byte[] buffer = new byte[CHUNK_SIZE]; + int read; + while (bytes > 0 && + (read = source.Read(buffer, 0, Math.Min(buffer.Length, bytes))) > 0) + { + destination.Write(buffer, 0, read); + bytes -= read; + } + } + HttpWebResponse resp = req.GetResponse() as HttpWebResponse; + HttpStatusCode statusCode = resp.StatusCode; + + // After sending every chunk upload request to server, response will contain a status indicating if it is complete. + using (StreamReader reader = new StreamReader(resp.GetResponseStream())) + { + string respString = reader.ReadToEnd(); + Dictionary res = (Dictionary)serializer.DeserializeObject(respString); + log.InfoFormat("The status of chunk upload: {0}", res["status"]); + } + + resp.Close(); + + if (statusCode == HttpStatusCode.OK) + return true; + else + return false; + } + } + + // Upload the zip file. + public string UploadZip(string fileName, System.Threading.CancellationToken cancel) + { + FileInfo fileInfo = new FileInfo(fileName); + long size = fileInfo.Length; + + // Fetch the upload UUID from CIS server. + string uploadUuid = InitiateUpload(size); + if (string.IsNullOrEmpty(uploadUuid)) + { + log.ErrorFormat("Cannot fetch the upload UUID from CIS server"); + return ""; + } + + // Start to upload zip file. + log.InfoFormat("Upload ID: {0}", uploadUuid); + StringBuilder url = new StringBuilder(UPLOAD_URL + "upload_raw_chunk/?id=" + uploadUuid); + + long offset = 0; + while (offset < size) + { + url.Append(String.Format("&offset={0}", offset)); + long remainingSize = size - offset; + long thisChunkSize = (remainingSize > CHUNK_SIZE) ? CHUNK_SIZE : remainingSize; + + try + { + for (int i = 0; i < 3; i++) + { + if (cancel.IsCancellationRequested) + { + return ""; + } + + if (UploadChunk(url.ToString(), fileName, offset, thisChunkSize, uploadToken)) + { + // This chunk is successfully uploaded + offset += thisChunkSize; + break; + } + + // Fail to upload the chunk for 3 times so fail the bundle upload. + log.ErrorFormat("Fail to upload the chunk"); + return ""; + } + } + catch (Exception e) + { + log.Error(e, e); + return ""; + } + + } + + log.InfoFormat("Succeed to upload bundle {0}", fileName); + return uploadUuid; + + } + + } +} diff --git a/XenServerHealthCheck/app.config b/XenServerHealthCheck/app.config index 85260dc05..4df9082cb 100755 --- a/XenServerHealthCheck/app.config +++ b/XenServerHealthCheck/app.config @@ -1,21 +1,22 @@ -
+
+ - - - - - - + + + + + + - + @@ -23,4 +24,19 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + \ No newline at end of file