├── robots.txt ├── readme-assets └── images │ ├── WIP_dec-2015.png │ ├── pixelated_rescompare_v0.1.png │ ├── pixelated_rescompare_v1.0.png │ └── pixelated_rescompare_v1.0.3.png ├── humans.txt ├── crossdomain.xml ├── js ├── sortable.number.js ├── main.js ├── csv.js ├── sortable.min.js ├── dom.js └── aws.js ├── css ├── main.css ├── sortable.css ├── boilerplate.css └── normalize.css ├── LICENSE ├── 404.html ├── main.css ├── README.md ├── dev.html ├── index.html └── main.js /robots.txt: -------------------------------------------------------------------------------- 1 | # www.robotstxt.org/ 2 | 3 | # Allow crawling of all content 4 | User-agent: * 5 | Disallow: 6 | -------------------------------------------------------------------------------- /readme-assets/images/WIP_dec-2015.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ckelner/AWSResco/HEAD/readme-assets/images/WIP_dec-2015.png -------------------------------------------------------------------------------- /readme-assets/images/pixelated_rescompare_v0.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ckelner/AWSResco/HEAD/readme-assets/images/pixelated_rescompare_v0.1.png -------------------------------------------------------------------------------- /readme-assets/images/pixelated_rescompare_v1.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ckelner/AWSResco/HEAD/readme-assets/images/pixelated_rescompare_v1.0.png -------------------------------------------------------------------------------- /readme-assets/images/pixelated_rescompare_v1.0.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ckelner/AWSResco/HEAD/readme-assets/images/pixelated_rescompare_v1.0.3.png -------------------------------------------------------------------------------- /humans.txt: -------------------------------------------------------------------------------- 1 | # humanstxt.org/ 2 | # The humans responsible & technology colophon 3 | 4 | # TEAM 5 | 6 | Chris Kelner - Twitter: @kelner - GitHub: @ckelner 7 | 8 | # TECHNOLOGY COLOPHON 9 | 10 | CSS3, HTML5 11 | AngularJS, Modernizr, Normalize.css 12 | -------------------------------------------------------------------------------- /crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /js/sortable.number.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | var cleanNumber = function(i) { 3 | return i.replace(/[^\-?0-9.]/g, ''); 4 | }, 5 | 6 | compareNumber = function(a, b) { 7 | a = parseFloat(a); 8 | b = parseFloat(b); 9 | 10 | a = isNaN(a) ? 0 : a; 11 | b = isNaN(b) ? 0 : b; 12 | 13 | return a - b; 14 | }; 15 | 16 | Tablesort.extend('number', function(item) { 17 | return item.match(/^-?[£\x24Û¢´€]?\d+\s*([,\.]\d{0,2})/) || // Prefixed currency 18 | item.match(/^-?\d+\s*([,\.]\d{0,2})?[£\x24Û¢´€]/) || // Suffixed currency 19 | item.match(/^-?(\d)*-?([,\.]){0,1}-?(\d)+([E,e][\-+][\d]+)?%?$/); // Number 20 | }, function(a, b) { 21 | a = cleanNumber(a); 22 | b = cleanNumber(b); 23 | 24 | return compareNumber(b, a); 25 | }); 26 | }()); 27 | -------------------------------------------------------------------------------- /css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 1px !important; 3 | } 4 | .page-header { 5 | margin-top: 1px !important; 6 | margin-bottom: 10px !important; 7 | padding-bottom: 0px !important; 8 | } 9 | .col-padding-top-25 { 10 | margin-top: 25px !important; 11 | } 12 | h1 { 13 | line-height: 0.8 !important; 14 | margin-top: 5px !important; 15 | } 16 | h1 a:hover { 17 | text-decoration: none !important; 18 | } 19 | #awsQueryResults { 20 | display: none; 21 | } 22 | #pleaseWait { 23 | display: none; 24 | font-size: 2em; 25 | } 26 | #tabDataRunNames { 27 | width: 200px; 28 | } 29 | #errorAccessSecretKey { 30 | display: none; 31 | } 32 | #errorCredentials { 33 | display: none; 34 | } 35 | #security { 36 | font-size: .5em; 37 | float: right; 38 | position: relative; 39 | top: 15px; 40 | } 41 | .right .github-fork-ribbon { 42 | background-color: #f80; 43 | } 44 | -------------------------------------------------------------------------------- /css/sortable.css: -------------------------------------------------------------------------------- 1 | th.sort-header::-moz-selection { 2 | background: transparent; 3 | } 4 | th.sort-header::selection { 5 | background: transparent; 6 | } 7 | th.sort-header { 8 | cursor: pointer; 9 | } 10 | th.sort-up { 11 | background-color: #5bc0de !important; 12 | } 13 | th.sort-down { 14 | background-color: #5bc0de !important; 15 | } 16 | th.sort-header::-moz-selection, th.sort-header::selection { 17 | background: transparent; 18 | } 19 | table th.sort-header:after { 20 | content: ''; 21 | float: right; 22 | margin-top: 7px; 23 | border-width: 0 4px 4px; 24 | border-style: solid; 25 | border-color: #404040 transparent; 26 | visibility: hidden; 27 | } 28 | table th.sort-header:hover:after { 29 | visibility: visible; 30 | } 31 | table th.sort-up:after, table th.sort-down:after, table th.sort-down:hover:after { 32 | visibility: visible; 33 | opacity: 0.4; 34 | } 35 | table th.sort-up:after { 36 | border-bottom: none; 37 | border-width: 4px 4px 0; 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Chris Kelner 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 | 23 | -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | window.onload = function() { 2 | init(); 3 | }; 4 | 5 | function init() { 6 | document.getElementById("awsQueryButton").addEventListener("click", function() { 7 | awsQueryButtonAction(); 8 | }); 9 | } 10 | 11 | function validateKeys(val) { 12 | if (val == undefined || val == null || val == "") { 13 | showAccessSecretErrorDiv(); 14 | return null; 15 | } 16 | return val; 17 | } 18 | 19 | function getAccessKeyValue() { 20 | return validateKeys(document.getElementById('awsAccessKey').value); 21 | } 22 | 23 | function getSecretKeyValue() { 24 | return validateKeys(document.getElementById('awsSecretKey').value); 25 | } 26 | 27 | function getTokenValue() { 28 | return document.getElementById('awsToken').value; 29 | } 30 | 31 | function getRegionValue() { 32 | return document.getElementById('regionSelect').value; 33 | } 34 | 35 | function awsQueryButtonAction() { 36 | resetCredChecks(); 37 | showPleaseWaitDiv(); 38 | hideAccessSecretErrorDiv(); 39 | hideCredentialsErrorDiv(); 40 | testAWSCredentials(getAccessKeyValue(), getSecretKeyValue(), getTokenValue(), getRegionValue()); 41 | waitForCredCheck(); 42 | // always return false to avoid page refresh 43 | return false; 44 | } 45 | 46 | // utils 47 | Array.prototype.contains = function(obj) { 48 | var i = this.length; 49 | while (i--) { 50 | if (this[i] === obj) { 51 | return true; 52 | } 53 | } 54 | return false; 55 | } 56 | -------------------------------------------------------------------------------- /404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page Not Found 6 | 7 | 54 | 55 | 56 |

Page Not Found

57 |

Sorry, but the page you were trying to view does not exist.

