├── .babelrc ├── .gitignore ├── Controllers ├── AllocationController.cs ├── ClientController.cs ├── DashboardController.cs ├── ErrorController.cs ├── EvaluationController.cs ├── JobController.cs ├── ServerController.cs └── ToolsController.cs ├── Extensions └── DateTimeExtension.cs ├── LICENSE ├── Models ├── Allocation.cs ├── Client.cs ├── Dashboard.cs ├── Evaluation.cs ├── Job.cs └── Server.cs ├── Nomad.csproj ├── Nomad.csproj.user ├── Program.cs ├── Properties └── launchSettings.json ├── README.md ├── Startup.cs ├── Views ├── Components │ ├── Allocation.jsx │ ├── Allocations.jsx │ ├── App.jsx │ ├── Client.jsx │ ├── Clients.jsx │ ├── Dashboard.jsx │ ├── Evaluation.jsx │ ├── Evaluations.jsx │ ├── Job.jsx │ ├── Jobs.jsx │ ├── Server.jsx │ └── Servers.jsx ├── Error.cshtml ├── Index.cshtml ├── Shared │ └── Header.cshtml └── index.js ├── appsettings.Development.json ├── appsettings.json ├── bundleconfig.json ├── package.json ├── webpack.config.js └── wwwroot ├── css ├── bootstrap.css ├── bootstrap.min.css ├── site.css └── site.min.css ├── images ├── collage.png ├── loading.gif └── nomad.png └── js ├── site.js └── site.min.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vs 2 | .vscode 3 | bin 4 | node_modules 5 | obj 6 | yarn.lock 7 | -------------------------------------------------------------------------------- /Controllers/AllocationController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Newtonsoft.Json; 8 | using Nomad.Models; 9 | 10 | namespace Nomad.Controllers 11 | { 12 | public class AllocationController : Controller 13 | { 14 | private static readonly string NomadUrl = Environment.GetEnvironmentVariable("NOMAD_URL"); 15 | 16 | [HttpGet("/allocations")] 17 | public IActionResult Index() 18 | { 19 | return View("~/Views/Index.cshtml"); 20 | } 21 | 22 | [HttpGet("/api/allocations")] 23 | public async Task GetAllocationsAsJsonResult(string search) 24 | { 25 | var allocations = await GetAllocationsAsync(); 26 | 27 | if (!String.IsNullOrEmpty(search)) 28 | { 29 | allocations = allocations.Where(a => a.Name.ToLower().Contains(search.ToLower())).ToList(); 30 | } 31 | 32 | return Json(allocations); 33 | } 34 | 35 | [HttpGet("/allocation")] 36 | public IActionResult Allocation() 37 | { 38 | return View("~/Views/Index.cshtml"); 39 | } 40 | 41 | [HttpGet("/api/allocation")] 42 | public async Task GetAllocationAsJsonResult(string id) 43 | { 44 | var allocation = await GetAllocationAsync(id); 45 | allocation.Stats = await GetAllocationStatsAsync(allocation.Resources.Networks.FirstOrDefault().IP, id); 46 | allocation.Logs = await GetAllocationLogsAsync(allocation.Resources.Networks.FirstOrDefault().IP, id); 47 | 48 | return Json(allocation); 49 | } 50 | 51 | [HttpGet("/allocation/log")] 52 | public async Task Log(string client, string id, string log) 53 | { 54 | var content = await GetAllocationLogAsync(client, id, log); 55 | 56 | return Content(content); 57 | } 58 | 59 | public async Task> GetAllocationsAsync() 60 | { 61 | List allocations; 62 | 63 | using (HttpClient client = new HttpClient()) 64 | using (HttpResponseMessage response = await client.GetAsync(NomadUrl + "/v1/allocations")) 65 | using (HttpContent content = response.Content) 66 | { 67 | var result = await content.ReadAsStringAsync(); 68 | 69 | allocations = JsonConvert.DeserializeObject>(result); 70 | 71 | foreach (var allocation in allocations) 72 | { 73 | if (allocation.TaskStates == null) 74 | continue; 75 | 76 | foreach (var key in allocation.TaskStates.Keys) 77 | { 78 | if (allocation.DesiredStatus == "run" && allocation.TaskStates[key].State == "running") { allocation.Running++; } 79 | if (allocation.DesiredStatus == "run" && allocation.TaskStates[key].State == "pending") { allocation.Pending++; } 80 | if (allocation.DesiredStatus == "run" && allocation.TaskStates[key].State == "dead") { allocation.Dead++; } 81 | } 82 | } 83 | } 84 | 85 | return allocations.OrderBy(a => a.Name).ToList(); 86 | } 87 | 88 | public async Task GetAllocationAsync(string id) 89 | { 90 | using (HttpClient client = new HttpClient()) 91 | using (HttpResponseMessage response = await client.GetAsync(NomadUrl + "/v1/allocation/" + id)) 92 | using (HttpContent content = response.Content) 93 | { 94 | var result = await content.ReadAsStringAsync(); 95 | 96 | return JsonConvert.DeserializeObject(result); 97 | } 98 | } 99 | 100 | public List GetAllocationEvents(List allocations, int count) 101 | { 102 | var events = new List(); 103 | 104 | foreach (var allocation in allocations) 105 | { 106 | if (allocation.TaskStates == null) 107 | continue; 108 | 109 | foreach (var key in allocation.TaskStates.Keys) 110 | { 111 | foreach (var @event in allocation.TaskStates[key].Events) 112 | { 113 | @event.AllocationID = allocation.ID; 114 | @event.AllocationName = allocation.Name; 115 | events.Add(@event); 116 | } 117 | } 118 | } 119 | 120 | if (count > 0) 121 | return events.OrderByDescending(e => e.DateTime).Take(15).ToList(); 122 | 123 | return events.OrderByDescending(e => e.DateTime).ToList(); 124 | } 125 | 126 | public async Task GetAllocationStatsAsync(string client, string id) 127 | { 128 | using (HttpClient httpClient = new HttpClient()) 129 | using (HttpResponseMessage response = await httpClient.GetAsync("http://" + client + ":4646/v1/client/allocation/" + id + "/stats")) 130 | using (HttpContent content = response.Content) 131 | { 132 | var result = await content.ReadAsStringAsync(); 133 | 134 | return JsonConvert.DeserializeObject(result); 135 | } 136 | } 137 | 138 | public async Task> GetAllocationLogsAsync(string client, string id) 139 | { 140 | using (HttpClient httpClient = new HttpClient()) 141 | using (HttpResponseMessage response = await httpClient.GetAsync("http://" + client + ":4646/v1/client/fs/ls/" + id + "?path=/alloc/logs")) 142 | using (HttpContent content = response.Content) 143 | { 144 | var result = await content.ReadAsStringAsync(); 145 | 146 | return JsonConvert.DeserializeObject>(result); 147 | } 148 | } 149 | 150 | public async Task GetAllocationLogAsync(string client, string id, string log) 151 | { 152 | using (HttpClient httpClient = new HttpClient()) 153 | using (HttpResponseMessage response = await httpClient.GetAsync("http://" + client + ":4646/v1/client/fs/cat/" + id + "?path=/alloc/logs/" + log)) 154 | using (HttpContent content = response.Content) 155 | { 156 | return await content.ReadAsStringAsync(); 157 | } 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /Controllers/ClientController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Newtonsoft.Json; 8 | using Nomad.Models; 9 | 10 | namespace Nomad.Controllers 11 | { 12 | public class ClientController : Controller 13 | { 14 | private static readonly string NomadUrl = Environment.GetEnvironmentVariable("NOMAD_URL"); 15 | 16 | [HttpGet("/clients")] 17 | public IActionResult Index() 18 | { 19 | return View("~/Views/Index.cshtml"); 20 | } 21 | 22 | [HttpGet("/api/clients")] 23 | public async Task GetClientsAsJsonResult(string search) 24 | { 25 | var clients = await GetClientsAsync(); 26 | 27 | if (!String.IsNullOrEmpty(search)) 28 | { 29 | clients = clients.Where(c => c.Name.ToLower().Contains(search.ToLower())).ToList(); 30 | } 31 | 32 | return Json(clients); 33 | } 34 | 35 | [HttpGet("/client")] 36 | public IActionResult Client() 37 | { 38 | return View("~/Views/Index.cshtml"); 39 | } 40 | 41 | [HttpGet("/api/client")] 42 | public async Task GetClientAsJsonResult(string id) 43 | { 44 | var client = await GetClientAsync(id); 45 | client.Stats = await GetClientStatsAsync(client.Resources.Networks.FirstOrDefault().IP); 46 | client.Allocations = await GetClientAllocationsAsync(id); 47 | 48 | return Json(client); 49 | } 50 | 51 | public async Task> GetClientsAsync() 52 | { 53 | List clients; 54 | 55 | using (HttpClient httpClient = new HttpClient()) 56 | using (HttpResponseMessage response = await httpClient.GetAsync(NomadUrl + "/v1/nodes")) 57 | using (HttpContent content = response.Content) 58 | { 59 | var result = await content.ReadAsStringAsync(); 60 | 61 | clients = JsonConvert.DeserializeObject>(result); 62 | 63 | foreach (var client in clients) 64 | { 65 | if (client.Status == "ready") { client.Up++; } 66 | if (client.Status == "down" && !client.Drain) { client.Down++; } 67 | if (client.Status == "down" && client.Drain) { client.Draining++; } 68 | } 69 | } 70 | 71 | return clients.OrderBy(c => c.Name).ToList(); 72 | } 73 | 74 | public async Task GetClientAsync(string id) 75 | { 76 | using (HttpClient client = new HttpClient()) 77 | using (HttpResponseMessage response = await client.GetAsync(NomadUrl + "/v1/node/" + id)) 78 | using (HttpContent content = response.Content) 79 | { 80 | var result = await content.ReadAsStringAsync(); 81 | 82 | return JsonConvert.DeserializeObject(result); 83 | } 84 | } 85 | 86 | public async Task GetClientStatsAsync(string client) 87 | { 88 | using (HttpClient httpClient = new HttpClient()) 89 | using (HttpResponseMessage response = await httpClient.GetAsync("http://" + client + ":4646/v1/client/stats")) 90 | using (HttpContent content = response.Content) 91 | { 92 | var result = await content.ReadAsStringAsync(); 93 | 94 | return JsonConvert.DeserializeObject(result); 95 | } 96 | } 97 | 98 | public async Task> GetClientAllocationsAsync(string id) 99 | { 100 | using (HttpClient client = new HttpClient()) 101 | using (HttpResponseMessage response = await client.GetAsync(NomadUrl + "/v1/node/" + id + "/allocations")) 102 | using (HttpContent content = response.Content) 103 | { 104 | var result = await content.ReadAsStringAsync(); 105 | 106 | return JsonConvert.DeserializeObject>(result); 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Controllers/DashboardController.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Nomad.Models; 4 | using System; 5 | 6 | namespace Nomad.Controllers 7 | { 8 | public class DashboardController : Controller 9 | { 10 | public IActionResult Index() 11 | { 12 | return View("~/Views/Index.cshtml"); 13 | } 14 | 15 | [HttpGet("/api/dashboard")] 16 | public async Task GetDashboardAsJsonResult() 17 | { 18 | var jobs = new JobController().GetJobsAsync(); 19 | var allocations = new AllocationController().GetAllocationsAsync(); 20 | var clients = new ClientController().GetClientsAsync(); 21 | var servers = new ServerController().GetServersAsync(); 22 | await System.Threading.Tasks.Task.WhenAll(jobs, allocations, clients, servers); 23 | 24 | var dashboard = new Dashboard 25 | { 26 | Jobs = await jobs, 27 | Allocations = await allocations, 28 | Clients = await clients, 29 | Servers = await servers, 30 | Events = new AllocationController().GetAllocationEvents(await allocations, 15) 31 | }; 32 | 33 | return Json(dashboard); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Controllers/ErrorController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Diagnostics; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace Nomad.Controllers 5 | { 6 | public class ErrorController : Controller 7 | { 8 | [HttpGet("/error")] 9 | public IActionResult ExceptionHandler() 10 | { 11 | var exception = HttpContext.Features.Get(); 12 | 13 | ViewData["StatusCode"] = HttpContext.Response.StatusCode; 14 | ViewData["Message"] = exception.Error.Message; 15 | ViewData["StackTrace"] = exception.Error.StackTrace; 16 | 17 | return View("~/Views/Error.cshtml"); 18 | } 19 | 20 | [HttpGet("/error/{0}")] 21 | public IActionResult StatusCodeHandler() 22 | { 23 | ViewData["StatusCode"] = HttpContext.Response.StatusCode; 24 | 25 | return View("~/Views/Error.cshtml"); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Controllers/EvaluationController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Newtonsoft.Json; 8 | using Nomad.Models; 9 | 10 | namespace Nomad.Controllers 11 | { 12 | public class EvaluationController : Controller 13 | { 14 | private static readonly string NomadUrl = Environment.GetEnvironmentVariable("NOMAD_URL"); 15 | 16 | [HttpGet("/evaluations")] 17 | public IActionResult Index() 18 | { 19 | return View("~/Views/Index.cshtml"); 20 | } 21 | 22 | [HttpGet("/api/evaluations")] 23 | public async Task GetEvaluationsAsJsonResult(string search) 24 | { 25 | var evaluations = await GetEvaluationsAsync(); 26 | 27 | if (!String.IsNullOrEmpty(search)) 28 | { 29 | evaluations = evaluations.Where(e => e.JobID.ToLower().Contains(search.ToLower())).ToList(); 30 | } 31 | 32 | return Json(evaluations); 33 | } 34 | 35 | [HttpGet("/evaluation")] 36 | public IActionResult Evaluation(string id) 37 | { 38 | return View("~/Views/Index.cshtml"); 39 | } 40 | 41 | [HttpGet("/api/evaluation")] 42 | public async Task GetEvaluationAsJsonResult(string id) 43 | { 44 | var evaluation = await GetEvaluationAsync(id); 45 | evaluation.Allocations = await GetEvaluationAllocationsAsync(id); 46 | 47 | return Json(evaluation); 48 | } 49 | 50 | public async Task> GetEvaluationsAsync() 51 | { 52 | List evaluations; 53 | 54 | using (HttpClient client = new HttpClient()) 55 | using (HttpResponseMessage response = await client.GetAsync(NomadUrl + "/v1/evaluations")) 56 | using (HttpContent content = response.Content) 57 | { 58 | var result = await content.ReadAsStringAsync(); 59 | 60 | evaluations = JsonConvert.DeserializeObject>(result); 61 | } 62 | 63 | return evaluations.OrderBy(e => e.JobID).ToList(); 64 | } 65 | 66 | public async Task GetEvaluationAsync(string id) 67 | { 68 | using (HttpClient client = new HttpClient()) 69 | using (HttpResponseMessage response = await client.GetAsync(NomadUrl + "/v1/evaluation/" + id)) 70 | using (HttpContent content = response.Content) 71 | { 72 | var result = await content.ReadAsStringAsync(); 73 | 74 | return JsonConvert.DeserializeObject(result); 75 | } 76 | } 77 | 78 | public async Task> GetEvaluationAllocationsAsync(string id) 79 | { 80 | using (HttpClient client = new HttpClient()) 81 | using (HttpResponseMessage response = await client.GetAsync(NomadUrl + "/v1/evaluation/" + id + "/allocations")) 82 | using (HttpContent content = response.Content) 83 | { 84 | var result = await content.ReadAsStringAsync(); 85 | 86 | return JsonConvert.DeserializeObject>(result); 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Controllers/JobController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Newtonsoft.Json; 8 | using Nomad.Models; 9 | 10 | namespace Nomad.Controllers 11 | { 12 | public class JobController : Controller 13 | { 14 | private static readonly string NomadUrl = Environment.GetEnvironmentVariable("NOMAD_URL"); 15 | 16 | [HttpGet("/jobs")] 17 | public IActionResult Index() 18 | { 19 | return View("~/Views/Index.cshtml"); 20 | } 21 | 22 | [HttpGet("/api/jobs")] 23 | public async Task GetJobsAsJsonResult(string search) 24 | { 25 | var jobs = await GetJobsAsync(); 26 | 27 | if (!String.IsNullOrEmpty(search)) 28 | { 29 | jobs = jobs.Where(j => j.ID.ToLower().Contains(search.ToLower())).ToList(); 30 | } 31 | 32 | return Json(jobs); 33 | } 34 | 35 | [HttpGet("/job")] 36 | public IActionResult Job() 37 | { 38 | return View("~/Views/Index.cshtml"); 39 | } 40 | 41 | [HttpGet("/api/job")] 42 | public async Task GetJobAsJsonResult(string id) 43 | { 44 | var job = await GetJobAsync(id); 45 | job.Evaluations = await GetJobEvaluationsAsync(id); 46 | job.Allocations = await GetJobAllocationsAsync(id); 47 | 48 | return Json(job); 49 | } 50 | 51 | public async Task> GetJobsAsync() 52 | { 53 | List jobs; 54 | 55 | using (HttpClient client = new HttpClient()) 56 | using (HttpResponseMessage response = await client.GetAsync(NomadUrl + "/v1/jobs")) 57 | using (HttpContent content = response.Content) 58 | { 59 | var result = await content.ReadAsStringAsync(); 60 | 61 | jobs = JsonConvert.DeserializeObject>(result); 62 | 63 | foreach (var job in jobs) 64 | { 65 | if (job.Status == "running") { job.Running++; } 66 | if (job.Status == "pending") { job.Pending++; } 67 | if (job.Status == "dead") { job.Dead++; } 68 | 69 | job.NumOfTaskGroups = job.JobSummary.Summary.Keys.Count(); 70 | 71 | foreach (var value in job.JobSummary.Summary.Values) 72 | { 73 | job.QueuedTaskGroups = job.QueuedTaskGroups + value.Queued; 74 | job.CompleteTaskGroups = job.CompleteTaskGroups + value.Complete; 75 | job.FailedTaskGroups = job.FailedTaskGroups + value.Failed; 76 | job.RunningTaskGroups = job.RunningTaskGroups + value.Running; 77 | job.StartingTaskGroups = job.StartingTaskGroups + value.Starting; 78 | job.LostTaskGroups = job.LostTaskGroups + value.Lost; 79 | } 80 | } 81 | } 82 | 83 | return jobs.OrderBy(j => j.Name).ToList(); 84 | } 85 | 86 | public async Task GetJobAsync(string id) 87 | { 88 | using (HttpClient client = new HttpClient()) 89 | using (HttpResponseMessage response = await client.GetAsync(NomadUrl + "/v1/job/" + id)) 90 | using (HttpContent content = response.Content) 91 | { 92 | var result = await content.ReadAsStringAsync(); 93 | 94 | return JsonConvert.DeserializeObject(result); 95 | } 96 | } 97 | 98 | public async Task> GetJobEvaluationsAsync(string id) 99 | { 100 | using (HttpClient client = new HttpClient()) 101 | using (HttpResponseMessage response = await client.GetAsync(NomadUrl + "/v1/job/" + id + "/evaluations")) 102 | using (HttpContent content = response.Content) 103 | { 104 | var result = await content.ReadAsStringAsync(); 105 | 106 | return JsonConvert.DeserializeObject>(result); 107 | } 108 | } 109 | 110 | public async Task> GetJobAllocationsAsync(string id) 111 | { 112 | using (HttpClient client = new HttpClient()) 113 | using (HttpResponseMessage response = await client.GetAsync(NomadUrl + "/v1/job/" + id + "/allocations")) 114 | using (HttpContent content = response.Content) 115 | { 116 | var result = await content.ReadAsStringAsync(); 117 | 118 | return JsonConvert.DeserializeObject>(result); 119 | } 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Controllers/ServerController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Newtonsoft.Json; 8 | using Nomad.Models; 9 | 10 | namespace Nomad.Controllers 11 | { 12 | public class ServerController : Controller 13 | { 14 | private static readonly string NomadUrl = Environment.GetEnvironmentVariable("NOMAD_URL"); 15 | 16 | [HttpGet("/servers")] 17 | public IActionResult Index() 18 | { 19 | return View("~/Views/Index.cshtml"); 20 | } 21 | 22 | [HttpGet("/api/servers")] 23 | public async Task GetServersAsJsonResult(string search) 24 | { 25 | var agents = await GetServersAsync(); 26 | 27 | if (!String.IsNullOrEmpty(search)) 28 | { 29 | agents = agents.Where(a => a.Name.ToLower().Contains(search.ToLower())).ToList(); 30 | } 31 | 32 | return Json(agents); 33 | } 34 | 35 | [HttpGet("/server")] 36 | public IActionResult Server() 37 | { 38 | return View("~/Views/Index.cshtml"); 39 | } 40 | 41 | [HttpGet("/api/server")] 42 | public async Task Server(string ip) 43 | { 44 | var agent = await GetServerAsync(ip); 45 | agent.Member.Operator = await GetOperatorsAsync(); 46 | 47 | return Json(agent); 48 | } 49 | 50 | public async Task> GetServersAsync() 51 | { 52 | Agent agent; 53 | Operator @operator = await GetOperatorsAsync(); 54 | List members = new List(); 55 | 56 | using (HttpClient client = new HttpClient()) 57 | using (HttpResponseMessage response = await client.GetAsync(NomadUrl + "/v1/agent/members")) 58 | using (HttpContent content = response.Content) 59 | { 60 | var result = await content.ReadAsStringAsync(); 61 | 62 | agent = JsonConvert.DeserializeObject(result); 63 | 64 | foreach (var member in agent.Members) 65 | { 66 | if (member.Status == "alive") { member.Up++; } 67 | if (member.Status == "dead") { member.Down++; } 68 | 69 | foreach (var server in @operator.Servers) 70 | { 71 | if (member.Name == server.Node) 72 | { 73 | member.Voter = server.Voter; 74 | member.Leader = server.Leader; 75 | } 76 | } 77 | 78 | members.Add(member); 79 | } 80 | } 81 | 82 | return members.OrderBy(m => m.Name).ToList(); 83 | } 84 | 85 | public async Task GetServerAsync(string ip) 86 | { 87 | using (HttpClient client = new HttpClient()) 88 | using (HttpResponseMessage response = await client.GetAsync("http://" + ip + ":4646/v1/agent/self")) 89 | using (HttpContent content = response.Content) 90 | { 91 | var result = await content.ReadAsStringAsync(); 92 | 93 | return JsonConvert.DeserializeObject(result); 94 | } 95 | } 96 | 97 | public async Task GetOperatorsAsync() 98 | { 99 | using (HttpClient client = new HttpClient()) 100 | using (HttpResponseMessage response = await client.GetAsync(NomadUrl + "/v1/operator/raft/configuration")) 101 | using (HttpContent content = response.Content) 102 | { 103 | var result = await content.ReadAsStringAsync(); 104 | 105 | return JsonConvert.DeserializeObject(result); 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Controllers/ToolsController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | namespace Nomad.Controllers 8 | { 9 | public class ToolsController : Controller 10 | { 11 | private static readonly string NomadUrl = Environment.GetEnvironmentVariable("NOMAD_URL"); 12 | 13 | [HttpGet("/tools/gc")] 14 | public async Task GarbageCollection() 15 | { 16 | var httpContent = new StringContent("", Encoding.UTF8, "application/json"); 17 | 18 | using (HttpClient client = new HttpClient()) 19 | using (HttpResponseMessage response = await client.PutAsync(NomadUrl + "/v1/system/gc", httpContent)) 20 | { 21 | return response; 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Extensions/DateTimeExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Nomad.Extensions 4 | { 5 | public static class DateTimeExtension 6 | { 7 | private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); 8 | public static DateTime FromUnixTime(this long unixTime) 9 | => Epoch.AddTicks(unixTime / 100).ToLocalTime(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Christopher van Dal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Models/Allocation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Nomad.Extensions; 4 | 5 | namespace Nomad.Models 6 | { 7 | public class Allocation 8 | { 9 | public string ID { get; set; } 10 | public string EvalID { get; set; } 11 | public string Name { get; set; } 12 | public string NodeID { get; set; } 13 | public string JobID { get; set; } 14 | public string TaskGroup { get; set; } 15 | public string DesiredStatus { get; set; } 16 | public string DesiredDescription { get; set; } 17 | public string ClientStatus { get; set; } 18 | public string ClientDescription { get; set; } 19 | public Dictionary TaskStates { get; set; } 20 | public long CreateIndex { get; set; } 21 | public long ModifyIndex { get; set; } 22 | public long CreateTime { get; set; } 23 | public Job Job { get; set; } 24 | public Resources Resources { get; set; } 25 | public SharedResources SharedResources { get; set; } 26 | public Dictionary TaskResources { get; set; } 27 | public Metrics Metrics { get; set; } 28 | public string PreviousAllocation { get; set; } 29 | public int AllocModifyIndex { get; set; } 30 | 31 | // Custom Properties 32 | public int Running { get; set; } 33 | public int Pending { get; set; } 34 | public int Dead { get; set; } 35 | public DateTime CreateDateTime => CreateTime.FromUnixTime(); 36 | public Stats Stats { get; set; } 37 | public List Logs { get; set; } 38 | } 39 | 40 | public class TaskName 41 | { 42 | public string State { get; set; } 43 | public bool Failed { get; set; } 44 | public List Events { get; set; } 45 | public object Pids { get; set; } 46 | public ResourceUsage ResourceUsage { get; set; } 47 | } 48 | 49 | public class Event 50 | { 51 | public string Type { get; set; } 52 | public long Time { get; set; } 53 | public bool FailsTask { get; set; } 54 | public string RestartReason { get; set; } 55 | public string SetupError { get; set; } 56 | public string DriverError { get; set; } 57 | public int ExitCode { get; set; } 58 | public long Signal { get; set; } 59 | public string Message { get; set; } 60 | public long KillTimeout { get; set; } 61 | public string KillError { get; set; } 62 | public string KillReason { get; set; } 63 | public long StartDelay { get; set; } 64 | public string DownloadError { get; set; } 65 | public string ValidationError { get; set; } 66 | public int DiskLimit { get; set; } 67 | public string FailedSibling { get; set; } 68 | public string VaultError { get; set; } 69 | public string TaskSignalReason { get; set; } 70 | public string TaskSignal { get; set; } 71 | public string DriverMessage { get; set; } 72 | 73 | // Custom Properties 74 | public string AllocationID { get; set; } 75 | public string AllocationName { get; set; } 76 | public DateTime DateTime => Time.FromUnixTime(); 77 | } 78 | 79 | public class SharedResources 80 | { 81 | public int CPU { get; set; } 82 | public int MemoryMB { get; set; } 83 | public int DiskMB { get; set; } 84 | public int IOPS { get; set; } 85 | public object Networks { get; set; } 86 | } 87 | 88 | public class Metrics 89 | { 90 | public int NodesEvaluated { get; set; } 91 | public int NodesFiltered { get; set; } 92 | public object ClassFiltered { get; set; } 93 | public int NodesExhausted { get; set; } 94 | public object ClassExhausted { get; set; } 95 | public object DimensionExhausted { get; set; } 96 | public int AllocationTime { get; set; } 97 | public int CoalescedFailures { get; set; } 98 | } 99 | 100 | public class Log 101 | { 102 | public string FileMode { get; set; } 103 | public bool IsDir { get; set; } 104 | public string ModTime { get; set; } 105 | public string Name { get; set; } 106 | public long Size { get; set; } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Models/Client.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Nomad.Models 4 | { 5 | public class Client 6 | { 7 | public string ID { get; set; } 8 | public string Datacenter { get; set; } 9 | public string Name { get; set; } 10 | public string NodeClass { get; set; } 11 | public bool Drain { get; set; } 12 | public string Status { get; set; } 13 | public string StatusDescription { get; set; } 14 | public long CreateIndex { get; set; } 15 | public long ModifyIndex { get; set; } 16 | public string SecretID { get; set; } 17 | public string HTTPAddr { get; set; } 18 | public bool TLSEnabled { get; set; } 19 | public Dictionary Attributes { get; set; } 20 | public Resources Resources { get; set; } 21 | public Reserved Reserved { get; set; } 22 | public Dictionary Links { get; set; } 23 | public Dictionary Meta { get; set; } 24 | public string ComputedClass { get; set; } 25 | public int StatusUpdatedAt { get; set; } 26 | 27 | // Custom Properties 28 | public int Up { get; set; } 29 | public int Down { get; set; } 30 | public int Draining { get; set; } 31 | public Stats Stats { get; set; } 32 | public List Allocations { get; set; } 33 | } 34 | 35 | public class Stats 36 | { 37 | public ResourceUsage ResourceUsage { get; set; } 38 | public Dictionary Tasks { get; set; } 39 | public long Timestamp { get; set; } 40 | public AllocDirStats AllocDirStats { get; set; } 41 | public List CPU { get; set; } 42 | public double CPUTicksConsumed { get; set; } 43 | public List DiskStats { get; set; } 44 | public Memory Memory { get; set; } 45 | public int Uptime { get; set; } 46 | } 47 | 48 | public class ResourceUsage 49 | { 50 | public CpuStats CpuStats { get; set; } 51 | public MemoryStats MemoryStats { get; set; } 52 | } 53 | 54 | public class CpuStats 55 | { 56 | public List Measured { get; set; } 57 | public double Percent { get; set; } 58 | public double SystemMode { get; set; } 59 | public int ThrottledPeriods { get; set; } 60 | public int ThrottledTime { get; set; } 61 | public double TotalTicks { get; set; } 62 | public double UserMode { get; set; } 63 | } 64 | 65 | public class MemoryStats 66 | { 67 | public int Cache { get; set; } 68 | public int KernelMaxUsage { get; set; } 69 | public int KernelUsage { get; set; } 70 | public int MaxUsage { get; set; } 71 | public List Measured { get; set; } 72 | public int RSS { get; set; } 73 | public int Swap { get; set; } 74 | } 75 | 76 | public class Reserved 77 | { 78 | public int CPU { get; set; } 79 | public int MemoryMB { get; set; } 80 | public int DiskMB { get; set; } 81 | public int IOPS { get; set; } 82 | public List Networks { get; set; } 83 | } 84 | 85 | public class AllocDirStats 86 | { 87 | public long Available { get; set; } 88 | public string Device { get; set; } 89 | public double InodesUsedPercent { get; set; } 90 | public string Mountpoint { get; set; } 91 | public long Size { get; set; } 92 | public long Used { get; set; } 93 | public double UsedPercent { get; set; } 94 | } 95 | 96 | public class ClientCPU 97 | { 98 | public string CPU { get; set; } 99 | public double Idle { get; set; } 100 | public double System { get; set; } 101 | public double Total { get; set; } 102 | public double User { get; set; } 103 | } 104 | 105 | public class DiskStat 106 | { 107 | public long Available { get; set; } 108 | public string Device { get; set; } 109 | public double InodesUsedPercent { get; set; } 110 | public string Mountpoint { get; set; } 111 | public long Size { get; set; } 112 | public long Used { get; set; } 113 | public double UsedPercent { get; set; } 114 | } 115 | 116 | public class Memory 117 | { 118 | public long Available { get; set; } 119 | public long Free { get; set; } 120 | public long Total { get; set; } 121 | public long Used { get; set; } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Models/Dashboard.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace Nomad.Models 5 | { 6 | public class Dashboard 7 | { 8 | public List Jobs { get; set; } 9 | public int RunningJobs => Jobs.Sum(j => j.Running); 10 | public int PendingJobs => Jobs.Sum(j => j.Pending); 11 | public int DeadJobs => Jobs.Sum(j => j.Dead); 12 | 13 | public List Allocations { get; set; } 14 | public int RunningAllocations => Allocations.Sum(a => a.Running); 15 | public int PendingAllocations => Allocations.Sum(a => a.Pending); 16 | public int DeadAllocations => Allocations.Sum(a => a.Dead); 17 | 18 | public List Clients { get; set; } 19 | public int UpClients => Clients.Sum(c => c.Up); 20 | public int DownClients => Clients.Sum(c => c.Down); 21 | public int DrainingClients => Clients.Sum(c => c.Draining); 22 | 23 | public List Servers { get; set; } 24 | public int UpServers => Servers.Sum(s => s.Up); 25 | public int DownServers => Servers.Sum(s => s.Down); 26 | 27 | public List Events { get; set; } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Models/Evaluation.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Nomad.Models 4 | { 5 | public class Evaluation 6 | { 7 | public string ID { get; set; } 8 | public int Priority { get; set; } 9 | public string Type { get; set; } 10 | public string TriggeredBy { get; set; } 11 | public string JobID { get; set; } 12 | public long JobModifyIndex { get; set; } 13 | public string NodeID { get; set; } 14 | public long NodeModifyIndex { get; set; } 15 | public string Status { get; set; } 16 | public string StatusDescription { get; set; } 17 | public long Wait { get; set; } 18 | public string NextEval { get; set; } 19 | public string PreviousEval { get; set; } 20 | public string BlockedEval { get; set; } 21 | public object FailedTGAllocs { get; set; } 22 | //public ClassEligibility ClassEligibility { get; set; } 23 | public bool EscapedComputedClass { get; set; } 24 | public bool AnnotatePlan { get; set; } 25 | public Dictionary QueuedAllocations { get; set; } 26 | public long SnapshotIndex { get; set; } 27 | public long CreateIndex { get; set; } 28 | public long ModifyIndex { get; set; } 29 | 30 | // Custom Properties 31 | public List Allocations { get; set; } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Models/Job.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Nomad.Models 4 | { 5 | public class Job 6 | { 7 | public string ID { get; set; } 8 | public string ParentID { get; set; } 9 | public string Name { get; set; } 10 | public string Type { get; set; } 11 | public int Priority { get; set; } 12 | public string Status { get; set; } 13 | public string StatusDescription { get; set; } 14 | public JobSummary JobSummary { get; set; } 15 | public long CreateIndex { get; set; } 16 | public long ModifyIndex { get; set; } 17 | public long JobModifyIndex { get; set; } 18 | public string Region { get; set; } 19 | public bool AllAtOnce { get; set; } 20 | public List Datacenters { get; set; } 21 | public List Constraints { get; set; } 22 | public List TaskGroups { get; set; } 23 | public Update Update { get; set; } 24 | public object Periodic { get; set; } 25 | public object ParameterizedJob { get; set; } 26 | public object Payload { get; set; } 27 | public Dictionary Meta { get; set; } 28 | public string VaultToken { get; set; } 29 | 30 | // Custom Properties 31 | public int Running { get; set; } 32 | public int Pending { get; set; } 33 | public int Dead { get; set; } 34 | public int NumOfTaskGroups { get; set; } 35 | public int QueuedTaskGroups { get; set; } 36 | public int CompleteTaskGroups { get; set; } 37 | public int FailedTaskGroups { get; set; } 38 | public int RunningTaskGroups { get; set; } 39 | public int StartingTaskGroups { get; set; } 40 | public int LostTaskGroups { get; set; } 41 | public List Evaluations { get; set; } 42 | public List Allocations { get; set; } 43 | } 44 | 45 | public class JobSummary 46 | { 47 | public string JobID { get; set; } 48 | public Dictionary Summary { get; set; } 49 | public Children Children { get; set; } 50 | public long CreateIndex { get; set; } 51 | public long ModifyIndex { get; set; } 52 | } 53 | 54 | public class TaskGroupStatus 55 | { 56 | public int Queued { get; set; } 57 | public int Complete { get; set; } 58 | public int Failed { get; set; } 59 | public int Running { get; set; } 60 | public int Starting { get; set; } 61 | public int Lost { get; set; } 62 | } 63 | 64 | public class Children 65 | { 66 | public int Pending { get; set; } 67 | public int Running { get; set; } 68 | public int Dead { get; set; } 69 | } 70 | 71 | public class Constraint 72 | { 73 | public string LTarget { get; set; } 74 | public string RTarget { get; set; } 75 | public string Operand { get; set; } 76 | } 77 | 78 | public class TaskGroup 79 | { 80 | public string Name { get; set; } 81 | public int Count { get; set; } 82 | public List Constraints { get; set; } 83 | public RestartPolicy RestartPolicy { get; set; } 84 | public List Tasks { get; set; } 85 | public EphemeralDisk EphemeralDisk { get; set; } 86 | public Dictionary Meta { get; set; } 87 | } 88 | 89 | public class RestartPolicy 90 | { 91 | public int Attempts { get; set; } 92 | public object Interval { get; set; } 93 | public object Delay { get; set; } 94 | public string Mode { get; set; } 95 | } 96 | 97 | public class Task 98 | { 99 | public string Name { get; set; } 100 | public string Driver { get; set; } 101 | public string User { get; set; } 102 | public Config Config { get; set; } 103 | public Dictionary Env { get; set; } 104 | public List Services { get; set; } 105 | public object Vault { get; set; } 106 | public List Templates { get; set; } 107 | public List Constraints { get; set; } 108 | public Resources Resources { get; set; } 109 | public object DispatchPayload { get; set; } 110 | public Dictionary Meta { get; set; } 111 | public object KillTimeout { get; set; } 112 | public LogConfig LogConfig { get; set; } 113 | public List Artifacts { get; set; } 114 | public bool Leader { get; set; } 115 | } 116 | 117 | public class Config 118 | { 119 | public string Image { get; set; } 120 | public string Network_Mode { get; set; } 121 | public List Args { get; set; } 122 | public string Command { get; set; } 123 | } 124 | 125 | public class Service 126 | { 127 | public string Name { get; set; } 128 | public string PortLabel { get; set; } 129 | public List Tags { get; set; } 130 | public List Checks { get; set; } 131 | } 132 | 133 | public class Check 134 | { 135 | public string Name { get; set; } 136 | public string Type { get; set; } 137 | public string Command { get; set; } 138 | public List Args { get; set; } 139 | public string Path { get; set; } 140 | public string Protocol { get; set; } 141 | public string PortLabel { get; set; } 142 | public object Interval { get; set; } 143 | public int Timeout { get; set; } 144 | public string InitialStatus { get; set; } 145 | } 146 | 147 | public class Resources 148 | { 149 | public int CPU { get; set; } 150 | public int MemoryMB { get; set; } 151 | public int DiskMB { get; set; } 152 | public int IOPS { get; set; } 153 | public List Networks { get; set; } 154 | } 155 | 156 | public class Network 157 | { 158 | public string Device { get; set; } 159 | public string CIDR { get; set; } 160 | public string IP { get; set; } 161 | public int MBits { get; set; } 162 | public object ReservedPorts { get; set; } 163 | public List DynamicPorts { get; set; } 164 | } 165 | 166 | public class DynamicPort 167 | { 168 | public string Label { get; set; } 169 | public int Value { get; set; } 170 | } 171 | 172 | public class LogConfig 173 | { 174 | public int MaxFiles { get; set; } 175 | public int MaxFileSizeMB { get; set; } 176 | } 177 | 178 | public class EphemeralDisk 179 | { 180 | public bool Sticky { get; set; } 181 | public int SizeMB { get; set; } 182 | public bool Migrate { get; set; } 183 | } 184 | 185 | public class Update 186 | { 187 | public long Stagger { get; set; } 188 | public int MaxParallel { get; set; } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /Models/Server.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Nomad.Models 4 | { 5 | public class Agent 6 | { 7 | public string ServerName { get; set; } 8 | public string ServerRegion { get; set; } 9 | public string ServerDC { get; set; } 10 | public List Members { get; set; } 11 | public Member Member { get; set; } 12 | public ServerConfig Config { get; set; } 13 | public ServerStats Stats { get; set; } 14 | } 15 | 16 | public class Member 17 | { 18 | public string Name { get; set; } 19 | public string Addr { get; set; } 20 | public int Port { get; set; } 21 | public Tags Tags { get; set; } 22 | public string Status { get; set; } 23 | public int ProtocolMin { get; set; } 24 | public int ProtocolMax { get; set; } 25 | public int ProtocolCur { get; set; } 26 | public int DelegateMin { get; set; } 27 | public int DelegateMax { get; set; } 28 | public int DelegateCur { get; set; } 29 | 30 | 31 | // Custom Properties 32 | public int Up { get; set; } 33 | public int Down { get; set; } 34 | public bool Leader { get; set; } 35 | public bool Voter { get; set; } 36 | public Operator Operator { get; set; } 37 | } 38 | 39 | public class Tags 40 | { 41 | public string Build { get; set; } 42 | public string Port { get; set; } 43 | public string Expect { get; set; } 44 | public string Role { get; set; } 45 | public string Region { get; set; } 46 | public string DC { get; set; } 47 | public string VSN { get; set; } 48 | public string MNV { get; set; } 49 | } 50 | 51 | public class Operator 52 | { 53 | public long Index { get; set; } 54 | public List Servers { get; set; } 55 | } 56 | 57 | public class Server 58 | { 59 | public string Address { get; set; } 60 | public string ID { get; set; } 61 | public bool Leader { get; set; } 62 | public string Node { get; set; } 63 | public bool Voter { get; set; } 64 | } 65 | 66 | public class ServerConfig 67 | { 68 | public Addresses Addresses { get; set; } 69 | public AdvertiseAddrs AdvertiseAddrs { get; set; } 70 | public Atlas Atlas { get; set; } 71 | public string BindAddr { get; set; } 72 | public Client Client { get; set; } 73 | public Consul Consul { get; set; } 74 | public string DataDir { get; set; } 75 | public string Datacenter { get; set; } 76 | public bool DevMode { get; set; } 77 | public bool DisableAnonymousSignature { get; set; } 78 | public bool DisableUpdateCheck { get; set; } 79 | public bool EnableDebug { get; set; } 80 | public bool EnableSyslog { get; set; } 81 | public List Files { get; set; } 82 | //public HTTPAPIResponseHeaders HTTPAPIResponseHeaders { get; set; } 83 | public bool LeaveOnInt { get; set; } 84 | public bool LeaveOnTerm { get; set; } 85 | public string LogLevel { get; set; } 86 | public string NodeName { get; set; } 87 | public Ports Ports { get; set; } 88 | public string Region { get; set; } 89 | public string Revision { get; set; } 90 | public Server Server { get; set; } 91 | public string SyslogFacility { get; set; } 92 | public TLSConfig TLSConfig { get; set; } 93 | public Telemetry Telemetry { get; set; } 94 | public Vault Vault { get; set; } 95 | public string Version { get; set; } 96 | public string VersionPrerelease { get; set; } 97 | } 98 | 99 | public class Addresses 100 | { 101 | public string HTTP { get; set; } 102 | public string RPC { get; set; } 103 | public string Serf { get; set; } 104 | } 105 | 106 | public class AdvertiseAddrs 107 | { 108 | public string HTTP { get; set; } 109 | public string RPC { get; set; } 110 | public string Serf { get; set; } 111 | } 112 | 113 | public class Atlas 114 | { 115 | public string Endpoint { get; set; } 116 | public string Infrastructure { get; set; } 117 | public bool Join { get; set; } 118 | } 119 | 120 | public class Consul 121 | { 122 | public string Addr { get; set; } 123 | public string Auth { get; set; } 124 | public bool AutoAdvertise { get; set; } 125 | public string CAFile { get; set; } 126 | public string CertFile { get; set; } 127 | public bool ChecksUseAdvertise { get; set; } 128 | public bool ClientAutoJoin { get; set; } 129 | public string ClientServiceName { get; set; } 130 | public bool EnableSSL { get; set; } 131 | public string KeyFile { get; set; } 132 | public bool ServerAutoJoin { get; set; } 133 | public string ServerServiceName { get; set; } 134 | public long Timeout { get; set; } 135 | public string Token { get; set; } 136 | public bool VerifySSL { get; set; } 137 | } 138 | 139 | public class Ports 140 | { 141 | public int HTTP { get; set; } 142 | public int RPC { get; set; } 143 | public int Serf { get; set; } 144 | } 145 | 146 | public class TLSConfig 147 | { 148 | public string CAFile { get; set; } 149 | public string CertFile { get; set; } 150 | public bool EnableHTTP { get; set; } 151 | public bool EnableRPC { get; set; } 152 | public string KeyFile { get; set; } 153 | public bool VerifyServerHostname { get; set; } 154 | } 155 | 156 | public class Telemetry 157 | { 158 | public string CirconusAPIApp { get; set; } 159 | public string CirconusAPIToken { get; set; } 160 | public string CirconusAPIURL { get; set; } 161 | public string CirconusBrokerID { get; set; } 162 | public string CirconusBrokerSelectTag { get; set; } 163 | public string CirconusCheckDisplayName { get; set; } 164 | public string CirconusCheckForceMetricActivation { get; set; } 165 | public string CirconusCheckID { get; set; } 166 | public string CirconusCheckInstanceID { get; set; } 167 | public string CirconusCheckSearchTag { get; set; } 168 | public string CirconusCheckSubmissionURL { get; set; } 169 | public string CirconusCheckTags { get; set; } 170 | public string CirconusSubmissionInterval { get; set; } 171 | public string CollectionInterval { get; set; } 172 | public string DataDogAddr { get; set; } 173 | public bool DisableHostname { get; set; } 174 | public bool PublishAllocationMetrics { get; set; } 175 | public bool PublishNodeMetrics { get; set; } 176 | public string StatsdAddr { get; set; } 177 | public string StatsiteAddr { get; set; } 178 | public bool UseNodeName { get; set; } 179 | } 180 | 181 | public class Vault 182 | { 183 | public string Addr { get; set; } 184 | public bool AllowUnauthenticated { get; set; } 185 | public long ConnectionRetryIntv { get; set; } 186 | public object Enabled { get; set; } 187 | public string Role { get; set; } 188 | public string TLSCaFile { get; set; } 189 | public string TLSCaPath { get; set; } 190 | public string TLSCertFile { get; set; } 191 | public string TLSKeyFile { get; set; } 192 | public string TLSServerName { get; set; } 193 | public object TLSSkipVerify { get; set; } 194 | public string TaskTokenTTL { get; set; } 195 | public string Token { get; set; } 196 | } 197 | 198 | public class ServerStats 199 | { 200 | public Raft Raft { get; set; } 201 | public Serf Serf { get; set; } 202 | public Runtime Runtime { get; set; } 203 | public Nomad Nomad { get; set; } 204 | } 205 | 206 | public class Raft 207 | { 208 | public string Applied_Index { get; set; } 209 | public string Last_Snapshot_Index { get; set; } 210 | public string Protocol_Version_Min { get; set; } 211 | public string State { get; set; } 212 | public string Last_Log_Index { get; set; } 213 | public string Latest_Configuration_Index { get; set; } 214 | public string Latest_Configuration { get; set; } 215 | public string Num_Peers { get; set; } 216 | public string Term { get; set; } 217 | public string Snapshot_Version_Max { get; set; } 218 | public string Protocol_Version { get; set; } 219 | public string Snapshot_Version_Min { get; set; } 220 | public string Last_Contact { get; set; } 221 | public string Last_Log_Term { get; set; } 222 | public string Commit_Index { get; set; } 223 | public string Protocol_Version_Max { get; set; } 224 | public string Fsm_Pending { get; set; } 225 | public string Last_Snapshot_Term { get; set; } 226 | } 227 | 228 | public class Serf 229 | { 230 | public string Members { get; set; } 231 | public string Failed { get; set; } 232 | public string Left { get; set; } 233 | public string Event_Time { get; set; } 234 | public string Query_Time { get; set; } 235 | public string Intent_Queue { get; set; } 236 | public string Health_Score { get; set; } 237 | public string Member_Time { get; set; } 238 | public string Event_Queue { get; set; } 239 | public string Query_Queue { get; set; } 240 | public string Encrypted { get; set; } 241 | } 242 | 243 | public class Runtime 244 | { 245 | public string Cpu_Count { get; set; } 246 | public string Arch { get; set; } 247 | public string Version { get; set; } 248 | public string Max_Procs { get; set; } 249 | public string GoRoutines { get; set; } 250 | } 251 | 252 | public class Nomad 253 | { 254 | public string Leader_Addr { get; set; } 255 | public string Bootstrap { get; set; } 256 | public string Known_Regions { get; set; } 257 | public string Server { get; set; } 258 | public string Leader { get; set; } 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /Nomad.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp1.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Nomad.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | IIS Express 5 | false 6 | 7 | -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Hosting; 7 | 8 | namespace Nomad 9 | { 10 | public class Program 11 | { 12 | public static void Main(string[] args) 13 | { 14 | var host = new WebHostBuilder() 15 | .UseKestrel() 16 | .UseContentRoot(Directory.GetCurrentDirectory()) 17 | .UseIISIntegration() 18 | .UseStartup() 19 | .Build(); 20 | 21 | host.Run(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:63449/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": false, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "Nomad": { 19 | "commandName": "Project" 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # With the upcoming release of Nomad 0.7 and its awesome built in UI, this project is no longer under development. 2 | 3 | # Nomad UI 4 | 5 | A cross platform UI for Nomad by HashiCorp written in .NET Core and React. 6 | 7 | ## Usage 8 | 1. Run `docker run -itd -e "NOMAD_URL=http://:" -e "ASPNETCORE_URLS=http://*:5000" -p 5000:5000 cvandal/nomad-ui:1.0.0` 9 | 10 | ## Development 11 | ### Prerequisites 12 | 1. Download and install the latest version of .NET Core from https://www.microsoft.com/net/download/core 13 | 2. Download and install the latest version of Node.js from https://nodejs.org/en/download/ 14 | 3. Download and install the latest version of Yarn from https://yarnpkg.com/en/docs/install 15 | 4. Clone the repository from GitHub by running `git clone https://github.com/cvandal/nomad-ui.git` 16 | 17 | ### Build 18 | 1. Run `yarn`, followed by `.\node_modules\.bin\webpack` 19 | 2. Run `dotnet restore`, followed by `dotnet build`, followed by `dotnet publish` 20 | 3. Run `cd .\bin\Debug\netcoreapp1.1\publish\` and create a file named `Dockerfile` with the following content: 21 | ``` 22 | FROM microsoft/dotnet 23 | 24 | COPY . /app 25 | 26 | WORKDIR /app 27 | 28 | ENV ASPNETCORE_URLS=http://*:5000 29 | 30 | ENTRYPOINT ["dotnet", "Nomad.dll"] 31 | ``` 32 | 4. Run `docker build -t : .` 33 | ### Run 34 | 1. Run `docker run -itd -e "NOMAD_URL=http://:" -p 5000:5000 :` 35 | 36 | ## Discuss 37 | Join the HashiCorp Community Slack team! https://join.slack.com/t/hashicorpcommunity/shared_invite/MjE5NzE3ODI3NzE2LTE1MDE0NDM4OTYtZDI1MTNlMTJmNw 38 | 39 | ## Screenshots 40 | ![alt text](https://github.com/cvandal/nomad-ui/raw/master/wwwroot/images/collage.png "Nomad UI") 41 | 42 | ## Known Issues 43 | `¯\_(ツ)_/¯` 44 | -------------------------------------------------------------------------------- /Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace Nomad 8 | { 9 | public class Startup 10 | { 11 | public Startup(IHostingEnvironment env) 12 | { 13 | var builder = new ConfigurationBuilder() 14 | .SetBasePath(env.ContentRootPath) 15 | .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) 16 | .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) 17 | .AddEnvironmentVariables(); 18 | Configuration = builder.Build(); 19 | } 20 | 21 | public IConfigurationRoot Configuration { get; } 22 | 23 | public void ConfigureServices(IServiceCollection services) 24 | { 25 | services.AddMvc(); 26 | } 27 | 28 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) 29 | { 30 | loggerFactory.AddConsole(Configuration.GetSection("Logging")); 31 | loggerFactory.AddDebug(); 32 | 33 | app.UseExceptionHandler("/error"); 34 | app.UseStatusCodePagesWithReExecute("/error/{0}"); 35 | 36 | app.UseStaticFiles(); 37 | 38 | app.UseMvc(routes => 39 | { 40 | routes.MapRoute( 41 | name: "default", 42 | template: "{controller=Dashboard}/{action=Index}/{id?}"); 43 | }); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Views/Components/Allocation.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import axios from 'axios'; 3 | import { 4 | defaults, 5 | Doughnut 6 | } from 'react-chartjs-2'; 7 | import moment from 'moment'; 8 | 9 | global.jQuery = require('jquery'); 10 | var bootstrap = require('bootstrap'); 11 | 12 | export default class Allocation extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | 16 | this.state = { 17 | allocation: null, 18 | error: null, 19 | currentLog: { 20 | loading: false, 21 | contents: '' 22 | } 23 | } 24 | } 25 | 26 | componentDidMount() { 27 | const makeRequest = () => 28 | axios.get("/api/allocation" + this.props.location.search) 29 | .then(({ data }) => this.setState({ allocation: data })) 30 | .catch((error) => this.setState({ error: error })) 31 | 32 | this.serverRequest = makeRequest(); 33 | 34 | this.poll = setInterval(() => { 35 | this.serverRequest = makeRequest(); 36 | }, 3000); 37 | } 38 | 39 | componentWillUnmount() { 40 | this.serverRequest.abort(); 41 | clearInterval(this.poll); 42 | } 43 | 44 | handleLogClick(logName) { 45 | const { allocation: { id, resources: { networks } } } = this.state; 46 | const { ip } = networks[0] 47 | 48 | return e => { 49 | e.preventDefault() 50 | 51 | this.setState({ 52 | currentLog: { 53 | loading: true, 54 | contents: '' 55 | } 56 | }) 57 | 58 | fetch(`/allocation/log?client=${ip}&id=${id}&log=${logName}`) 59 | .then(res => res.text()) 60 | .then(contents => this.setState({ 61 | currentLog: { 62 | loading: false, 63 | contents, 64 | } 65 | })) 66 | } 67 | } 68 | 69 | render() { 70 | const { allocation, error } = this.state; 71 | 72 | if (error) { 73 | return ( 74 |
75 |
76 |

Oops!

77 |

Something went wrong and the allocation you've selected could not be loaded.

78 |
79 |
80 | ) 81 | } 82 | 83 | if (allocation === null) { 84 | return null 85 | } 86 | 87 | var idleCpu = (100 - allocation.stats.resourceUsage.cpuStats.percent); 88 | var activeCpu = allocation.stats.resourceUsage.cpuStats.percent; 89 | var availableMem = (allocation.resources.memoryMB - (allocation.stats.resourceUsage.memoryStats.maxUsage / 1024) / 1024); 90 | var consumedMem = ((allocation.stats.resourceUsage.memoryStats.maxUsage / 1024) / 1024); 91 | 92 | return ( 93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |

Allocation Properties

101 |
102 |
103 |
    104 |
  • ID: {allocation.id}
  • 105 |
  • Name: {allocation.name}
  • 106 |
  • Task Group: {allocation.taskGroup}
  • 107 |
  • Desired Status: {allocation.desiredStatus}
  • 108 |
  • Job: {allocation.jobID}
  • 109 |
  • Evaluation: {allocation.evalID}
  • 110 |
  • Client: {allocation.nodeID}
  • 111 |
  • Client Description: {allocation.clientDescription}
  • 112 |
  • Client Status: {allocation.clientStatus}
  • 113 |
  • Create Time: {moment(allocation.createDateTime).format('DD/MM/YYYY hh:mm:ss A')}
  • 114 |
115 |
116 |
117 |
118 | 119 |
120 |
121 |
122 |

Allocation Resources

123 |
124 |
125 |
    126 |
  • CPU (MHz): {allocation.resources.cpu}
  • 127 |
  • Memory (MB): {allocation.resources.memoryMB}
  • 128 |
  • Disk (MB): {allocation.resources.diskMB}
  • 129 |
  • 130 | Network (Mpbs): 131 |
      132 | {allocation.resources.networks.map((network) => { 133 | return ( 134 |
    • {network.mBits}
    • 135 | ) 136 | })} 137 |
    138 |
  • 139 |
  • 140 | Address: 141 | {allocation.resources.networks.map((network) => { 142 | return ([ 143 | 150 | ]) 151 | })} 152 |
  • 153 |
154 |
155 |
156 |
157 |
158 | 159 |
160 |
161 |
162 |
163 |

CPU Utilisation

164 |
165 |
166 | 191 |
192 |
193 |
194 | 195 |
196 |
197 |
198 |

Memory Utilisation

199 |
200 |
201 | 226 |
227 |
228 |
229 |
230 |
231 | 232 |
233 |
234 |
235 |

Allocation Logs

236 |
237 |
238 |
239 |
    240 | {allocation.logs.map((log) => { 241 | return ( 242 |
  • {log.name}, {log.size} (Bytes)
  • 243 | ) 244 | })} 245 |
246 |
247 | 248 |
249 | {this.state.currentLog.loading &&
Loading...
} 250 | 251 |
252 |                                         {!this.state.currentLog.loading && this.state.currentLog.contents}
253 |                                     
254 |
255 |
256 |
257 |
258 |
259 | 260 |
261 |
262 |
263 |
264 |

Allocation Events

265 |
266 |
267 |
268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | {Object.entries(allocation.taskStates).map(([key]) => allocation.taskStates[key].events.sort((a, b) => a.dateTime < b.dateTime).map((event) => ( 279 | 280 | 281 | 282 | 297 | 298 | 299 | ))).reduce((flattened, rows) => [...flattened, ...rows], [])} 300 | 301 |
NameTypeMessageTime
{allocation.name}{event.type} 283 | {event.restartReason} 284 | {event.setupError} 285 | {event.driverError} 286 | {event.message} 287 | {event.killError} 288 | {event.killReason} 289 | {event.downloadError} 290 | {event.validationError} 291 | {event.failedSibling} 292 | {event.vaultError} 293 | {event.taskSignalReason} 294 | {event.taskSignal} 295 | {event.driverMessage} 296 | {moment(event.dateTime).format('DD/MM/YYYY hh:mm:ss A')}
302 |
303 |
304 |
305 |
306 |
307 |
308 | ); 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /Views/Components/Allocations.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import axios from 'axios'; 3 | import Pager from 'react-pager'; 4 | import moment from 'moment'; 5 | 6 | global.jQuery = require('jquery'); 7 | var bootstrap = require('bootstrap'); 8 | 9 | export default class Allocations extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { 14 | allocations: null, 15 | error: null, 16 | current: 0 17 | } 18 | 19 | this.handlePageChanged = this.handlePageChanged.bind(this); 20 | } 21 | 22 | componentDidMount() { 23 | var self = this; 24 | 25 | const makeRequest = () => { 26 | this.props.location.search ? 27 | axios.get("/api/allocations" + this.props.location.search) 28 | .then(({ data }) => this.setState({ allocations: data })) 29 | .catch((error) => this.setState({ error: error })) : 30 | axios.get("/api/allocations") 31 | .then(({ data }) => this.setState({ allocations: data })) 32 | .catch((error) => this.setState({ error: error })) 33 | } 34 | 35 | this.serverRequest = makeRequest(); 36 | 37 | this.poll = setInterval(() => { 38 | this.serverRequest = makeRequest(); 39 | }, 3000); 40 | } 41 | 42 | componentWillUnmount() { 43 | this.serverRequest.abort(); 44 | clearInterval(this.poll); 45 | } 46 | 47 | handlePageChanged(newPage) { 48 | this.setState({ current: newPage }); 49 | } 50 | 51 | render() { 52 | const { allocations, error, current } = this.state; 53 | const itemsPerPage = 15; 54 | 55 | if (error) { 56 | return ( 57 |
58 |
59 |

Oops!

60 |

Something went wrong and the list of allocations could not be loaded.

61 |
62 |
63 | ) 64 | } 65 | 66 | if (allocations === null) { 67 | return null 68 | } 69 | 70 | var visibleItems = allocations.slice(itemsPerPage * current, (itemsPerPage * current) + itemsPerPage); 71 | 72 | return ( 73 |
74 |
75 |
76 |
77 |
78 |

Allocations

79 |
80 |
81 |
82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | {visibleItems.map((allocation) => { 95 | return ( 96 | 97 | {allocation.desiredStatus === "run" ? 98 | : 99 | 100 | } 101 | 102 | 103 | 104 | 105 | 106 | 107 | ) 108 | })} 109 | 110 |
IDNameTask GroupDesired StatusClient StatusCreate Time
{allocation.id}{allocation.id}{allocation.name}{allocation.taskGroup}{allocation.desiredStatus}{allocation.clientStatus}{moment(allocation.createDateTime).format('DD/MM/YYYY hh:mm:ss A')}
111 |
112 | 113 | {allocations.length > 15 ? 114 |
115 | 121 |
: 122 |
123 | } 124 |
125 |
126 |
127 |
128 |
129 | ); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Views/Components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | BrowserRouter as Router, 4 | Route, 5 | Link 6 | } from 'react-router-dom'; 7 | import Dashboard from './Dashboard.jsx'; 8 | import Jobs from './Jobs.jsx'; 9 | import Job from './Job.jsx'; 10 | import Evaluations from './Evaluations.jsx'; 11 | import Evaluation from './Evaluation.jsx'; 12 | import Allocations from './Allocations.jsx'; 13 | import Allocation from './Allocation.jsx'; 14 | import Clients from './Clients.jsx'; 15 | import Client from './Client.jsx'; 16 | import Servers from './Servers.jsx'; 17 | import Server from './Server.jsx'; 18 | 19 | export default class App extends React.Component { 20 | render() { 21 | return ( 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
36 |
37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Views/Components/Client.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import axios from 'axios'; 3 | import { 4 | defaults, 5 | Doughnut 6 | } from 'react-chartjs-2'; 7 | import moment from 'moment'; 8 | 9 | global.jQuery = require('jquery'); 10 | var bootstrap = require('bootstrap'); 11 | 12 | export default class Client extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | 16 | this.state = { 17 | client: null, 18 | error: null 19 | } 20 | } 21 | 22 | componentDidMount() { 23 | var self = this; 24 | 25 | const makeRequest = () => 26 | axios.get("/api/client" + this.props.location.search) 27 | .then(({ data }) => this.setState({ client: data })) 28 | .catch((error) => this.setState({ error: error })) 29 | 30 | this.serverRequest = makeRequest(); 31 | 32 | this.poll = setInterval(() => { 33 | this.serverRequest = makeRequest(); 34 | }, 3000); 35 | } 36 | 37 | componentWillUnmount() { 38 | this.serverRequest.abort(); 39 | clearInterval(this.poll); 40 | } 41 | 42 | render() { 43 | const { client, error } = this.state; 44 | 45 | if (error) { 46 | return ( 47 |
48 |
49 |

Oops!

50 |

Something went wrong and the client you've selected could not be loaded.

51 |
52 |
53 | ) 54 | } 55 | 56 | if (client === null) { 57 | return null 58 | } 59 | 60 | return ( 61 |
62 |
63 |
64 |
65 |
66 |

Client Properties

67 |
68 |
69 |
    70 |
  • ID: {client.id}
  • 71 |
  • Name: {client.name}
  • 72 |
  • Datacenter: {client.datacenter}
  • 73 |
  • Address: {client.httpAddr}
  • 74 |
  • TLS: {client.tlsEnabled.toString()}
  • 75 |
  • Drain: {client.drain.toString()}
  • 76 |
  • Status Description: {client.statusDescription}
  • 77 |
  • Status: {client.status}
  • 78 |
  • 79 | Meta: 80 | {client.meta ? 81 |
      82 | {Object.entries(client.meta).map(([key, value]) => { 83 | return ( 84 |
    • {key} = {value}
    • 85 | ) 86 | })} 87 |
    : 88 |

    89 | } 90 |
  • 91 |
92 |
93 |
94 |
95 | 96 |
97 |
98 |
99 |

Client Resources

100 |
101 |
102 |
    103 |
  • CPU (MHz): {client.resources.cpu}
  • 104 |
  • Memory (MB): {client.resources.memoryMB}
  • 105 |
  • Disk (MB): {client.resources.diskMB}
  • 106 |
  • 107 | {client.resources.networks.map((network) => { 108 | return ( 109 |
    Network (Mpbs): {network.mBits}
    110 | ) 111 | })} 112 |
  • 113 |
  • 114 | {client.resources.networks.map((network) => { 115 | return ( 116 |
    IP Address: {network.ip}
    117 | ) 118 | })} 119 |
  • 120 |
121 |
122 |
123 |
124 | 125 |
126 |
127 |
128 |

Client Attributes

129 |
130 |
131 |
    132 |
  • Operating System: {client.attributes['os.name']} {client.attributes['os.version']}
  • 133 |
  • Docker Version: {client.attributes['driver.docker.version']}
  • 134 |
  • Nomad Version: {client.attributes['nomad.version']}
  • 135 |
  • Consul Version: {client.attributes['consul.version']}
  • 136 |
137 |
138 |
139 |
140 |
141 | 142 |
143 | {client.stats.cpu.map((cpu) => { 144 | return ( 145 |
146 |
147 |
148 |

{cpu.cpu === "" ? CPU0 : {cpu.cpu.toUpperCase()}} Utilisation

149 |
150 |
151 | 176 |
177 |
178 |
179 | ) 180 | })} 181 | 182 |
183 |
184 |
185 |

Memory Utilisation

186 |
187 |
188 | 213 |
214 |
215 |
216 |
217 | 218 |
219 |
220 |
221 |
222 |

Allocations

223 |
224 |
225 |
226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | {client.allocations.sort((a, b) => a.createDateTime < b.createDateTime).map((allocation) => { 240 | return ( 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | ) 251 | })} 252 | 253 |
IDNameTask GroupDesired StatusClientClient StatusCreate Time
{allocation.id}{allocation.name}{allocation.taskGroup}{allocation.desiredStatus}{allocation.nodeID}{allocation.clientStatus}{moment(allocation.createDateTime).format('DD/MM/YYYY hh:mm:ss A')}
254 |
255 |
256 |
257 |
258 |
259 |
260 | ); 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /Views/Components/Clients.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import axios from 'axios'; 3 | import Pager from 'react-pager'; 4 | 5 | global.jQuery = require('jquery'); 6 | var bootstrap = require('bootstrap'); 7 | 8 | export default class Clients extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | clients: null, 14 | error: null, 15 | current: 0 16 | } 17 | 18 | this.handlePageChanged = this.handlePageChanged.bind(this); 19 | } 20 | 21 | componentDidMount() { 22 | var self = this; 23 | 24 | const makeRequest = () => { 25 | this.props.location.search ? 26 | axios.get("/api/clients" + this.props.location.search) 27 | .then(({ data }) => this.setState({ clients: data })) 28 | .catch((error) => this.setState({ error: error })) : 29 | axios.get("/api/clients") 30 | .then(({ data }) => this.setState({ clients: data })) 31 | .catch((error) => this.setState({ error: error })) 32 | } 33 | 34 | this.serverRequest = makeRequest(); 35 | 36 | this.poll = setInterval(() => { 37 | this.serverRequest = makeRequest(); 38 | }, 3000); 39 | } 40 | 41 | componentWillUnmount() { 42 | this.serverRequest.abort(); 43 | clearInterval(this.poll); 44 | } 45 | 46 | handlePageChanged(newPage) { 47 | this.setState({ current: newPage }); 48 | } 49 | 50 | render() { 51 | const { clients, error, current } = this.state; 52 | const itemsPerPage = 15; 53 | 54 | if (error) { 55 | return ( 56 |
57 |
58 |

Oops!

59 |

Something went wrong and the list of clients could not be loaded.

60 |
61 |
62 | ) 63 | } 64 | 65 | if (clients === null) { 66 | return null 67 | } 68 | 69 | var visibleItems = clients.slice(itemsPerPage * current, (itemsPerPage * current) + itemsPerPage); 70 | 71 | return ( 72 |
73 |
74 |
75 |
76 |
77 |

Clients

78 |
79 |
80 |
81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | {visibleItems.map((client) => { 93 | return ( 94 | 95 | {client.status === "ready" ? 96 | : 97 | 98 | } 99 | 100 | 101 | 102 | 103 | 104 | ) 105 | })} 106 | 107 |
IDNameDatacenterDrainStatus
{client.id}{client.id}{client.name}{client.datacenter}{client.drain.toString()}{client.status}
108 |
109 | 110 | {clients.length > 15 ? 111 |
112 | 118 |
: 119 |
120 | } 121 |
122 |
123 |
124 |
125 |
126 | ); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /Views/Components/Dashboard.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import axios from 'axios'; 3 | import { 4 | defaults, 5 | Doughnut 6 | } from 'react-chartjs-2'; 7 | import moment from 'moment'; 8 | 9 | global.jQuery = require('jquery'); 10 | var bootstrap = require('bootstrap'); 11 | 12 | export default class Dashboard extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | 16 | this.state = { 17 | dashboard: null, 18 | error: null 19 | } 20 | } 21 | 22 | componentDidMount() { 23 | var self = this; 24 | 25 | const makeRequest = () => 26 | axios.get("/api/dashboard") 27 | .then(({ data }) => this.setState({ dashboard: data })) 28 | .catch((error) => this.setState({ error: error })) 29 | 30 | this.serverRequest = makeRequest(); 31 | 32 | this.poll = setInterval(() => { 33 | this.serverRequest = makeRequest(); 34 | }, 3000); 35 | } 36 | 37 | componentWillUnmount() { 38 | this.serverRequest.abort(); 39 | clearInterval(this.poll); 40 | } 41 | 42 | render() { 43 | const { dashboard, error } = this.state; 44 | 45 | if (error) { 46 | return ( 47 |
48 |
49 |

Oops!

50 |

Something went wrong and the dashboard could not be loaded.

51 |
52 |
53 | ) 54 | } 55 | 56 | if (dashboard === null) { 57 | return null 58 | } 59 | 60 | return ( 61 |
62 |
63 |
64 |
65 |
66 |

Job States

67 |
68 |
69 | 94 |
95 |
96 |
97 | 98 |
99 |
100 |
101 |

Allocation States

102 |
103 |
104 | 129 |
130 |
131 |
132 | 133 |
134 |
135 |
136 |

Client States

137 |
138 |
139 | 164 |
165 |
166 |
167 | 168 |
169 |
170 |
171 |

Server States

172 |
173 |
174 | 199 |
200 |
201 |
202 |
203 | 204 |
205 |
206 |
207 |
208 |

Allocation Events

209 |
210 |
211 |
212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | {dashboard.events.map((event) => { 223 | return ( 224 | 225 | 226 | 227 | 242 | 243 | 244 | ) 245 | })} 246 | 247 |
NameTypeMessageTime
{event.allocationName}{event.type} 228 | {event.restartReason} 229 | {event.setupError} 230 | {event.driverError} 231 | {event.message} 232 | {event.killError} 233 | {event.killReason} 234 | {event.downloadError} 235 | {event.validationError} 236 | {event.failedSibling} 237 | {event.vaultError} 238 | {event.taskSignalReason} 239 | {event.taskSignal} 240 | {event.driverMessage} 241 | {moment(event.dateTime).format('DD/MM/YYYY hh:mm:ss A')}
248 |
249 |
250 |
251 |
252 |
253 |
254 | ); 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /Views/Components/Evaluation.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import axios from 'axios'; 3 | import moment from 'moment'; 4 | 5 | global.jQuery = require('jquery'); 6 | var bootstrap = require('bootstrap'); 7 | 8 | export default class Evaluation extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | evaluation: null, 14 | error: null 15 | } 16 | } 17 | 18 | componentDidMount() { 19 | var self = this; 20 | 21 | const makeRequest = () => 22 | axios.get("/api/evaluation" + this.props.location.search) 23 | .then(({ data }) => this.setState({ evaluation: data })) 24 | .catch((error) => this.setState({ error: error })) 25 | 26 | this.serverRequest = makeRequest(); 27 | 28 | this.poll = setInterval(() => { 29 | this.serverRequest = makeRequest(); 30 | }, 3000); 31 | } 32 | 33 | componentWillUnmount() { 34 | this.serverRequest.abort(); 35 | clearInterval(this.poll); 36 | } 37 | 38 | render() { 39 | const { evaluation, error } = this.state; 40 | 41 | if (error) { 42 | return ( 43 |
44 |
45 |

Oops!

46 |

Something went wrong and the evaluation you've selected could not be loaded.

47 |
48 |
49 | ) 50 | } 51 | 52 | if (evaluation === null) { 53 | return null 54 | } 55 | 56 | return ( 57 |
58 |
59 |
60 |
61 |
62 |

Evaluation Properties

63 |
64 |
65 |
    66 |
  • ID: {evaluation.id}
  • 67 |
  • Job: {evaluation.jobID}
  • 68 |
  • Type: {evaluation.type}
  • 69 |
  • Priority: {evaluation.priority}
  • 70 |
  • Parent: {evaluation.previousEval}
  • 71 |
  • Triggered By: {evaluation.triggeredBy}
  • 72 |
  • Status Description: {evaluation.statusDescription}
  • 73 |
  • Status: {evaluation.status}
  • 74 |
75 |
76 |
77 |
78 |
79 | 80 |
81 |
82 |
83 |
84 |

Allocations

85 |
86 |
87 |
88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | {evaluation.allocations.sort((a, b) => a.createDateTime < b.createDateTime).map((allocation) => { 102 | return ( 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | ) 113 | })} 114 | 115 |
IDNameTask GroupDesired StatusClientClient StatusCreate Time
{allocation.id}{allocation.name}{allocation.taskGroup}{allocation.desiredStatus}{allocation.nodeID}{allocation.clientStatus}{moment(allocation.createDateTime).format('DD/MM/YYYY hh:mm:ss A')}
116 |
117 |
118 |
119 |
120 |
121 |
122 | ); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Views/Components/Evaluations.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import axios from 'axios'; 3 | import Pager from 'react-pager'; 4 | 5 | global.jQuery = require('jquery'); 6 | var bootstrap = require('bootstrap'); 7 | 8 | export default class Evaluations extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | evaluations: null, 14 | error: null, 15 | current: 0 16 | } 17 | 18 | this.handlePageChanged = this.handlePageChanged.bind(this); 19 | } 20 | 21 | componentDidMount() { 22 | var self = this; 23 | 24 | const makeRequest = () => { 25 | this.props.location.search ? 26 | axios.get("/api/evaluations" + this.props.location.search) 27 | .then(({ data }) => this.setState({ evaluations: data })) 28 | .catch((error) => this.setState({ error: error })) : 29 | axios.get("/api/evaluations") 30 | .then(({ data }) => this.setState({ evaluations: data })) 31 | .catch((error) => this.setState({ error: error })) 32 | } 33 | 34 | this.serverRequest = makeRequest(); 35 | 36 | this.poll = setInterval(() => { 37 | this.serverRequest = makeRequest(); 38 | }, 3000); 39 | } 40 | 41 | componentWillUnmount() { 42 | this.serverRequest.abort(); 43 | clearInterval(this.poll); 44 | } 45 | 46 | handlePageChanged(newPage) { 47 | this.setState({ current: newPage }); 48 | } 49 | 50 | render() { 51 | const { evaluations, error, current } = this.state; 52 | const itemsPerPage = 15; 53 | 54 | if (error) { 55 | return ( 56 |
57 |
58 |

Oops!

59 |

Something went wrong and the list of evaluations could not be loaded.

60 |
61 |
62 | ) 63 | } 64 | 65 | if (evaluations === null) { 66 | return null 67 | } 68 | 69 | var visibleItems = evaluations.slice(itemsPerPage * current, (itemsPerPage * current) + itemsPerPage); 70 | 71 | return ( 72 |
73 |
74 |
75 |
76 |
77 |

Evaluations

78 |
79 |
80 |
81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | {visibleItems.map((evaluation) => { 94 | return ( 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | ) 104 | })} 105 | 106 |
Evaluation IDJob IDTypePriorityTriggered ByStatus
{evaluation.id}{evaluation.jobID}{evaluation.type}{evaluation.priority}{evaluation.triggeredBy}{evaluation.status}
107 |
108 | 109 | {evaluations.length > 15 ? 110 |
111 | 117 |
: 118 |
119 | } 120 |
121 |
122 |
123 |
124 |
125 | ); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Views/Components/Job.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import axios from 'axios'; 3 | import moment from 'moment'; 4 | 5 | global.jQuery = require('jquery'); 6 | var bootstrap = require('bootstrap'); 7 | 8 | export default class Job extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | job: null, 14 | error: null 15 | } 16 | } 17 | 18 | componentDidMount() { 19 | var self = this; 20 | 21 | const makeRequest = () => 22 | axios.get("/api/job" + this.props.location.search) 23 | .then(({ data }) => this.setState({ job: data })) 24 | .catch((error) => this.setState({ error: error })) 25 | 26 | this.serverRequest = makeRequest(); 27 | 28 | this.poll = setInterval(() => { 29 | this.serverRequest = makeRequest(); 30 | }, 3000); 31 | } 32 | 33 | componentWillUnmount() { 34 | this.serverRequest.abort(); 35 | clearInterval(this.poll); 36 | } 37 | 38 | render() { 39 | const { job, error } = this.state; 40 | 41 | if (error) { 42 | return ( 43 |
44 |
45 |

Oops!

46 |

Something went wrong and the job you've selected could not be loaded.

47 |
48 |
49 | ) 50 | } 51 | 52 | if (job === null) { 53 | return null 54 | } 55 | 56 | return ( 57 |
58 |
59 |
60 |
61 |
62 |

Job Properties

63 |
64 |
65 |
    66 |
  • ID: {job.id}
  • 67 |
  • Name: {job.name}
  • 68 |
  • Region: {job.region}
  • 69 |
  • 70 | Datacenters: 71 | {job.datacenters.map((datacenter) => { 72 | return ( 73 |
      74 |
    • {datacenter}
    • 75 |
    76 | ) 77 | })} 78 |
  • 79 |
  • Type: {job.type}
  • 80 |
  • Priority: {job.priority}
  • 81 |
  • Status: {job.status}
  • 82 |
83 |
84 |
85 |
86 | 87 |
88 |
89 |
90 |

Job Constraints

91 |
92 |
93 | {job.constraints ? 94 |
    95 | {job.constraints.map((constraint) => { 96 | return ( 97 |
  • {constraint.lTarget} {constraint.operand} {constraint.rTarget}
  • 98 | ) 99 | })} 100 |
: 101 |

102 | } 103 |
104 |
105 |
106 | 107 |
108 |
109 |
110 |

Job Meta

111 |
112 |
113 | {job.meta ? 114 |
    115 | {Object.entries(job.meta).sort().map(([key, value]) => { 116 | return ( 117 |
  • {key} = {value}
  • 118 | ) 119 | })} 120 |
: 121 |

122 | } 123 |
124 |
125 |
126 |
127 | 128 |
129 |
130 |
131 |
132 |

Task Groups

133 |
134 |
135 |
136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | {job.taskGroups.map((taskGroup) => { 151 | return ( 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 171 | 183 | 184 | ) 185 | })} 186 | 187 |
NameCountTasksRestart PolicyRestart AttemptsRestart DelayConstraintsMeta
{taskGroup.name}{taskGroup.count}{taskGroup.tasks.length}{taskGroup.restartPolicy.mode}{taskGroup.restartPolicy.attempts}{taskGroup.restartPolicy.delay} 160 | {taskGroup.constraints ? 161 |
    162 | {taskGroup.constraints.map((constraint) => { 163 | return ( 164 |
  • {constraint.lTarget} {constraint.operand} {constraint.rTarget}
  • 165 | ) 166 | })} 167 |
: 168 |

169 | } 170 |
172 | {taskGroup.meta ? 173 |
    174 | {Object.entries(taskGroup.meta).sort().map(([key, value]) => { 175 | return ( 176 |
  • {key} = {value}
  • 177 | ) 178 | })} 179 |
: 180 |

181 | } 182 |
188 |
189 |
190 |
191 |
192 |
193 | 194 |
195 |
196 |
197 |
198 |

Tasks

199 |
200 |
201 |
202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | {job.taskGroups.map((taskGroup) => taskGroup.tasks.map((task) => ( 217 | 218 | 219 | 220 | 221 | 222 | 223 | 230 | 242 | 254 | 255 | ))).reduce((flattened, rows) => [...flattened, ...rows], [])} 256 | 257 |
NameTask GroupDriverCPU (MHz)Memory (MB)Network (Mbps)ConstraintsMeta
{task.name}{taskGroup.name}{task.driver}{task.resources.cpu}{task.resources.memoryMB} 224 | {task.resources.networks.map((network) => { 225 | return ( 226 | {network.mBits} 227 | ) 228 | })} 229 | 231 | {task.constraints ? 232 |
    233 | {task.constraints.map((constraint) => { 234 | return ( 235 |
  • {constraint.lTarget} {constraint.operand} {constraint.rTarget}
  • 236 | ) 237 | })} 238 |
: 239 |

240 | } 241 |
243 | {task.meta ? 244 |
    245 | {Object.entries(task.meta).sort().map(([key, value]) => { 246 | return ( 247 |
  • {key} = {value}
  • 248 | ) 249 | })} 250 |
: 251 |

252 | } 253 |
258 |
259 |
260 |
261 |
262 |
263 | 264 |
265 |
266 |
267 |
268 |

Docker Properties

269 |
270 |
271 |
272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | {job.taskGroups.map((taskGroup) => taskGroup.tasks.map((task) => ( 285 | 286 | 287 | 288 | 289 | 290 | 302 | 314 | 315 | ))).reduce((flattened, rows) => [...flattened, ...rows], [])} 316 | 317 |
TaskImageNetworkCommandArgumentsEnvironment Variables
{task.name}{task.config.image}{task.config.network_Mode}{task.config.command} 291 | {task.config.args ? 292 |
    293 | {task.config.args.map((arg) => { 294 | return ( 295 |
  • {arg}
  • 296 | ) 297 | })} 298 |
: 299 |

300 | } 301 |
303 | {task.env ? 304 |
    305 | {Object.entries(task.env).sort().map(([key, value]) => { 306 | return ( 307 |
  • {key} = {value}
  • 308 | ) 309 | })} 310 |
: 311 |

312 | } 313 |
318 |
319 |
320 |
321 |
322 |
323 | 324 |
325 |
326 |
327 |
328 |

Evaluations

329 |
330 |
331 |
332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | {job.evaluations.map((evaluation) => { 346 | return ( 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | ) 357 | })} 358 | 359 |
IDParent IDJobTypePriorityTriggered ByStatus
{evaluation.id}{evaluation.previousEval}{evaluation.jobID}{evaluation.type}{evaluation.priority}{evaluation.triggeredBy}{evaluation.status}
360 |
361 |
362 |
363 |
364 |
365 | 366 |
367 |
368 |
369 |
370 |

Allocations

371 |
372 |
373 |
374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | {job.allocations.sort((a, b) => a.createDateTime < b.createDateTime).map((allocation) => { 388 | return ( 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | ) 399 | })} 400 | 401 |
IDNameTask GroupDesired StatusClientClient StatusCreate Time
{allocation.id}{allocation.name}{allocation.taskGroup}{allocation.desiredStatus}{allocation.nodeID}{allocation.clientStatus}{moment(allocation.createDateTime).format('DD/MM/YYYY hh:mm:ss A')}
402 |
403 |
404 |
405 |
406 |
407 |
408 | ); 409 | } 410 | } 411 | -------------------------------------------------------------------------------- /Views/Components/Jobs.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import axios from 'axios'; 3 | import Pager from 'react-pager'; 4 | 5 | global.jQuery = require('jquery'); 6 | var bootstrap = require('bootstrap'); 7 | 8 | export default class Jobs extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | jobs: null, 14 | error: null, 15 | current: 0 16 | } 17 | 18 | this.handlePageChanged = this.handlePageChanged.bind(this); 19 | } 20 | 21 | componentDidMount() { 22 | var self = this; 23 | 24 | const makeRequest = () => { 25 | this.props.location.search ? 26 | axios.get("/api/jobs" + this.props.location.search) 27 | .then(({ data }) => this.setState({ jobs: data })) 28 | .catch((error) => this.setState({ error: error })) : 29 | axios.get("/api/jobs") 30 | .then(({ data }) => this.setState({ jobs: data })) 31 | .catch((error) => this.setState({ error: error })) 32 | } 33 | 34 | this.serverRequest = makeRequest(); 35 | 36 | this.poll = setInterval(() => { 37 | this.serverRequest = makeRequest(); 38 | }, 3000); 39 | } 40 | 41 | componentWillUnmount() { 42 | this.serverRequest.abort(); 43 | clearInterval(this.poll); 44 | } 45 | 46 | handlePageChanged(newPage) { 47 | this.setState({ current: newPage }); 48 | } 49 | 50 | render() { 51 | const { jobs, error, current } = this.state; 52 | const itemsPerPage = 15; 53 | 54 | if (error) { 55 | return ( 56 |
57 |
58 |

Oops!

59 |

Something went wrong and the list of jobs could not be loaded.

60 |
61 |
62 | ) 63 | } 64 | 65 | if (jobs === null) { 66 | return null 67 | } 68 | 69 | var visibleItems = jobs.slice(itemsPerPage * current, (itemsPerPage * current) + itemsPerPage); 70 | 71 | return ( 72 |
73 |
74 |
75 |
76 |
77 |

Jobs

78 |
79 |
80 |
81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | {visibleItems.map((job) => { 99 | return ( 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | ) 114 | })} 115 | 116 |
IDTypePriorityTask GroupsQueuedStartingRunningFailedLostCompleteStatus
{job.id}{job.type}{job.priority}{job.numOfTaskGroups}{job.queuedTaskGroups}{job.startingTaskGroups}{job.runningTaskGroups}{job.failedTaskGroups}{job.lostTaskGroups}{job.completeTaskGroups}{job.status}
117 |
118 | 119 | {jobs.length > 15 ? 120 |
121 | 127 |
: 128 |
129 | } 130 |
131 |
132 |
133 |
134 |
135 | ); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /Views/Components/Server.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import axios from 'axios'; 3 | import moment from 'moment'; 4 | 5 | global.jQuery = require('jquery'); 6 | var bootstrap = require('bootstrap'); 7 | 8 | export default class Server extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | server: null, 14 | error: null 15 | } 16 | } 17 | 18 | componentDidMount() { 19 | var self = this; 20 | 21 | const makeRequest = () => 22 | axios.get("/api/server" + this.props.location.search) 23 | .then(({ data }) => this.setState({ server: data })) 24 | .catch((error) => this.setState({ error: error })) 25 | 26 | this.serverRequest = makeRequest(); 27 | 28 | this.poll = setInterval(() => { 29 | this.serverRequest = makeRequest(); 30 | }, 3000); 31 | } 32 | 33 | componentWillUnmount() { 34 | this.serverRequest.abort(); 35 | clearInterval(this.poll); 36 | } 37 | 38 | render() { 39 | const { server, error } = this.state; 40 | 41 | if (error) { 42 | return ( 43 |
44 |
45 |

Oops!

46 |

Something went wrong and the server you've selected could not be loaded.

47 |
48 |
49 | ) 50 | } 51 | 52 | if (server === null) { 53 | return null 54 | } 55 | 56 | return ( 57 |
58 |
59 |
60 |
61 |
62 |

Server Properties

63 |
64 |
65 |
    66 |
  • Name: {server.member.name}
  • 67 |
  • Address: {server.member.addr}
  • 68 |
  • Port: {server.member.port}
  • 69 |
  • 70 | {server.member.operator.servers.map((s) => { 71 | return ( 72 |
    73 | {server.member.name === s.node ? Voter: {s.voter.toString()} : } 74 |
    75 | ) 76 | })} 77 |
  • 78 |
  • 79 | {server.member.operator.servers.map((s) => { 80 | return ( 81 |
    82 | {server.member.name === s.node ? Leader: {s.leader.toString()} : } 83 |
    84 | ) 85 | })} 86 |
  • 87 |
  • Status: {server.member.status}
  • 88 |
89 |
90 |
91 |
92 | 93 |
94 |
95 |
96 |

Server Configuration

97 |
98 |
99 |
    100 |
  • Region: {server.config.region}
  • 101 |
  • Datacenter: {server.config.datacenter}
  • 102 |
  • Data Directory: {server.config.dataDir}
  • 103 |
  • 104 | Files: 105 | {server.config.files.map((file) => { 106 | return ( 107 |
      108 |
    • {file}
    • 109 |
    110 | ) 111 | })} 112 |
  • 113 |
  • Log Level: {server.config.logLevel}
  • 114 |
  • Bind Address: {server.config.bindAddr}
  • 115 |
  • 116 | Advertise Addresses: 117 |
      118 |
    • {server.config.advertiseAddrs.http}
    • 119 |
    • {server.config.advertiseAddrs.rpc}
    • 120 |
    • {server.config.advertiseAddrs.serf}
    • 121 |
    122 |
  • 123 |
  • Nomad Version: {server.config.version}
  • 124 |
125 |
126 |
127 |
128 |
129 |
130 | ); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Views/Components/Servers.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import axios from 'axios'; 3 | import Pager from 'react-pager'; 4 | 5 | global.jQuery = require('jquery'); 6 | var bootstrap = require('bootstrap'); 7 | 8 | export default class Servers extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | servers: null, 14 | error: null, 15 | current: 0 16 | } 17 | 18 | this.handlePageChanged = this.handlePageChanged.bind(this); 19 | } 20 | 21 | componentDidMount() { 22 | var self = this; 23 | 24 | const makeRequest = () => { 25 | this.props.location.search ? 26 | axios.get("/api/servers" + this.props.location.search) 27 | .then(({ data }) => this.setState({ servers: data })) 28 | .catch((error) => this.setState({ error: error })) : 29 | axios.get("/api/servers") 30 | .then(({ data }) => this.setState({ servers: data })) 31 | .catch((error) => this.setState({ error: error })) 32 | } 33 | 34 | this.serverRequest = makeRequest(); 35 | 36 | this.poll = setInterval(() => { 37 | this.serverRequest = makeRequest(); 38 | }, 3000); 39 | } 40 | 41 | componentWillUnmount() { 42 | this.serverRequest.abort(); 43 | clearInterval(this.poll); 44 | } 45 | 46 | handlePageChanged(newPage) { 47 | this.setState({ current: newPage }); 48 | } 49 | 50 | render() { 51 | const { servers, error, current } = this.state; 52 | const itemsPerPage = 15; 53 | 54 | if (error) { 55 | return ( 56 |
57 |
58 |

Oops!

59 |

Something went wrong and the list of servers could not be loaded.

60 |
61 |
62 | ) 63 | } 64 | 65 | if (servers === null) { 66 | return null 67 | } 68 | 69 | var visibleItems = servers.slice(itemsPerPage * current, (itemsPerPage * current) + itemsPerPage); 70 | 71 | return ( 72 |
73 |
74 |
75 |
76 |
77 |

Servers

78 |
79 |
80 |
81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | {visibleItems.map((server) => { 94 | return ( 95 | 96 | {server.status === "alive" ? 97 | : 98 | 99 | } 100 | 101 | 102 | 103 | 104 | 105 | 106 | ) 107 | })} 108 | 109 |
NameAddressPortVoterLeaderStatus
{server.name}{server.name}{server.addr}{server.port}{server.voter.toString()}{server.leader.toString()}{server.status}
110 |
111 | 112 | {servers.length > 15 ? 113 |
114 | 120 |
: 121 |
122 | } 123 |
124 |
125 |
126 |
127 |
128 | ); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Views/Error.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | Nomad 9 | 10 | 11 | 12 | 13 | 14 | 15 | @Html.Partial("Header") 16 | 17 |
18 |
19 |

¯\_(ツ)_/¯

20 | 21 |

@ViewData["StatusCode"]

22 | 23 |

@ViewData["Message"]

24 | 25 |
@ViewData["StackTrace"]
26 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /Views/Index.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | Nomad 9 | 10 | 11 | 12 | 13 | 14 | 15 | @Html.Partial("Header") 16 | 17 |
18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Views/Shared/Header.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | var searchPaths = new List(); 3 | searchPaths.Add("/jobs"); 4 | searchPaths.Add("/evaluations"); 5 | searchPaths.Add("/allocations"); 6 | searchPaths.Add("/clients"); 7 | searchPaths.Add("/servers"); 8 | } 9 | 10 | 46 | -------------------------------------------------------------------------------- /Views/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './Components/App.jsx'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /bundleconfig.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "outputFileName": "wwwroot/css/site.min.css", 4 | "inputFiles": [ 5 | "wwwroot/css/site.css" 6 | ] 7 | }, 8 | { 9 | "outputFileName": "wwwroot/js/site.min.js", 10 | "inputFiles": [ 11 | "wwwroot/js/site.js" 12 | ], 13 | "minify": { 14 | "enabled": true, 15 | "renameLocals": true 16 | }, 17 | "sourceMap": false 18 | } 19 | ] 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nomad", 3 | "version": "1.0.0", 4 | "main": "./Views/index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "axios": "0.16.2", 8 | "babel-core": "6.25.0", 9 | "babel-loader": "7.1.1", 10 | "babel-preset-es2015": "6.24.1", 11 | "babel-preset-react": "6.24.1", 12 | "bootstrap": "3.3.7", 13 | "chart.js": "2.6.0", 14 | "jquery": "3.2.1", 15 | "moment": "2.18.1", 16 | "react": "15.6.1", 17 | "react-chartjs-2": "2.1.0", 18 | "react-dom": "15.6.1", 19 | "react-pager": "1.3.2", 20 | "react-router-dom": "4.1.2", 21 | "webpack": "3.3.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: './Views/index.js', 3 | output: { 4 | filename: './wwwroot/js/site.js' 5 | }, 6 | module: { 7 | loaders: [ 8 | { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ }, 9 | { test: /\.jsx$/, loader: 'babel-loader', exclude: /node_modules/ } 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | /* 2 | https://www.w3schools.com/colors/colors_picker.asp 3 | black = #1e1e1e 4 | dark grey = #262626 5 | grey = #333 6 | light grey = #666 7 | white = #fff 8 | green = #1fb58f 9 | orange = #eab126 10 | red = #f24c4e 11 | */ 12 | 13 | * { 14 | border-radius: 0 !important; 15 | } 16 | 17 | body { 18 | color: #fff; 19 | background-color: #1e1e1e; 20 | } 21 | 22 | a, 23 | a:hover, 24 | a:focus { 25 | color: #1fb58f; 26 | } 27 | 28 | a:hover { 29 | text-decoration: none; 30 | } 31 | 32 | button { 33 | color: #1e1e1e; 34 | background-color: #1fb58f; 35 | } 36 | 37 | button:active, 38 | button:focus { 39 | outline: none !important; 40 | } 41 | 42 | .row { 43 | margin-top: 10px; 44 | } 45 | 46 | .row-condensed { 47 | margin-top: 0; 48 | padding-top: 0; 49 | } 50 | 51 | .navbar-default { 52 | background-color: #1fb58f; 53 | border: none; 54 | } 55 | 56 | .navbar-default .navbar-nav > li > a, 57 | .navbar-default .navbar-nav .dropdown .dropdown-menu > li > a { 58 | color: #1e1e1e; 59 | } 60 | 61 | .navbar-default .navbar-nav > li > a:hover, 62 | .navbar-default .navbar-nav > li > a:focus, 63 | .navbar-default .navbar-nav .active > a, 64 | .navbar-default .navbar-nav .active > a:hover, 65 | .navbar-default .navbar-nav .active > a:focus { 66 | color: #fff; 67 | background-color: #1e1e1e; 68 | } 69 | 70 | .dropdown-toggle:active, .open .dropdown-toggle { 71 | color: #fff !important; 72 | background-color: #1e1e1e !important; 73 | } 74 | 75 | .navbar-default .navbar-nav .dropdown .dropdown-menu { 76 | background-color: #1fb58f; 77 | border: solid 1px #1fb58f; 78 | } 79 | 80 | .navbar-default .navbar-nav .dropdown .dropdown-menu > li > a:hover, 81 | .navbar-default .navbar-nav .dropdown .dropdown-menu > li > a:focus { 82 | color: #1e1e1e; 83 | background-color: #1fb58f; 84 | } 85 | 86 | .navbar-default .navbar-toggle { 87 | margin-top: 9px; 88 | background-color: #1e1e1e; 89 | border: none; 90 | } 91 | 92 | .navbar-default .navbar-toggle:hover, 93 | .navbar-default .navbar-toggle:focus { 94 | background-color: #1e1e1e; 95 | } 96 | 97 | .navbar-default .navbar-toggle .icon-bar { 98 | background-color: #fff; 99 | } 100 | 101 | .navbar-collapse, 102 | .navbar-form { 103 | border: none; 104 | box-shadow: none; 105 | } 106 | 107 | .search .form-group > input { 108 | color: #1e1e1e; 109 | background-color: #1fb58f; 110 | border: solid 1px #1e1e1e; 111 | box-shadow: none; 112 | } 113 | 114 | .search > button, 115 | .search > button:hover, 116 | .search > button:focus { 117 | color: #fff; 118 | background-color: #1e1e1e; 119 | border: solid 1px #1e1e1e; 120 | } 121 | 122 | .search .form-group > input::-webkit-input-placeholder { color: #1e1e1e; } 123 | .search .form-group > input:-webkit-input-placeholder { color: #1e1e1e; } 124 | .search .form-group > input:-moz-placeholder { color: #1e1e1e; } 125 | .search .form-group > input::-moz-placeholder { color: #1e1e1e; } 126 | .search .form-group > input::-ms-input-placeholder { color: #1e1e1e; } 127 | .search .form-group > input:-ms-input-placeholder { color: #1e1e1e; } 128 | 129 | .panel { 130 | box-shadow: 0 1px 30px rgba(0, 0, 0, 0.4) 131 | } 132 | 133 | .panel, 134 | .panel .panel-heading { 135 | color: #fff; 136 | background-color: #1e1e1e; 137 | border: none; 138 | } 139 | 140 | .table-responsive { 141 | border: none; 142 | } 143 | 144 | .table > thead > tr > th, 145 | .table > tbody > tr > td { 146 | border: none; 147 | } 148 | 149 | .table > tbody > tr:nth-child(2n+1) { 150 | background-color: #262626; 151 | } 152 | 153 | .table > tbody > tr:hover { 154 | background-color: #333; 155 | } 156 | 157 | .pagination > li > a { 158 | outline: none; 159 | } 160 | 161 | .pagination > li > a, 162 | .pagination > li > a:hover, 163 | .pagination > li > a:focus, 164 | .pagination .disabled > a, 165 | .pagination .disabled > a:hover, 166 | .pagination .disabled > a:focus { 167 | color: #1e1e1e; 168 | background-color: #1fb58f; 169 | border-color: #1fb58f; 170 | } 171 | 172 | .pagination .active > a, 173 | .pagination .active > a:hover, 174 | .pagination .active > a:focus { 175 | color: #fff; 176 | background-color: #1e1e1e; 177 | border: solid 1px #1e1e1e; 178 | } 179 | 180 | .pagination > li > a, 181 | .pagination > li > a:hover, 182 | .pagination > li > a:focus { 183 | cursor: pointer; 184 | } 185 | 186 | .list-group-condensed { 187 | margin: 0; 188 | padding: 0; 189 | list-style: none; 190 | } 191 | 192 | .list-group-item { 193 | background-color: #1e1e1e; 194 | border: none; 195 | } 196 | 197 | .list-group-item-condensed { 198 | margin-bottom: 0; 199 | padding-bottom: 0; 200 | } 201 | 202 | .log-content, 203 | .stacktrace { 204 | white-space: pre-wrap; 205 | color: #fff; 206 | background-color: #1e1e1e; 207 | max-height: 800px; 208 | border: none; 209 | } 210 | 211 | .loading { 212 | position: absolute; 213 | top: 8px; 214 | left: 20px; 215 | width: 50px; 216 | } 217 | 218 | #attributeModal > .modal-dialog > .modal-content { 219 | background-color: #1e1e1e; 220 | border: solid 1px #1fb58f; 221 | } 222 | 223 | #attributeModal > .modal-dialog > .modal-content > .modal-body { 224 | margin: 0; 225 | padding: 1px; 226 | } 227 | 228 | #attributeModal > .modal-dialog > .modal-content > .modal-body > ul { 229 | margin: 0; 230 | } 231 | 232 | #attributeModal > .modal-dialog > .modal-content > .modal-body > ul > li { 233 | word-wrap: break-word; 234 | } 235 | 236 | .border { 237 | border: solid 1px red; 238 | } 239 | -------------------------------------------------------------------------------- /wwwroot/css/site.min.css: -------------------------------------------------------------------------------- 1 | *{border-radius:0 !important}body{color:#fff;background-color:#1e1e1e}a,a:hover,a:focus{color:#1fb58f}a:hover{text-decoration:none}button{color:#1e1e1e;background-color:#1fb58f}button:active,button:focus{outline:none !important}.row{margin-top:10px}.row-condensed{margin-top:0;padding-top:0}.navbar-default{background-color:#1fb58f;border:none}.navbar-default .navbar-nav>li>a,.navbar-default .navbar-nav .dropdown .dropdown-menu>li>a{color:#1e1e1e}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav .active>a,.navbar-default .navbar-nav .active>a:hover,.navbar-default .navbar-nav .active>a:focus{color:#fff;background-color:#1e1e1e}.dropdown-toggle:active,.open .dropdown-toggle{color:#fff !important;background-color:#1e1e1e !important}.navbar-default .navbar-nav .dropdown .dropdown-menu{background-color:#1fb58f;border:solid 1px #1fb58f}.navbar-default .navbar-nav .dropdown .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .dropdown .dropdown-menu>li>a:focus{color:#1e1e1e;background-color:#1fb58f}.navbar-default .navbar-toggle{margin-top:9px;background-color:#1e1e1e;border:none}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#1e1e1e}.navbar-default .navbar-toggle .icon-bar{background-color:#fff}.navbar-collapse,.navbar-form{border:none;box-shadow:none}.search .form-group>input{color:#1e1e1e;background-color:#1fb58f;border:solid 1px #1e1e1e;box-shadow:none}.search>button,.search>button:hover,.search>button:focus{color:#fff;background-color:#1e1e1e;border:solid 1px #1e1e1e}.search .form-group>input::-webkit-input-placeholder{color:#1e1e1e}.search .form-group>input:-webkit-input-placeholder{color:#1e1e1e}.search .form-group>input:-moz-placeholder{color:#1e1e1e}.search .form-group>input::-moz-placeholder{color:#1e1e1e}.search .form-group>input::-ms-input-placeholder{color:#1e1e1e}.search .form-group>input:-ms-input-placeholder{color:#1e1e1e}.panel{box-shadow:0 1px 30px rgba(0,0,0,.4)}.panel,.panel .panel-heading{color:#fff;background-color:#1e1e1e;border:none}.table-responsive{border:none}.table>thead>tr>th,.table>tbody>tr>td{border:none}.table>tbody>tr:nth-child(2n+1){background-color:#262626}.table>tbody>tr:hover{background-color:#333}.pagination>li>a{outline:none}.pagination>li>a,.pagination>li>a:hover,.pagination>li>a:focus,.pagination .disabled>a,.pagination .disabled>a:hover,.pagination .disabled>a:focus{color:#1e1e1e;background-color:#1fb58f;border-color:#1fb58f}.pagination .active>a,.pagination .active>a:hover,.pagination .active>a:focus{color:#fff;background-color:#1e1e1e;border:solid 1px #1e1e1e}.pagination>li>a,.pagination>li>a:hover,.pagination>li>a:focus{cursor:pointer}.list-group-condensed{margin:0;padding:0;list-style:none}.list-group-item{background-color:#1e1e1e;border:none}.list-group-item-condensed{margin-bottom:0;padding-bottom:0}.log-content,.stacktrace{white-space:pre-wrap;color:#fff;background-color:#1e1e1e;max-height:800px;border:none}.loading{position:absolute;top:8px;left:20px;width:50px}#attributeModal>.modal-dialog>.modal-content{background-color:#1e1e1e;border:solid 1px #1fb58f}#attributeModal>.modal-dialog>.modal-content>.modal-body{margin:0;padding:1px}#attributeModal>.modal-dialog>.modal-content>.modal-body>ul{margin:0}#attributeModal>.modal-dialog>.modal-content>.modal-body>ul>li{word-wrap:break-word}.border{border:solid 1px #f00} -------------------------------------------------------------------------------- /wwwroot/images/collage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cvandal/nomad-ui/db098f1ae8e8570451e0714347e23a846bb7f74e/wwwroot/images/collage.png -------------------------------------------------------------------------------- /wwwroot/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cvandal/nomad-ui/db098f1ae8e8570451e0714347e23a846bb7f74e/wwwroot/images/loading.gif -------------------------------------------------------------------------------- /wwwroot/images/nomad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cvandal/nomad-ui/db098f1ae8e8570451e0714347e23a846bb7f74e/wwwroot/images/nomad.png --------------------------------------------------------------------------------