58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /js/csv.js: -------------------------------------------------------------------------------- 1 | function getTableDataAsCSV() { 2 | var dataTable = document.getElementById("resCoTable"); 3 | var csvData = "data:text/csv;charset=utf-8,"; 4 | for (var i = 0, row; row = dataTable.rows[i]; i++) { 5 | for (var j = 0, col; col = row.cells[j]; j++) { 6 | if (j > 0) { 7 | csvData += ","; 8 | } 9 | var cellContents = col.innerHTML.toString().trim(); 10 | // sanitize contents 11 | if (cellContents.indexOf(',') >= 0 || cellContents.indexOf('\"') >= 0 || cellContents.indexOf('\n') >= 0) { 12 | cellContents = "\"" + cellContents.replace(/\"/g, "\"\"") + "\""; 13 | } 14 | if (cellContents.indexOf('
') >= 0) { 15 | cellContents = cellContents.replace(/
/g, ""); 16 | } 17 | csvData += cellContents; 18 | } 19 | csvData += '\n'; 20 | } 21 | var link = document.createElement('a'); 22 | link.setAttribute('href', encodeURI(csvData)); 23 | link.setAttribute('download', buildCSVFileName()); 24 | link.click(); 25 | } 26 | 27 | function buildCSVFileName() { 28 | var fileName = "awsresco-export_"; 29 | var d = new Date(); 30 | fileName += d.getFullYear() + "-" + (d.getMonth() + 1) + "-" + d.getDate() + 31 | "_" + d.getHours() + ":" + d.getMinutes() + ":" + d.getSeconds(); 32 | return fileName; 33 | } 34 | 35 | // Polyfill from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim 36 | if (!String.prototype.trim) { 37 | String.prototype.trim = function() { 38 | return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ''); 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /js/sortable.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * tablesort v4.0.0 (2015-12-17) 3 | * Copyright (c) 2015 ; Licensed MIT 4 | */!function(){function a(b,c){if(!(this instanceof a))return new a(b,c);if(!b||"TABLE"!==b.tagName)throw new Error("Element must be a table");this.init(b,c||{})}var b=[],c=function(a){var b;return window.CustomEvent&&"function"==typeof window.CustomEvent?b=new CustomEvent(a):(b=document.createEvent("CustomEvent"),b.initCustomEvent(a,!1,!1,void 0)),b},d=function(a){return a.getAttribute("data-sort")||a.textContent||a.innerText||""},e=function(a,b){return a=a.toLowerCase(),b=b.toLowerCase(),a===b?0:b>a?1:-1},f=function(a,b){return function(c,d){var e=a(c.td,d.td);return 0===e?b?d.index-c.index:c.index-d.index:e}};a.extend=function(a,c,d){if("function"!=typeof c||"function"!=typeof d)throw new Error("Pattern and sort must be a function");b.push({name:a,pattern:c,sort:d})},a.prototype={init:function(a,b){var c,d,e,f,g=this;if(g.table=a,g.thead=!1,g.options=b,a.rows&&a.rows.length>0&&(a.tHead&&a.tHead.rows.length>0?(c=a.tHead.rows[a.tHead.rows.length-1],g.thead=!0):c=a.rows[0]),c){var h=function(){g.current&&g.current!==this&&(g.current.classList.remove("sort-up"),g.current.classList.remove("sort-down")),g.current=this,g.sortTable(this)};for(e=0;e0&&m.push(l),n++;if(!m)return}for(n=0;nn;n++)s[n]?(l=s[n],u++):l=r[n-u].tr,i.table.tBodies[0].appendChild(l);i.table.dispatchEvent(c("afterSort"))}},refresh:function(){void 0!==this.current&&this.sortTable(this.current,!0)}},"undefined"!=typeof module&&module.exports?module.exports=a:window.Tablesort=a}(); 5 | -------------------------------------------------------------------------------- /main.css: -------------------------------------------------------------------------------- 1 | /*! HTML5 Boilerplate v5.2.0 | MIT License | https://html5boilerplate.com/ */html{color:#222;font-size:1em;line-height:1.4}::-moz-selection{background:#b3d4fc;text-shadow:none}::selection{background:#b3d4fc;text-shadow:none}hr{display:block;height:1px;border:0;border-top:1px solid #ccc;margin:1em 0;padding:0}audio,canvas,iframe,img,svg,video{vertical-align:middle}fieldset{border:0;margin:0;padding:0}textarea{resize:vertical}.hidden{display:none !important}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.clearfix:before,.clearfix:after{content:" ";display:table}.clearfix:after{clear:both} 2 | /*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0} 3 | th.sort-header::-moz-selection{background:transparent}th.sort-header::selection{background:transparent}th.sort-header{cursor:pointer}th.sort-up{background-color:#5bc0de !important}th.sort-down{background-color:#5bc0de !important}th.sort-header::-moz-selection,th.sort-header::selection{background:transparent}table th.sort-header:after{content:'';float:right;margin-top:7px;border-width:0 4px 4px;border-style:solid;border-color:#404040 transparent;visibility:hidden}table th.sort-header:hover:after{visibility:visible}table th.sort-up:after,table th.sort-down:after,table th.sort-down:hover:after{visibility:visible;opacity:.4}table th.sort-up:after{border-bottom:0;border-width:4px 4px 0} 4 | body{padding-top:1px !important}.page-header{margin-top:1px !important;margin-bottom:10px !important;padding-bottom:0 !important}.col-padding-top-25{margin-top:25px !important}h1{line-height:.8 !important;margin-top:5px !important}h1 a:hover{text-decoration:none !important}#awsQueryResults{display:none}#pleaseWait{display:none;font-size:2em}#tabDataRunNames{width:200px}#errorAccessSecretKey{display:none}#errorCredentials{display:none}#security{font-size:.5em;float:right;position:relative;top:15px}.right .github-fork-ribbon{background-color:#f80} 5 | -------------------------------------------------------------------------------- /css/boilerplate.css: -------------------------------------------------------------------------------- 1 | /*! HTML5 Boilerplate v5.2.0 | MIT License | https://html5boilerplate.com/ */ 2 | 3 | /* 4 | * What follows is the result of much research on cross-browser styling. 5 | * Credit left inline and big thanks to Nicolas Gallagher, Jonathan Neal, 6 | * Kroc Camen, and the H5BP dev community and team. 7 | */ 8 | 9 | /* ========================================================================== 10 | Base styles: opinionated defaults 11 | ========================================================================== */ 12 | 13 | html { 14 | color: #222; 15 | font-size: 1em; 16 | line-height: 1.4; 17 | } 18 | 19 | /* 20 | * Remove text-shadow in selection highlight: 21 | * https://twitter.com/miketaylr/status/12228805301 22 | * 23 | * These selection rule sets have to be separate. 24 | * Customize the background color to match your design. 25 | */ 26 | 27 | ::-moz-selection { 28 | background: #b3d4fc; 29 | text-shadow: none; 30 | } 31 | 32 | ::selection { 33 | background: #b3d4fc; 34 | text-shadow: none; 35 | } 36 | 37 | /* 38 | * A better looking default horizontal rule 39 | */ 40 | 41 | hr { 42 | display: block; 43 | height: 1px; 44 | border: 0; 45 | border-top: 1px solid #ccc; 46 | margin: 1em 0; 47 | padding: 0; 48 | } 49 | 50 | /* 51 | * Remove the gap between audio, canvas, iframes, 52 | * images, videos and the bottom of their containers: 53 | * https://github.com/h5bp/html5-boilerplate/issues/440 54 | */ 55 | 56 | audio, 57 | canvas, 58 | iframe, 59 | img, 60 | svg, 61 | video { 62 | vertical-align: middle; 63 | } 64 | 65 | /* 66 | * Remove default fieldset styles. 67 | */ 68 | 69 | fieldset { 70 | border: 0; 71 | margin: 0; 72 | padding: 0; 73 | } 74 | 75 | /* 76 | * Allow only vertical resizing of textareas. 77 | */ 78 | 79 | textarea { 80 | resize: vertical; 81 | } 82 | 83 | /* ========================================================================== 84 | Helper classes 85 | ========================================================================== */ 86 | 87 | /* 88 | * Hide visually and from screen readers: 89 | */ 90 | 91 | .hidden { 92 | display: none !important; 93 | } 94 | 95 | /* 96 | * Hide only visually, but have it available for screen readers: 97 | * http://snook.ca/archives/html_and_css/hiding-content-for-accessibility 98 | */ 99 | 100 | .visuallyhidden { 101 | border: 0; 102 | clip: rect(0 0 0 0); 103 | height: 1px; 104 | margin: -1px; 105 | overflow: hidden; 106 | padding: 0; 107 | position: absolute; 108 | width: 1px; 109 | } 110 | 111 | /* 112 | * Extends the .visuallyhidden class to allow the element 113 | * to be focusable when navigated to via the keyboard: 114 | * https://www.drupal.org/node/897638 115 | */ 116 | 117 | .visuallyhidden.focusable:active, 118 | .visuallyhidden.focusable:focus { 119 | clip: auto; 120 | height: auto; 121 | margin: 0; 122 | overflow: visible; 123 | position: static; 124 | width: auto; 125 | } 126 | 127 | /* 128 | * Hide visually and from screen readers, but maintain layout 129 | */ 130 | 131 | .invisible { 132 | visibility: hidden; 133 | } 134 | 135 | /* 136 | * Clearfix: contain floats 137 | * 138 | * For modern browsers 139 | * 1. The space content is one way to avoid an Opera bug when the 140 | * `contenteditable` attribute is included anywhere else in the document. 141 | * Otherwise it causes space to appear at the top and bottom of elements 142 | * that receive the `clearfix` class. 143 | * 2. The use of `table` rather than `block` is only necessary if using 144 | * `:before` to contain the top-margins of child elements. 145 | */ 146 | 147 | .clearfix:before, 148 | .clearfix:after { 149 | content: " "; /* 1 */ 150 | display: table; /* 2 */ 151 | } 152 | 153 | .clearfix:after { 154 | clear: both; 155 | } 156 | -------------------------------------------------------------------------------- /js/dom.js: -------------------------------------------------------------------------------- 1 | var g_PleaseWaitIntervalId = null; 2 | var g_ReservationTotal = 0; //shitty hax 3 | var g_RunningTotal = 0; //shitty hax 4 | var g_Zones = []; 5 | 6 | function displayEc2DataTable(data) { 7 | getEc2DataTableBody().innerHTML = ""; 8 | getEc2DataTableBody().innerHTML += buildEc2DataTable(data); 9 | getTotalDiv().innerHTML = "Total Reservations: " + g_ReservationTotal + 10 | " --- Total Running Instances: " + g_RunningTotal + 11 | " ---

"; 13 | // let it be sortable :) 14 | new Tablesort(document.getElementById('resCoTable')); 15 | document.getElementById("differentialHeader").click(); 16 | hidePleaseWaitDiv(); 17 | setupDownloadAsCSVButtonClick(); 18 | showAwsQueryResults(); 19 | } 20 | 21 | function setupDownloadAsCSVButtonClick() { 22 | document.getElementById("downloadAsCSV").addEventListener("click", function() { 23 | getTableDataAsCSV(); 24 | }); 25 | } 26 | 27 | // Currently not in use 28 | function buildZoneCheckListWithButton() { 29 | var html = "
"; 30 | for (var x = 0; x < g_Zones.length; x++) { 31 | html += "
" + 33 | "    "; 34 | } 35 | html += "
"; 37 | return html; 38 | } 39 | 40 | function resetEc2DataTable() { 41 | hideAwsQueryResults(); 42 | getEc2DataTableBody().innerHTML = ""; 43 | getTotalDiv().innerHTML = ""; 44 | g_ReservationTotal = 0; 45 | g_RunningTotal = 0; 46 | } 47 | 48 | function showQueryError(htmlSnippit) { 49 | getEc2DataTableBody().innerHTML = htmlSnippit; 50 | } 51 | 52 | // TODO: Make this prettier :) 53 | function buildEc2DataTable(data) { 54 | var htmlSnippit = ""; 55 | var dataLen = data.length; 56 | for (var i = 0; i < dataLen; i++) { 57 | htmlSnippit += "" + 58 | data[i]["count"] + 59 | "" + data[i]["running"] + 60 | "" + data[i]["diff"] + 61 | "" + data[i]["type"] + 62 | "" + data[i]["az"] + 63 | "" + data[i]["windows"].toString() + 64 | "" + data[i]["vpc"].toString() + 65 | "" + data[i]["running_ids"].join(",
") + 66 | "" + data[i]["running_names"].join(",
") + 67 | ""; 68 | g_ReservationTotal += data[i]["count"]; 69 | g_RunningTotal += data[i]["running"]; 70 | if (!g_Zones.contains(data[i]["az"])) { 71 | g_Zones.push(data[i]["az"]); 72 | } 73 | } 74 | return htmlSnippit; 75 | } 76 | 77 | function setPleaseWaitDivUpdateInterval() { 78 | g_PleaseWaitIntervalId = setInterval(updatePleaseWaitDiv, 1000); 79 | } 80 | 81 | function updatePleaseWaitDiv() { 82 | if (getPleaseWaitDiv().innerHTML.toLowerCase().indexOf("please wait.....") == -1) { 83 | getPleaseWaitDiv().innerHTML += "." 84 | } else { 85 | getPleaseWaitDiv().innerHTML = "Please Wait" 86 | } 87 | } 88 | 89 | function getTotalDiv() { 90 | return document.getElementById("totalData"); 91 | } 92 | 93 | function getPleaseWaitDiv() { 94 | return document.getElementById("pleaseWait"); 95 | } 96 | 97 | function showPleaseWaitDiv() { 98 | setPleaseWaitDivUpdateInterval(); 99 | getPleaseWaitDiv().style.display = "block"; 100 | } 101 | 102 | function hidePleaseWaitDiv() { 103 | clearPleaseWaitInterval(); 104 | getPleaseWaitDiv().style.display = "none"; 105 | } 106 | 107 | function clearPleaseWaitInterval() { 108 | clearInterval(g_PleaseWaitIntervalId); 109 | } 110 | 111 | function getCredentialsErrorDiv() { 112 | return document.getElementById("errorCredentials"); 113 | } 114 | 115 | function showCredentialsErrorDiv() { 116 | hidePleaseWaitDiv(); 117 | getCredentialsErrorDiv().style.display = "block"; 118 | } 119 | 120 | function hideCredentialsErrorDiv() { 121 | getCredentialsErrorDiv().style.display = "none"; 122 | } 123 | 124 | function getAccessSecretErrorDiv() { 125 | return document.getElementById("errorAccessSecretKey"); 126 | } 127 | 128 | function showAccessSecretErrorDiv() { 129 | hidePleaseWaitDiv(); 130 | getAccessSecretErrorDiv().style.display = "block"; 131 | } 132 | 133 | function hideAccessSecretErrorDiv() { 134 | getAccessSecretErrorDiv().style.display = "none"; 135 | } 136 | 137 | function getEc2DataTableBody() { 138 | return document.getElementById("ec2DataTableBody"); 139 | } 140 | 141 | function getAwsQueryResultsDiv() { 142 | return document.getElementById("awsQueryResults"); 143 | } 144 | 145 | function showAwsQueryResults() { 146 | getAwsQueryResultsDiv().style.display = "block"; 147 | } 148 | 149 | function hideAwsQueryResults() { 150 | getAwsQueryResultsDiv().style.display = "none"; 151 | } 152 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWSResco 2 | 3 | ### Table of Contents 4 | - [What](#what) 5 | - [Screenshot Example](#screenshot-example) 6 | - [Why?](#why) 7 | - [How it works](#how-it-works) 8 | - [Security?](#security) 9 | - [AWS IAM Policy for access](#aws-iam-policy-for-access) 10 | - [Gaps](#gaps) 11 | - [Development](#development) 12 | - [Test](#test) 13 | - [Build](#build) 14 | - [Deploy](#deploy) 15 | - [Build & Deploy Gaps](#build--deploy-gaps) 16 | 17 | * * * 18 | 19 | ## What 20 | AWSResco is a standalone SPA using the client side AWS JS SDK to get instance and reservation info for JIT comparison of running infrastructure. This comparison provides a "Differential" value to the user to see where they are over or under allotted on their reservations. 21 | 22 | The latest version is at: [http://awsresco.s3-website-us-east-1.amazonaws.com/](http://awsresco.s3-website-us-east-1.amazonaws.com/) 23 | 24 | ### Screenshot Example 25 | ![img](readme-assets/images/pixelated_rescompare_v1.0.3.png?raw=true) 26 | 27 | ## Why? 28 | In my time with working with AWS, the question of "Am I utilizing my reservations correctly?" has come up many times. AWS doesn't provide an interface, tool, or output that I am aware of that can provide this information in an easily consumable way. The closest tool AWS has is [Reservation Utilization Reports](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/usage-reports-ri.html) which works well in some cases, mostly smaller organizations, but lacks granularity for others. 29 | 30 | ## How it works 31 | AWS Reservation Comparison (AWSResco) takes a look at AWS reservations for a given account and compares them against running infrastructure. It combines multiple reservation purchases of the same `Instance Type`, `Availability Zone`, `Platform` (windows or linux), and `VPC` (EC2-Classic or VPC) into a single object, then looks at all running instances and matches them with any reservation based on the same values. 32 | 33 | This information is then presented to the user as: 34 | - The number of reservations of a given type, zone, platform, and vpc: `Reservation Count` 35 | - The number of running instances of a given type, zone, platform, and vpc: `Running Instances` 36 | - A differential, the number of reservations minus the number of running instances: `Differential` 37 | - The hard data on each reservation & running instance sets: `Type`, `Zone`, `Windows`, `VPC`, `Runnings Ids`, and `Running Names` 38 | 39 | ## Security? 40 | While AWSResco itself doesn't use HTTPS from S3 to the client's browser, all communication from the client's browser to AWS is over HTTPS. 41 | This means that your access and secret keys are never sent from your browser over anything other than HTTPS. 42 | This is enabled via [http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Config.html#sslEnabled-property](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Config.html#sslEnabled-property). 43 | 44 | If you are worried about what the tool does with your keys, please feel free to review the code for any malicious usage. If you find anything concering, feel free to open an issue or pull request. 45 | 46 | ## AWS IAM Policy for access 47 | The policy you use for AWSResco should follow the least privilege access rules. In the case of AWSResco, the only access needed is for the the [describeInstances](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/EC2.html#describeInstances-property) and the [describeReservedInstances](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/EC2.html#describeReservedInstances-property) API calls. The following is an example of the policy to use: 48 | 49 | ``` 50 | { 51 | "Version": "2012-10-17", 52 | "Statement": [ 53 | { 54 | "Sid": "Stmt1452989493668", 55 | "Action": [ 56 | "ec2:DescribeInstances", 57 | "ec2:DescribeReservedInstances" 58 | ], 59 | "Effect": "Allow", 60 | "Resource": "*" 61 | } 62 | ] 63 | } 64 | ``` 65 | 66 | If you are using an IAM instance profile you need to use the (access key, secret key, token) tuple to authenticate. 67 | You can retrieve the values from the EC2 metadata API. First, find the name of your instance-profile: 68 | 69 | `curl http://169.254.169.254/latest/meta-data/iam/security-credentials/` 70 | 71 | Then, plug in the name at the end of the URL 72 | 73 | `curl http://169.254.169.254/latest/meta-data/iam/security-credentials/instance-profile` 74 | 75 | The resulting JSON will have the necessary credentials to utilize the site. 76 | 77 | ## Gaps 78 | Currently AWSResco does not take into account `OfferingType`, it assumes that only `Heavy Utilization` is being used as that was the original use case for the tool. There are plans to support all `OfferingType` variations - see [Issue#3](https://github.com/ckelner/AWSResco/issues/3). 79 | 80 | ## Development 81 | - Run with: `python -m SimpleHTTPServer` (or your favorite web server) 82 | - Navigate to: `localhost:8000/dev.html` (or your favorite web server's configuration) 83 | 84 | ## Test 85 | - Manual process to test 86 | 87 | ## Build 88 | - Run `bash build.sh` which will uglify css and javascript 89 | 90 | ## Deploy 91 | - Manual process of pushing to S3 92 | 93 | ## Build & Deploy Gaps 94 | - Changes to `dev.html` need to be copied to `index.html` at this time. 95 | - No publish scripts to get new version to S3. 96 | -------------------------------------------------------------------------------- /dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | AWSResco 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 22 |
23 |
24 | Fork me on GitHub 25 |
26 |
27 |
28 | 39 |
40 |
41 |
42 | 43 | 44 |
45 |
46 |
47 |
48 | 49 | 50 |
51 |
52 |
53 |
54 | 55 | 56 |
57 |
58 |
59 | 71 |
72 |
73 | 74 |
75 |
76 |
77 |
Please Wait
78 |
79 | 82 |
83 |
84 | 87 |
88 |
89 |
90 | 91 | 92 | 93 | 96 | 99 | 102 | 105 | 108 | 111 | 114 | 117 | 120 | 121 | 122 | 123 | 124 |
94 | Reservation Count 95 | 97 | Running Instances 98 | 100 | Differential 101 | 103 | Type 104 | 106 | Zone 107 | 109 | Windows 110 | 112 | VPC 113 | 115 | Running Ids 116 | 118 | Running Names 119 |
125 |
126 |
127 |
128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | AWSResco 8 | 9 | 10 | 11 | 12 | 13 | 16 | 17 | 18 | 19 |
20 |
21 | Fork me on GitHub 22 |
23 |
24 |
25 | 36 |
37 |
38 |
39 | 40 | 41 |
42 |
43 |
44 |
45 | 46 | 47 |
48 |
49 |
50 |
51 | 52 | 53 |
54 |
55 |
56 | 68 |
69 |
70 | 71 |
72 |
73 |
74 |
Please Wait
75 |
76 | 79 |
80 |
81 | 84 |
85 |
86 |
87 | 88 | 89 | 90 | 93 | 96 | 99 | 102 | 105 | 108 | 111 | 114 | 117 | 118 | 119 | 120 | 121 |
91 | Reservation Count 92 | 94 | Running Instances 95 | 97 | Differential 98 | 100 | Type 101 | 103 | Zone 104 | 106 | Windows 107 | 109 | VPC 110 | 112 | Running Ids 113 | 115 | Running Names 116 |
122 |
123 |
124 |
125 | 126 | 127 | 128 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS and IE text size adjust after device orientation change, 6 | * without disabling user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | * and Firefox. 30 | * Correct `block` display not defined for `main` in IE 11. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | */ 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; /* 1 */ 59 | vertical-align: baseline; /* 2 */ 60 | } 61 | 62 | /** 63 | * Prevent modern browsers from displaying `audio` without controls. 64 | * Remove excess height in iOS 5 devices. 65 | */ 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22. 75 | */ 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | /* Links 83 | ========================================================================== */ 84 | 85 | /** 86 | * Remove the gray background color from active links in IE 10. 87 | */ 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | /** 94 | * Improve readability of focused elements when they are also in an 95 | * active/hover state. 96 | */ 97 | 98 | a:active, 99 | a:hover { 100 | outline: 0; 101 | } 102 | 103 | /* Text-level semantics 104 | ========================================================================== */ 105 | 106 | /** 107 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 108 | */ 109 | 110 | abbr[title] { 111 | border-bottom: 1px dotted; 112 | } 113 | 114 | /** 115 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 116 | */ 117 | 118 | b, 119 | strong { 120 | font-weight: bold; 121 | } 122 | 123 | /** 124 | * Address styling not present in Safari and Chrome. 125 | */ 126 | 127 | dfn { 128 | font-style: italic; 129 | } 130 | 131 | /** 132 | * Address variable `h1` font-size and margin within `section` and `article` 133 | * contexts in Firefox 4+, Safari, and Chrome. 134 | */ 135 | 136 | h1 { 137 | font-size: 2em; 138 | margin: 0.67em 0; 139 | } 140 | 141 | /** 142 | * Address styling not present in IE 8/9. 143 | */ 144 | 145 | mark { 146 | background: #ff0; 147 | color: #000; 148 | } 149 | 150 | /** 151 | * Address inconsistent and variable font size in all browsers. 152 | */ 153 | 154 | small { 155 | font-size: 80%; 156 | } 157 | 158 | /** 159 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 160 | */ 161 | 162 | sub, 163 | sup { 164 | font-size: 75%; 165 | line-height: 0; 166 | position: relative; 167 | vertical-align: baseline; 168 | } 169 | 170 | sup { 171 | top: -0.5em; 172 | } 173 | 174 | sub { 175 | bottom: -0.25em; 176 | } 177 | 178 | /* Embedded content 179 | ========================================================================== */ 180 | 181 | /** 182 | * Remove border when inside `a` element in IE 8/9/10. 183 | */ 184 | 185 | img { 186 | border: 0; 187 | } 188 | 189 | /** 190 | * Correct overflow not hidden in IE 9/10/11. 191 | */ 192 | 193 | svg:not(:root) { 194 | overflow: hidden; 195 | } 196 | 197 | /* Grouping content 198 | ========================================================================== */ 199 | 200 | /** 201 | * Address margin not present in IE 8/9 and Safari. 202 | */ 203 | 204 | figure { 205 | margin: 1em 40px; 206 | } 207 | 208 | /** 209 | * Address differences between Firefox and other browsers. 210 | */ 211 | 212 | hr { 213 | box-sizing: content-box; 214 | height: 0; 215 | } 216 | 217 | /** 218 | * Contain overflow in all browsers. 219 | */ 220 | 221 | pre { 222 | overflow: auto; 223 | } 224 | 225 | /** 226 | * Address odd `em`-unit font size rendering in all browsers. 227 | */ 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | /* Forms 238 | ========================================================================== */ 239 | 240 | /** 241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | * styling of `select`, unless a `border` property is set. 243 | */ 244 | 245 | /** 246 | * 1. Correct color not being inherited. 247 | * Known issue: affects color of disabled elements. 248 | * 2. Correct font properties not being inherited. 249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | color: inherit; /* 1 */ 258 | font: inherit; /* 2 */ 259 | margin: 0; /* 3 */ 260 | } 261 | 262 | /** 263 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | */ 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | /** 271 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | * All other form control elements do not inherit `text-transform` values. 273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | * Correct `select` style inheritance in Firefox. 275 | */ 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | /** 283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | * and `video` controls. 285 | * 2. Correct inability to style clickable `input` types in iOS. 286 | * 3. Improve usability and consistency of cursor style between image-type 287 | * `input` and others. 288 | */ 289 | 290 | button, 291 | html input[type="button"], /* 1 */ 292 | input[type="reset"], 293 | input[type="submit"] { 294 | -webkit-appearance: button; /* 2 */ 295 | cursor: pointer; /* 3 */ 296 | } 297 | 298 | /** 299 | * Re-set default cursor for disabled elements. 300 | */ 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | /** 308 | * Remove inner padding and border in Firefox 4+. 309 | */ 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | border: 0; 314 | padding: 0; 315 | } 316 | 317 | /** 318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 319 | * the UA stylesheet. 320 | */ 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | /** 327 | * It's recommended that you don't attempt to style these elements. 328 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | * 330 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | * 2. Remove excess padding in IE 8/9/10. 332 | */ 333 | 334 | input[type="checkbox"], 335 | input[type="radio"] { 336 | box-sizing: border-box; /* 1 */ 337 | padding: 0; /* 2 */ 338 | } 339 | 340 | /** 341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | * `font-size` values of the `input`, it causes the cursor style of the 343 | * decrement button to change from `default` to `text`. 344 | */ 345 | 346 | input[type="number"]::-webkit-inner-spin-button, 347 | input[type="number"]::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | /** 352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome. 354 | */ 355 | 356 | input[type="search"] { 357 | -webkit-appearance: textfield; /* 1 */ 358 | box-sizing: content-box; /* 2 */ 359 | } 360 | 361 | /** 362 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 363 | * Safari (but not Chrome) clips the cancel button when the search input has 364 | * padding (and `textfield` appearance). 365 | */ 366 | 367 | input[type="search"]::-webkit-search-cancel-button, 368 | input[type="search"]::-webkit-search-decoration { 369 | -webkit-appearance: none; 370 | } 371 | 372 | /** 373 | * Define consistent border, margin, and padding. 374 | */ 375 | 376 | fieldset { 377 | border: 1px solid #c0c0c0; 378 | margin: 0 2px; 379 | padding: 0.35em 0.625em 0.75em; 380 | } 381 | 382 | /** 383 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 384 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 385 | */ 386 | 387 | legend { 388 | border: 0; /* 1 */ 389 | padding: 0; /* 2 */ 390 | } 391 | 392 | /** 393 | * Remove default vertical scrollbar in IE 8/9/10/11. 394 | */ 395 | 396 | textarea { 397 | overflow: auto; 398 | } 399 | 400 | /** 401 | * Don't inherit the `font-weight` (applied by a rule above). 402 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 403 | */ 404 | 405 | optgroup { 406 | font-weight: bold; 407 | } 408 | 409 | /* Tables 410 | ========================================================================== */ 411 | 412 | /** 413 | * Remove most spacing between table cells. 414 | */ 415 | 416 | table { 417 | border-collapse: collapse; 418 | border-spacing: 0; 419 | } 420 | 421 | td, 422 | th { 423 | padding: 0; 424 | } 425 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | function e(){$=[],U=[],clearInterval(Y),Y=null}function n(n,s,r,a){if(null!=n&&null!=s){if(G=a,w(),e(),-1!=a.indexOf(q))for(var i=0;i0&&U.length>0&&(clearInterval(Y),g(r($,U)))}function s(e){for(var n=e.length,t=[],s=0;n>s;s++)Array.prototype.push.apply(t,e[s].data);return t}function r(e,n){for(var t=s(e),r=s(n),o=r.length,a=[],i=0,c={},l=[],u=0;o>u;u++){var d=JSON.parse(JSON.stringify(r[u])),f=d.type+d.az+d.windows+d.vpc;if(!l.contains(f)){void 0===c[f]&&(c[f]=i,l.push(f),a[i]=JSON.parse(JSON.stringify(d)),i++);for(var v=u+1;o>v;v++){var p=JSON.parse(JSON.stringify(r[v]));d.type===p.type&&d.az===p.az&&d.windows===p.windows&&d.vpc===p.vpc&&u!==v&&p!==d&&(a[c[f]].count+=p.count,Array.prototype.push.apply(a[c[f]].resIds,p.resIds))}}}for(var g=t.length,h=0;g>h;h++){for(var m=a.length,w=t[h],y=!1,I=0;m>I;I++){null==a[I].running&&(a[I].running=0,a[I].running_ids=[],a[I].running_names=[],a[I].diff=a[I].count);var b=a[I];if(w.type==b.type&&w.az==b.az&&w.windows==b.windows&&w.vpc==b.vpc){a[I].running+=1,a[I].diff-=1,a[I].running_ids.push(w.id),void 0!=w.name&&null!=w.name&&""!=w.name&&a[I].running_names.push(w.name),y=!0;break}}if(y===!1){var E={};E.running=1,E.running_ids=[w.id],E.running_names=[],void 0!=w.name&&null!=w.name&&""!=w.name&&E.running_names.push(w.name),E.diff=-1,E.resIds=[],E.type=w.type,E.count=0,E.az=w.az,E.cost=0,E.windows=w.windows,E.vpc=w.vpc,a.push(E)}}return a}function o(e,n,t,s,r){var o=new AWS.EC2({accessKeyId:n,secretAccessKey:t,sessionToken:s,region:e,maxRetries:5,sslEnabled:!0});if(r){var i={Filters:[{Name:"state",Values:["active"]}]};o.describeReservedInstances(i,function(n,t){a(n,t,e,r)})}else{var i={Filters:[{Name:"instance-state-name",Values:["running"]}],MaxResults:1e3};o.describeInstances(i,function(n,t){a(n,t,e,r)})}}function a(e,n,t,s){e?(console.log(e,e.stack),y("The following error has occured: "+e+"; see the javascript console for more details.")):i(n,t,s)}function i(e,n,t){t?U.push({region:n,data:c(e)}):$.push({region:n,data:l(e)})}function c(e){var n=[];if(!e||!e.ReservedInstances||0==e.ReservedInstances.length)return n;for(var t=e.ReservedInstances.length,s=0;t>s;s++)n[s]={},n[s].resIds=[e.ReservedInstances[s].ReservedInstancesId],n[s].type=e.ReservedInstances[s].InstanceType,n[s].count=e.ReservedInstances[s].InstanceCount,n[s].az=e.ReservedInstances[s].AvailabilityZone,e.ReservedInstances[s].RecurringCharges[0]&&(n[s].cost=e.ReservedInstances[s].RecurringCharges[0].Amount),-1!=e.ReservedInstances[s].ProductDescription.toLowerCase().indexOf("windows")?n[s].windows=!0:n[s].windows=!1,-1!=e.ReservedInstances[s].ProductDescription.toLowerCase().indexOf("vpc")?n[s].vpc=!0:n[s].vpc=!1;return n}function l(e){var n=[];if(!e||!e.Reservations||0==e.Reservations.length)return n;for(var t=e.Reservations.length,s=0,r=0;t>r;r++)for(var o=0;o1)for(var c=a.length,l=0;c>l;l++)try{if(null!=a[l].Key)a[l].Key.toLowerCase()=="aws:autoscaling:groupName".toLowerCase()&&""==i&&(i=a[l].Value),"name"==a[l].Key.toLowerCase()&&(i=a[l].Value);else try{console.log("Tags["+l+"] empty: "+a.toString())}catch(e){console.log("Exception Id 00x1")}}catch(e){console.log("Exception Id 00x3")}else if(1==a.length)i=a[0].Value;else try{console.log("No name tag found for instance with id: "+e.Reservations[r].Instances[o].InstanceId)}catch(e){console.log("Exception Id 00x2")}n[s]={},n[s].name=i,n[s].id=e.Reservations[r].Instances[o].InstanceId,n[s].type=e.Reservations[r].Instances[o].InstanceType,n[s].az=e.Reservations[r].Instances[o].Placement.AvailabilityZone;try{void 0!=e.Reservations[r].Instances[o].Platform&&null!=e.Reservations[r].Instances[0].Platform&&"windows"==e.Reservations[r].Instances[o].Platform.toLowerCase()?n[s].windows=!0:n[s].windows=!1}catch(e){console.log("Exception Id 00x4")}null!=e.Reservations[r].Instances[o].VpcId&&""!=e.Reservations[r].Instances[o].VpcId?n[s].vpc=!0:n[s].vpc=!1,s++}return n}function u(e,n,t,s){d(),"all"==s.toLowerCase()&&(s="us-east-1"),v();var r=new AWS.EC2({accessKeyId:e,secretAccessKey:n,sessionToken:t,region:s,maxRetries:5,sslEnabled:!0});try{r.describeRegions({},function(e,n){e?(console.log("BAD CREDENTIALS"),console.log(e,e.stack),X=!1,ee=!0):(X=!0,ee=!0)})}catch(e){return console.log("BAD CREDENTIALS"),console.log(e),!1}}function d(){X=!1,ee=!1}function f(){ne=setInterval(p,1e3)}function v(){clearInterval(ne)}function p(){ee&&(clearInterval(p),w(),X?(n(P(),J(),W(),j()),d()):(B(),d()))}function g(e){O().innerHTML="",O().innerHTML+=I(e),x().innerHTML="Total Reservations: "+se+" --- Total Running Instances: "+re+" ---

",new Tablesort(document.getElementById("resCoTable")),document.getElementById("differentialHeader").click(),T(),h(),F()}function h(){document.getElementById("downloadAsCSV").addEventListener("click",function(){D()})}function m(){for(var e="
",n=0;n    ";return e+=""}function w(){M(),O().innerHTML="",x().innerHTML="",se=0,re=0}function y(e){O().innerHTML=e}function I(e){for(var n="",t=e.length,s=0;t>s;s++)n+=""+e[s].count+""+e[s].running+""+e[s].diff+""+e[s].type+""+e[s].az+""+e[s].windows.toString()+""+e[s].vpc.toString()+""+e[s].running_ids.join(",
")+""+e[s].running_names.join(",
")+"",se+=e[s].count,re+=e[s].running,oe.contains(e[s].az)||oe.push(e[s].az);return n}function b(){te=setInterval(E,1e3)}function E(){-1==L().innerHTML.toLowerCase().indexOf("please wait.....")?L().innerHTML+=".":L().innerHTML="Please Wait"}function x(){return document.getElementById("totalData")}function L(){return document.getElementById("pleaseWait")}function R(){b(),L().style.display="block"}function T(){C(),L().style.display="none"}function C(){clearInterval(te)}function A(){return document.getElementById("errorCredentials")}function B(){T(),A().style.display="block"}function S(){A().style.display="none"}function k(){return document.getElementById("errorAccessSecretKey")}function N(){T(),k().style.display="block"}function H(){k().style.display="none"}function O(){return document.getElementById("ec2DataTableBody")}function z(){return document.getElementById("awsQueryResults")}function F(){z().style.display="block"}function M(){z().style.display="none"}function D(){for(var e,n=document.getElementById("resCoTable"),t="data:text/csv;charset=utf-8,",s=0;e=n.rows[s];s++){for(var r,o=0;r=e.cells[o];o++){o>0&&(t+=",");var a=r.innerHTML.toString().trim();(a.indexOf(",")>=0||a.indexOf('"')>=0||a.indexOf("\n")>=0)&&(a='"'+a.replace(/\"/g,'""')+'"'),a.indexOf("
")>=0&&(a=a.replace(/
/g,"")),t+=a}t+="\n"}var i=document.createElement("a");i.setAttribute("href",encodeURI(t)),i.setAttribute("download",_()),i.click()}function _(){var e="awsresco-export_",n=new Date;return e+=n.getFullYear()+"-"+(n.getMonth()+1)+"-"+n.getDate()+"_"+n.getHours()+":"+n.getMinutes()+":"+n.getSeconds()}function K(){document.getElementById("awsQueryButton").addEventListener("click",function(){Q()})}function V(e){return void 0==e||null==e||""==e?(N(),null):e}function P(){return V(document.getElementById("awsAccessKey").value)}function J(){return V(document.getElementById("awsSecretKey").value)}function W(){return document.getElementById("awsToken").value}function j(){return document.getElementById("regionSelect").value}function Q(){return d(),R(),H(),S(),u(P(),J(),W(),j()),f(),!1}var Z=["us-east-1","us-west-1","us-west-2","eu-west-1","eu-central-1","ap-southeast-1","ap-southeast-2","ap-northeast-1","sa-east-1"],$=[],U=[],Y=null,q="ALL",G="ALL",X=!1,ee=!1,ne=null,te=null,se=0,re=0,oe=[];String.prototype.trim||(String.prototype.trim=function(){return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")}),window.onload=function(){K()},Array.prototype.contains=function(e){for(var n=this.length;n--;)if(this[n]===e)return!0;return!1},!function(){function e(n,t){if(!(this instanceof e))return new e(n,t);if(!n||"TABLE"!==n.tagName)throw new Error("Element must be a table");this.init(n,t||{})}var n=[],t=function(e){var n;return window.CustomEvent&&"function"==typeof window.CustomEvent?n=new CustomEvent(e):(n=document.createEvent("CustomEvent"),n.initCustomEvent(e,!1,!1,void 0)),n},s=function(e){return e.getAttribute("data-sort")||e.textContent||e.innerText||""},r=function(e,n){return e=e.toLowerCase(),n=n.toLowerCase(),e===n?0:n>e?1:-1},o=function(e,n){return function(t,s){var r=e(t.td,s.td);return 0===r?n?s.index-t.index:t.index-s.index:r}};e.extend=function(e,t,s){if("function"!=typeof t||"function"!=typeof s)throw new Error("Pattern and sort must be a function");n.push({name:e,pattern:t,sort:s})},e.prototype={init:function(e,n){var t,s,r,o,a=this;if(a.table=e,a.thead=!1,a.options=n,e.rows&&e.rows.length>0&&(e.tHead&&e.tHead.rows.length>0?(t=e.tHead.rows[e.tHead.rows.length-1],a.thead=!0):t=e.rows[0]),t){var i=function(){a.current&&a.current!==this&&(a.current.classList.remove("sort-up"),a.current.classList.remove("sort-down")),a.current=this,a.sortTable(this)};for(r=0;r0&&f.push(d),v++;if(!f)return}for(v=0;vv;v++)w[v]?(d=w[v],I++):d=m[v-I].tr,c.table.tBodies[0].appendChild(d);c.table.dispatchEvent(t("afterSort"))}},refresh:function(){void 0!==this.current&&this.sortTable(this.current,!0)}},"undefined"!=typeof module&&module.exports?module.exports=e:window.Tablesort=e}(),function(){var e=function(e){return e.replace(/[^\-?0-9.]/g,"")},n=function(e,n){return e=parseFloat(e),n=parseFloat(n),e=isNaN(e)?0:e,n=isNaN(n)?0:n,e-n};Tablesort.extend("number",function(e){return e.match(/^-?[£\x24Û¢´€]?\d+\s*([,\.]\d{0,2})/)||e.match(/^-?\d+\s*([,\.]\d{0,2})?[£\x24Û¢´€]/)||e.match(/^-?(\d)*-?([,\.]){0,1}-?(\d)+([E,e][\-+][\d]+)?%?$/)},function(t,s){return t=e(t),s=e(s),n(s,t)})}(); -------------------------------------------------------------------------------- /js/aws.js: -------------------------------------------------------------------------------- 1 | var g_AWSRegions = [ 2 | 'us-east-1', 3 | 'us-west-1', 4 | 'us-west-2', 5 | 'eu-west-1', 6 | 'eu-central-1', 7 | 'ap-southeast-1', 8 | 'ap-southeast-2', 9 | 'ap-northeast-1', 10 | 'sa-east-1' 11 | ]; 12 | var g_EC2Data = []; 13 | var g_EC2ResData = []; 14 | var g_EC2DataTimer = null; 15 | var g_ALL_Region_Const = "ALL" 16 | var g_REGION = "ALL"; //default 17 | var g_GOOD_CREDS = false; 18 | var g_CREDS_CHECKED = false; 19 | var g_WAIT_FOR_CRED_CHECK = null; 20 | 21 | function resetAWSValues() { 22 | g_EC2Data = []; 23 | g_EC2ResData = []; 24 | clearInterval(g_EC2DataTimer); 25 | g_EC2DataTimer = null; 26 | } 27 | 28 | function queryAllAWSRegionsForEC2Data(key, secret, token, region) { 29 | if (key != null && secret != null) { 30 | g_REGION = region; 31 | resetEc2DataTable(); 32 | resetAWSValues(); 33 | if (region.indexOf(g_ALL_Region_Const) != -1) { 34 | for (var i = 0; i < g_AWSRegions.length; i++) { 35 | queryAWSforEC2Data(g_AWSRegions[i], key, secret, token, false); 36 | queryAWSforEC2Data(g_AWSRegions[i], key, secret, token, true); 37 | } 38 | } else { 39 | queryAWSforEC2Data(g_REGION, key, secret, token, false); 40 | queryAWSforEC2Data(g_REGION, key, secret, token, true); 41 | } 42 | // Because of the asynchronous nature of the AWS SDK calls, we need to 43 | // wait until all data is returned for all regions before we proceed 44 | g_EC2DataTimer = setInterval(waitForEC2ToGetReturned, 1000); 45 | } 46 | } 47 | 48 | function waitForEC2ToGetReturned() { 49 | if (g_REGION.indexOf(g_ALL_Region_Const) != -1) { 50 | if (g_EC2Data.length == g_AWSRegions.length && 51 | g_EC2ResData.length == g_AWSRegions.length) { 52 | clearInterval(g_EC2DataTimer); 53 | displayEc2DataTable( 54 | combineEC2AndResData(g_EC2Data, g_EC2ResData) 55 | ); 56 | } 57 | } else { 58 | if (g_EC2Data.length > 0 && g_EC2ResData.length > 0) { 59 | clearInterval(g_EC2DataTimer); 60 | displayEc2DataTable( 61 | combineEC2AndResData(g_EC2Data, g_EC2ResData) 62 | ); 63 | } 64 | } 65 | } 66 | 67 | // takes our custom data object segregated by region and 68 | // creates one giant array of all data 69 | function mergeDataFromAllRegionsIntoSingleArray(customRegionDataObj) { 70 | var objLen = customRegionDataObj.length; 71 | var newArr = []; 72 | for (var i = 0; i < objLen; i++) { 73 | Array.prototype.push.apply(newArr, customRegionDataObj[i].data); 74 | } 75 | return newArr; 76 | } 77 | 78 | function combineEC2AndResData(ec2, res) { 79 | var ec2Arr = mergeDataFromAllRegionsIntoSingleArray(ec2); 80 | var resArr = mergeDataFromAllRegionsIntoSingleArray(res); 81 | // first 82 | // merge all reservation data down 83 | // reservation data is grouped by "purchase" while we want to handle this 84 | // data by a unique combo of: type, az, windows, and vpc. 85 | var resLen = resArr.length; 86 | var newRes = []; 87 | var uniqCount = 0; 88 | var uniqKeeper = {}; 89 | var uniqArr = []; 90 | for (var i = 0; i < resLen; i++) { 91 | var resDataTop = JSON.parse(JSON.stringify(resArr[i])); // copy not reference 92 | var uniqResId = resDataTop["type"] + resDataTop["az"] + resDataTop["windows"] + resDataTop["vpc"]; 93 | if (uniqArr.contains(uniqResId)) { 94 | continue; 95 | } 96 | if (uniqKeeper[uniqResId] === undefined) { 97 | uniqKeeper[uniqResId] = uniqCount; 98 | uniqArr.push(uniqResId); 99 | newRes[uniqCount] = JSON.parse(JSON.stringify(resDataTop)); // copy not reference 100 | uniqCount++; 101 | } 102 | for (var y = i + 1; y < resLen; y++) { 103 | var resDataBottom = JSON.parse(JSON.stringify(resArr[y])); // copy not reference 104 | // TODO: take relevant data and mash it into the new array 105 | if ( 106 | resDataTop["type"] === resDataBottom["type"] && 107 | resDataTop["az"] === resDataBottom["az"] && 108 | resDataTop["windows"] === resDataBottom["windows"] && 109 | resDataTop["vpc"] === resDataBottom["vpc"] && 110 | i !== y && resDataBottom !== resDataTop // make sure we aren't looking at the same reservation 111 | ) { 112 | // we have the same reservation, just different purchase time 113 | //console.log(uniqResId + " --- " + newRes[uniqKeeper[uniqResId]]["count"] + " --- " + resDataBottom["count"]); 114 | newRes[uniqKeeper[uniqResId]]["count"] += resDataBottom["count"]; 115 | Array.prototype.push.apply(newRes[uniqKeeper[uniqResId]]["resIds"], resDataBottom["resIds"]); 116 | } 117 | } 118 | } 119 | // second 120 | // combine unique reservations with running ec2 instances 121 | var ec2Len = ec2Arr.length; 122 | for (var p = 0; p < ec2Len; p++) { 123 | var uniqResLen = newRes.length; 124 | var ec2Inst = ec2Arr[p]; 125 | var foundRes = false; 126 | for (var q = 0; q < uniqResLen; q++) { 127 | // setup for a new comparison 128 | if (newRes[q]["running"] == null) { 129 | newRes[q]["running"] = 0; 130 | newRes[q]["running_ids"] = []; 131 | newRes[q]["running_names"] = []; 132 | newRes[q]["diff"] = newRes[q]["count"]; 133 | } 134 | var resInst = newRes[q]; 135 | if ( 136 | ec2Inst["type"] == resInst["type"] && 137 | ec2Inst["az"] == resInst["az"] && 138 | ec2Inst["windows"] == resInst["windows"] && 139 | ec2Inst["vpc"] == resInst["vpc"] 140 | ) { 141 | // got a match 142 | newRes[q]["running"] += 1; 143 | newRes[q]["diff"] -= 1; 144 | newRes[q]["running_ids"].push(ec2Inst["id"]); 145 | if (ec2Inst["name"] != undefined && ec2Inst["name"] != null && ec2Inst["name"] != "") { 146 | newRes[q]["running_names"].push(ec2Inst["name"]); 147 | } 148 | foundRes = true; 149 | break; // exit since we found a match 150 | } 151 | } 152 | // if no reservation was found that matched, then add this to collection to report 153 | if (foundRes === false) { 154 | var aNewRes = {}; 155 | aNewRes["running"] = 1; 156 | aNewRes["running_ids"] = [ec2Inst["id"]]; 157 | aNewRes["running_names"] = []; 158 | if (ec2Inst["name"] != undefined && ec2Inst["name"] != null && ec2Inst["name"] != "") { 159 | aNewRes["running_names"].push(ec2Inst["name"]); 160 | } 161 | aNewRes["diff"] = -1; 162 | aNewRes["resIds"] = []; 163 | aNewRes["type"] = ec2Inst["type"]; 164 | aNewRes["count"] = 0; 165 | aNewRes["az"] = ec2Inst["az"]; 166 | aNewRes["cost"] = 0; 167 | aNewRes["windows"] = ec2Inst["windows"]; 168 | aNewRes["vpc"] = ec2Inst["vpc"]; 169 | newRes.push(aNewRes); 170 | } 171 | } 172 | return newRes; 173 | } 174 | 175 | function queryAWSforEC2Data(region, key, secret, token, reservations) { 176 | var ec2 = new AWS.EC2({ 177 | accessKeyId: key, 178 | secretAccessKey: secret, 179 | sessionToken: token, 180 | region: region, 181 | maxRetries: 5, 182 | // http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Config.html#sslEnabled-property 183 | sslEnabled: true 184 | }); 185 | if (!reservations) { 186 | var params = { 187 | Filters: [{ // only find running instances 188 | Name: 'instance-state-name', 189 | Values: ['running'] 190 | }, ], 191 | MaxResults: 1000 // max 192 | }; 193 | ec2.describeInstances(params, function(err, data) { 194 | handleAWSQueryReturnErr(err, data, region, reservations); 195 | }); 196 | } else { 197 | var params = { 198 | Filters: [{ // only active reservations 199 | Name: 'state', 200 | Values: ['active'] 201 | }, ], 202 | }; 203 | ec2.describeReservedInstances(params, function(err, data) { 204 | handleAWSQueryReturnErr(err, data, region, reservations); 205 | }); 206 | } 207 | } 208 | 209 | function handleAWSQueryReturnErr(err, data, region, reservations) { 210 | if (err) { 211 | console.log(err, err.stack); 212 | showQueryError("The following error has occured: " + 213 | err + "; see the javascript console for more details."); 214 | } else { 215 | //console.log(data); 216 | queryAWSReturn(data, region, reservations); 217 | } 218 | } 219 | 220 | function queryAWSReturn(regionData, region, reservations) { 221 | if (!reservations) { 222 | g_EC2Data.push({ 223 | "region": region, 224 | "data": mungeEc2Data(regionData) 225 | }); 226 | } else { 227 | g_EC2ResData.push({ 228 | "region": region, 229 | "data": mungeEc2ResData(regionData) 230 | }); 231 | } 232 | } 233 | 234 | function mungeEc2ResData(data) { 235 | var mungedDataArr = []; 236 | if (!data || !data.ReservedInstances || data.ReservedInstances.length == 0) { 237 | return mungedDataArr; 238 | } 239 | var dataLen = data.ReservedInstances.length; 240 | for (var i = 0; i < dataLen; i++) { 241 | mungedDataArr[i] = {}; 242 | mungedDataArr[i]["resIds"] = [data.ReservedInstances[i].ReservedInstancesId]; 243 | mungedDataArr[i]["type"] = data.ReservedInstances[i].InstanceType; 244 | mungedDataArr[i]["count"] = data.ReservedInstances[i].InstanceCount; 245 | mungedDataArr[i]["az"] = data.ReservedInstances[i].AvailabilityZone; 246 | if (data.ReservedInstances[i].RecurringCharges[0]) { 247 | mungedDataArr[i]["cost"] = data.ReservedInstances[i].RecurringCharges[0].Amount; 248 | } 249 | if (data.ReservedInstances[i].ProductDescription.toLowerCase().indexOf("windows") != -1) { 250 | mungedDataArr[i]["windows"] = true; 251 | } else { 252 | mungedDataArr[i]["windows"] = false; 253 | } 254 | if (data.ReservedInstances[i].ProductDescription.toLowerCase().indexOf("vpc") != -1) { 255 | mungedDataArr[i]["vpc"] = true; 256 | } else { 257 | mungedDataArr[i]["vpc"] = false; 258 | } 259 | } 260 | return mungedDataArr; 261 | } 262 | 263 | function mungeEc2Data(data) { 264 | var mungedDataArr = []; 265 | if (!data || !data.Reservations || data.Reservations.length == 0) { 266 | return mungedDataArr; 267 | } 268 | var dataLen = data.Reservations.length; 269 | var e = 0; 270 | for (var i = 0; i < dataLen; i++) { 271 | for (var n = 0; n < data.Reservations[i].Instances.length; n++) { 272 | var tags = data.Reservations[i].Instances[n].Tags; 273 | var name = ""; 274 | if (tags.length > 1) { 275 | var tagLen = tags.length; 276 | for (var y = 0; y < tagLen; y++) { 277 | try { 278 | if (tags[y].Key != null) { 279 | if (tags[y].Key.toLowerCase() == "aws:autoscaling:groupName".toLowerCase() && name == "") { 280 | name = tags[y].Value; 281 | } 282 | if (tags[y].Key.toLowerCase() == "name") { 283 | name = tags[y].Value; 284 | } 285 | } else { 286 | try { 287 | console.log("Tags[" + y + "] empty: " + tags.toString()); 288 | } catch (e) { 289 | console.log("Exception Id 00x1"); 290 | } 291 | } 292 | } catch (e) { 293 | console.log("Exception Id 00x3"); 294 | } 295 | } 296 | } else { 297 | if (tags.length == 1) { 298 | name = tags[0].Value; 299 | } else { 300 | try { 301 | console.log("No name tag found for instance with id: " + data.Reservations[i].Instances[n].InstanceId); 302 | } catch (e) { 303 | console.log("Exception Id 00x2"); 304 | } 305 | } 306 | } 307 | mungedDataArr[e] = {}; 308 | mungedDataArr[e]["name"] = name; 309 | mungedDataArr[e]["id"] = data.Reservations[i].Instances[n].InstanceId; 310 | mungedDataArr[e]["type"] = data.Reservations[i].Instances[n].InstanceType; 311 | mungedDataArr[e]["az"] = data.Reservations[i].Instances[n].Placement.AvailabilityZone; 312 | try { 313 | if (data.Reservations[i].Instances[n].Platform != undefined && data.Reservations[i].Instances[0].Platform != null) { 314 | if (data.Reservations[i].Instances[n].Platform.toLowerCase() == "windows") { 315 | mungedDataArr[e]["windows"] = true; 316 | } else { 317 | mungedDataArr[e]["windows"] = false; 318 | } 319 | } else { 320 | // Doesn't have 'Platform' defined, assuming linux 321 | mungedDataArr[e]["windows"] = false; 322 | } 323 | } catch (e) { 324 | console.log("Exception Id 00x4"); 325 | } 326 | if (data.Reservations[i].Instances[n].VpcId != null && 327 | data.Reservations[i].Instances[n].VpcId != "") { 328 | mungedDataArr[e]["vpc"] = true; 329 | } else { 330 | mungedDataArr[e]["vpc"] = false; 331 | } 332 | e++; 333 | } 334 | } 335 | return mungedDataArr; 336 | } 337 | 338 | function testAWSCredentials(key, secret, token, region) { 339 | resetCredChecks(); 340 | if (region.toLowerCase() == "all") { 341 | // pick us-east-1 should always be there 342 | region = "us-east-1"; 343 | } 344 | clearCredCheckInterval(); 345 | var ec2 = new AWS.EC2({ 346 | accessKeyId: key, 347 | secretAccessKey: secret, 348 | sessionToken: token, 349 | region: region, 350 | maxRetries: 5, 351 | sslEnabled: true 352 | }); 353 | try { 354 | ec2.describeRegions({}, function(err, data) { 355 | if (err) { 356 | console.log("BAD CREDENTIALS"); 357 | console.log(err, err.stack); 358 | g_GOOD_CREDS = false; 359 | g_CREDS_CHECKED = true; 360 | } else { 361 | g_GOOD_CREDS = true; 362 | g_CREDS_CHECKED = true; 363 | } 364 | }); 365 | } catch (e) { 366 | console.log("BAD CREDENTIALS"); 367 | console.log(e); 368 | return false; 369 | g_GOOD_CREDS = false; 370 | g_CREDS_CHECKED = true; 371 | } 372 | } 373 | 374 | function resetCredChecks() { 375 | g_GOOD_CREDS = false; 376 | g_CREDS_CHECKED = false; 377 | } 378 | 379 | function waitForCredCheck() { 380 | g_WAIT_FOR_CRED_CHECK = setInterval(checkCredCheck, 1000); 381 | } 382 | 383 | function clearCredCheckInterval() { 384 | clearInterval(g_WAIT_FOR_CRED_CHECK); 385 | } 386 | 387 | function checkCredCheck() { 388 | if (g_CREDS_CHECKED) { 389 | clearInterval(checkCredCheck); 390 | resetEc2DataTable(); 391 | if (g_GOOD_CREDS) { 392 | queryAllAWSRegionsForEC2Data( 393 | getAccessKeyValue(), 394 | getSecretKeyValue(), 395 | getTokenValue(), 396 | getRegionValue() 397 | ); 398 | resetCredChecks(); 399 | } else { 400 | showCredentialsErrorDiv(); 401 | resetCredChecks(); 402 | } 403 | } 404 | } 405 | --------------------------------------------------------------------------------