diff options
-rw-r--r-- | app/common/directives/app-header.js | 13 | ||||
-rw-r--r-- | app/common/directives/app-navigation.html | 22 | ||||
-rw-r--r-- | app/common/services/api-utils.js | 48 | ||||
-rw-r--r-- | app/common/services/dataService.js | 5 | ||||
-rw-r--r-- | app/index.js | 1 | ||||
-rw-r--r-- | app/server-health/controllers/syslog-controller.html | 100 | ||||
-rw-r--r-- | app/server-health/controllers/syslog-controller.js | 122 | ||||
-rw-r--r-- | app/server-health/index.js | 5 | ||||
-rw-r--r-- | app/server-health/styles/index.scss | 1 | ||||
-rw-r--r-- | app/server-health/styles/syslog.scss | 309 |
10 files changed, 616 insertions, 10 deletions
diff --git a/app/common/directives/app-header.js b/app/common/directives/app-header.js index 0e0a6e6..9e10619 100644 --- a/app/common/directives/app-header.js +++ b/app/common/directives/app-header.js @@ -95,10 +95,23 @@ window.angular && (function(angular) { }); }; + $scope.loadSystemName = function() { + // Dynamically get ComputerSystems Name/serial + // which differs across OEM's + APIUtils.getRedfishSysName().then( + function(res) { + dataService.setSystemName(res); + }, + function(error) { + console.log(JSON.stringify(error)); + }); + }; + function loadData() { $scope.loadServerStatus(); $scope.loadNetworkInfo(); $scope.loadServerHealth(); + $scope.loadSystemName(); } loadData(); diff --git a/app/common/directives/app-navigation.html b/app/common/directives/app-navigation.html index 2f3ded7..a45a24b 100644 --- a/app/common/directives/app-navigation.html +++ b/app/common/directives/app-navigation.html @@ -72,32 +72,34 @@ <a href="#/server-health/inventory-overview" tabindex="7">Hardware status</a></li> <li ng-class="{'active': (path == '/server-health/sensors-overview' || path == '/server-health/sensors')}"> <a href="#/server-health/sensors-overview" ng-click="closeSubnav()" tabindex="8">Sensors</a></li> + <li ng-class="{'active': (path == '/server-health/sys-log')}" ng-show="dataService.configJson.redfishSupportEnabled"> + <a href="#/server-health/sys-log" ng-click="closeSubnav()" tabindex="9">System Logs</a></li> </ul> <ul class="nav__second-level btn-control" ng-style="navStyle" ng-class="{opened: (showSubMenu && firstLevel == 'server-control')}"> <li ng-class="{'active': (path == '/server-control' || path == '/server-control/power-operations')}"> - <a href="#/server-control/power-operations" tabindex="9" ng-click="closeSubnav()">Server power operations</a> + <a href="#/server-control/power-operations" tabindex="10" ng-click="closeSubnav()">Server power operations</a> </li> <li ng-class="{'active': (path == '/server-control/power-usage')}"> - <a href="#/server-control/power-usage" tabindex="10" ng-click="closeSubnav()">Manage power usage</a></li> + <a href="#/server-control/power-usage" tabindex="11" ng-click="closeSubnav()">Manage power usage</a></li> <li ng-class="{'active': (path == '/server-control/server-led')}"> - <a href="#/server-control/server-led" tabindex="11" ng-click="closeSubnav()">Server LED</a></li> + <a href="#/server-control/server-led" tabindex="12" ng-click="closeSubnav()">Server LED</a></li> <li ng-class="{'active': (path == '/server-control/bmc-reboot')}"> - <a href="#/server-control/bmc-reboot" tabindex="12" ng-click="closeSubnav()">Reboot BMC</a></li> + <a href="#/server-control/bmc-reboot" tabindex="13" ng-click="closeSubnav()">Reboot BMC</a></li> <li ng-class="{'active': (path == '/server-control/remote-console')}"> - <a href="#/server-control/remote-console" tabindex="13" ng-click="closeSubnav()">Serial over LAN console</a></li> + <a href="#/server-control/remote-console" tabindex="14" ng-click="closeSubnav()">Serial over LAN console</a></li> </ul> <ul class="nav__second-level btn-firmware" ng-style="navStyle" ng-class="{opened: (showSubMenu && firstLevel == 'configuration')}"> <li ng-class="{'active': (path == '/configuration' || path == '/configuration/network')}"> - <a href="#/configuration/network" tabindex="14" ng-click="closeSubnav()">Network settings</a></li> + <a href="#/configuration/network" tabindex="15" ng-click="closeSubnav()">Network settings</a></li> <li ng-class="{'active': (path == '/configuration' || path == '/configuration/snmp')}"> - <a href="#/configuration/snmp" tabindex="15" ng-click="closeSubnav()">SNMP settings</a></li> + <a href="#/configuration/snmp" tabindex="16" ng-click="closeSubnav()">SNMP settings</a></li> <li ng-class="{'active': (path == '/configuration' || path == '/configuration/firmware')}"> - <a href="#/configuration/firmware" tabindex="16" ng-click="closeSubnav()">Firmware</a></li> + <a href="#/configuration/firmware" tabindex="17" ng-click="closeSubnav()">Firmware</a></li> <li ng-class="{'active': (path == '/configuration' || path == '/configuration/date-time')}"> - <a href="#/configuration/date-time" tabindex="17" ng-click="closeSubnav()">Date and time settings</a></li> + <a href="#/configuration/date-time" tabindex="18" ng-click="closeSubnav()">Date and time settings</a></li> </ul> <ul class="nav__second-level btn-users" ng-style="navStyle" ng-class="{opened: (showSubMenu && firstLevel == 'users')}"> <li ng-class="{'active': (path == '/users' || path == '/users/manage-accounts')}"> - <a href="#/users/manage-accounts" tabindex="18" ng-click="closeSubnav()">Manage user accounts</a></li> + <a href="#/users/manage-accounts" tabindex="19" ng-click="closeSubnav()">Manage user account</a></li> </ul> </nav> diff --git a/app/common/services/api-utils.js b/app/common/services/api-utils.js index 503fcd1..743d3fa 100644 --- a/app/common/services/api-utils.js +++ b/app/common/services/api-utils.js @@ -57,6 +57,54 @@ window.angular && (function(angular) { return ip.match( /\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/); }, + getRedfishSysName: function() { + return $http({ + method: 'GET', + url: DataService.getHost() + '/redfish/v1/Systems', + withCredentials: true + }) + .then( + function(response) { + var sysUrl = response.data['Members'][0]['@odata.id']; + return sysUrl.split('/').pop(-1); + }, + function(error) { + console.log(JSON.stringify(error)); + }); + }, + getSystemLogs: function(recordType) { + var uri = '/redfish/v1/Systems/' + DataService.systemName + + '/LogServices/EventLog/Entries'; + return $http({ + method: 'GET', + url: DataService.getHost() + uri, + withCredentials: true + }) + .then( + function(response) { + var logEntries = []; + angular.forEach(response.data['Members'], function(log) { + if (log.hasOwnProperty('EntryType')) { + if (log['EntryType'] == recordType) { + logEntries.push(log); + } + } + }); + return logEntries; + }, + function(error) { + console.log(JSON.stringify(error)); + }); + }, + clearSystemLogs: function() { + var uri = '/redfish/v1/Systems/' + DataService.systemName + + '/LogServices/EventLog/Actions/LogService.ClearLog'; + return $http({ + method: 'POST', + url: DataService.getHost() + uri, + withCredentials: true + }); + }, deleteObject: function(path) { return $http({ method: 'POST', diff --git a/app/common/services/dataService.js b/app/common/services/dataService.js index 76ab381..9017227 100644 --- a/app/common/services/dataService.js +++ b/app/common/services/dataService.js @@ -34,6 +34,7 @@ window.angular && (function(angular) { this.errorModalDetails = {}; this.ignoreHttpError = false; + this.systemName = ''; this.configJson = require('../../../config.json'); @@ -134,6 +135,10 @@ window.angular && (function(angular) { this.deactivateErrorModal = function() { this.displayErrorModal = false; }; + + this.setSystemName = function(sysName) { + this.systemName = sysName; + }; } ]); })(window.angular); diff --git a/app/index.js b/app/index.js index c9fed83..3b1089d 100644 --- a/app/index.js +++ b/app/index.js @@ -74,6 +74,7 @@ import server_health_index from './server-health/index.js'; import inventory_overview_controller from './server-health/controllers/inventory-overview-controller.js'; import log_controller from './server-health/controllers/log-controller.js'; import sensors_overview_controller from './server-health/controllers/sensors-overview-controller.js'; +import syslog_controller from './server-health/controllers/syslog-controller.js'; import redfish_index from './redfish/index.js'; import redfish_controller from './redfish/controllers/redfish-controller.js'; diff --git a/app/server-health/controllers/syslog-controller.html b/app/server-health/controllers/syslog-controller.html new file mode 100644 index 0000000..b503bd0 --- /dev/null +++ b/app/server-health/controllers/syslog-controller.html @@ -0,0 +1,100 @@ +<loader loading="loading"></loader> +<div id="sys-log"> + <section id="sys-log__events" class="table row column"> + <div class="row column"> + <h1>System Logs</h1> + <div class="page-header"> + <p class="inline h4">Select system log type: </p> + <div class="inline dropdown__wrapper" > + <button type="button" class="dropdown__button" ng-click="showLogDropdown = !showLogDropdown"><strong>{{selectedRecordType}}</strong></button> + <ul class="dropdown__list inline" ng-show="showLogDropdown"> + <li ng-repeat="recordType in recordTypeList"> + <button type="button" ng-click="selectRecordType(recordType);">{{recordType}}</button> + </li> + </ul> + </div> + <a ng-href="data:text/json;charset=utf-8,{{sysLogs}}" class="inline btn-export float-right" download="systemLogs.json" ng-show="sysLogs.length">Export </a> + <button class="inline clear-input float-right btn-secondary" ng-click="confirm = !confirm" ng-show="sysLogs.length">✕ Clear {{selectedRecordType}} Logs</button> + <div class="inline__confirm event__confirm" ng-show="confirm"> + <div class="inline__confirm-message"> + <p class="h3">Are you sure you want to <strong class="ng-binding">clear {{selectedRecordType}} logs</strong>? + </p> + </div> + <div class="inline__confirm-buttons"> + <button class="btn-primary" ng-click="clearSystemLogEntries()">Yes</button> + <button class="btn-primary" ng-click="confirm = false">No</button> + </div> + </div> + </div> + </div> + </section> + <section class="row column"> + <p class="content-label">Filter {{selectedRecordType}} Logs</p> + <div class="content__search"> + <label for="content__search-input">Search</label> + <input id="content__search-input" type="text" ng-model="customSearch" ng-keydown="doSearchOnEnter($event)"/> + <div class="search-submit__wrapper"> + <button class="clear-input" ng-click="clear()">✕</button> + <input id="content__search-submit" type="submit" class="btn btn-primary content__search-submit" value="Filter" ng-click="doSearchOnClick()"/> + </div> + </div> + </section> + <section id="sys-log__events" class="table row column" ng-show="sysLogs.length"> + <div class="table row column"> + <div class="table__head"> + <div class="table__row"> + <div class="table__cell sys-log__col-wrapper sys-log__col-id"> + <div class="column small-2 sort_button_wrapper"> + <button class="sort-ascending" ng-click="sortBy('Id', false)"></button> + <button class="sort-descending" ng-click="sortBy('Id', true)"></button> + </div> + ID + </div> + <div class="table__cell sys-log__col-wrapper"> + <div class="column small-2 sort_button_wrapper"> + <button class="sort-ascending" ng-click="sortBy('Created', false)"></button> + <button class="sort-descending" ng-click="sortBy('Created', true)"></button> + </div> + Timestamp + </div> + <div class="table__cell sys-log__col-wrapper"> + <div class="column small-2 sort_button_wrapper"> + <button class="sort-ascending" ng-click="sortBy('Name', false)"></button> + <button class="sort-descending" ng-click="sortBy('Name', true)"></button> + </div> + Name + </div> + <div class="table__cell sys-log__col-wrapper"> + <div class="column small-2 sort_button_wrapper"> + <button class="sort-ascending" ng-click="sortBy('SensorType', false)"></button> + <button class="sort-descending" ng-click="sortBy('SensorType', true)"></button> + </div> + Type + </div> + <div class="table__cell sys-log__col-wrapper sys-log__col-sev"> + <div class="column small-2 sort_button_wrapper"> + <button class="sort-ascending" ng-click="sortBy('Severity', false)"></button> + <button class="sort-descending" ng-click="sortBy('Severity', true)"></button> + </div> + Severity + </div> + <div class="table__cell sys-log__col-wrapper sys-log__col-desc">Description</div> + </div> + </div> + <div class="table__body"> + <div class="table__row" dir-paginate="log in (sysLogs | filter:filterBySearchTerms | orderBy:sortKey:reverse) | itemsPerPage:itemsPerPage"> + <div class="table__cell sys-log__col-wrapper sys-log__col-id">{{log.Id}}</div> + <div class="table__cell sys-log__col-wrapper">{{log.Created | localeDate}}</div> + <div class="table__cell sys-log__col-wrapper">{{log.Name}}</div> + <div class="table__cell sys-log__col-wrapper">{{log.SensorType}}</div> + <div class="table__cell sys-log__col-wrapper sys-log__col-sev">{{log.Severity}}</div> + <div class="table__cell sys-log__col-wrapper sys-log__col-desc">{{log.Message}}</div> + </div> + </div> + </div> + <dir-pagination-controls></dir-pagination-controls> + </section> + <section id="sys-log__events" class="table row column" ng-show="!sysLogs.length"> + <p>There are no {{selectedRecordType}} logs to display at this time.</p> + </section> +</div> <!-- end event log --> diff --git a/app/server-health/controllers/syslog-controller.js b/app/server-health/controllers/syslog-controller.js new file mode 100644 index 0000000..cec1d99 --- /dev/null +++ b/app/server-health/controllers/syslog-controller.js @@ -0,0 +1,122 @@ +/** + * Controller for log + * + * @module app/serverHealth + * @exports sysLogController + * @name sysLogController + */ + +window.angular && (function(angular) { + 'use strict'; + angular.module('app.serverHealth') + .config([ + 'paginationTemplateProvider', + function(paginationTemplateProvider) { + paginationTemplateProvider.setString( + require('../../common/directives/dirPagination.tpl.html')); + } + ]) + .controller('sysLogController', [ + '$scope', 'APIUtils', 'Constants', + function($scope, APIUtils, Constants) { + $scope.itemsPerPage = Constants.PAGINATION.LOG_ITEMS_PER_PAGE; + $scope.loading = true; + $scope.sysLogs = []; + $scope.customSearch = ''; + $scope.searchTerms = []; + $scope.sortKey = 'Id'; + $scope.showLogDropdown = false; + $scope.recordTypeList = + ['SEL', 'Event', 'Oem']; // From Redfish specification. + $scope.selectedRecordType = 'SEL'; // Default Select to SEL. + + $scope.selectRecordType = function(recordType) { + $scope.selectedRecordType = recordType; + $scope.showLogDropdown = false; + APIUtils.getSystemLogs(recordType) + .then( + function(res) { + $scope.sysLogs = res; + $scope.filterTypes.push('All'); + $scope.sysLogs.forEach(function(log) { + if ($scope.filterTypes.indexOf(log.SensorType) < 0) { + $scope.filterTypes.push(log.SensorType); + } + }); + }, + function(error) { + console.log(JSON.stringify(error)); + }) + .finally(function() { + $scope.loading = false; + }); + }; + + $scope.clearSystemLogEntries = function() { + $scope.confirm = false; + APIUtils.clearSystemLogs() + .then( + function(res) { + console.log(JSON.stringify(res)); + }, + function(error) { + console.log(JSON.stringify(error)); + }) + .finally(function() { + $scope.selectRecordType($scope.selectedRecordType); + }); + }; + + $scope.sortBy = function(keyname, isReverse) { + $scope.sortKey = keyname; + $scope.reverse = isReverse; + }; + + $scope.clear = function() { + $scope.customSearch = ''; + $scope.searchTerms = []; + }; + + $scope.doSearchOnEnter = function(event) { + var search = + $scope.customSearch.replace(/^\s+/g, '').replace(/\s+$/g, ''); + if (event.keyCode === 13 && search.length >= 2) { + $scope.searchTerms = $scope.customSearch.split(' '); + } else { + if (search.length == 0) { + $scope.searchTerms = []; + } + } + }; + + $scope.doSearchOnClick = function() { + var search = + $scope.customSearch.replace(/^\s+/g, '').replace(/\s+$/g, ''); + if (search.length >= 2) { + $scope.searchTerms = $scope.customSearch.split(' '); + } else { + if (search.length == 0) { + $scope.searchTerms = []; + } + } + }; + + $scope.filterBySearchTerms = function(log) { + if (!$scope.searchTerms.length) return true; + + for (var i = 0, length = $scope.searchTerms.length; i < length; + i++) { + // TODO: Form it while getting data + var search_text = log.Id + ' ' + log.Name.toLowerCase() + ' ' + + log.Message.toLowerCase(); + if (search_text.indexOf($scope.searchTerms[i].toLowerCase()) == + -1) + return false; + } + return true; + }; + + setTimeout($scope.selectRecordType($scope.selectedRecordType), 2000); + } + ]); +})(angular); diff --git a/app/server-health/index.js b/app/server-health/index.js index 088105b..96172d8 100644 --- a/app/server-health/index.js +++ b/app/server-health/index.js @@ -42,6 +42,11 @@ window.angular && (function(angular) { 'controller': 'sensorsOverviewController', authenticated: true }) + .when('/server-health/sys-log', { + 'template': require('./controllers/syslog-controller.html'), + 'controller': 'sysLogController', + authenticated: true + }) .when('/server-health', { 'template': require('./controllers/log-controller.html'), 'controller': 'logController', diff --git a/app/server-health/styles/index.scss b/app/server-health/styles/index.scss index 009e991..f120af0 100644 --- a/app/server-health/styles/index.scss +++ b/app/server-health/styles/index.scss @@ -1,3 +1,4 @@ @import "./inventory.scss"; @import "./log.scss"; @import "./sensors.scss"; +@import "./syslog.scss"; diff --git a/app/server-health/styles/syslog.scss b/app/server-health/styles/syslog.scss new file mode 100644 index 0000000..c225e55 --- /dev/null +++ b/app/server-health/styles/syslog.scss @@ -0,0 +1,309 @@ +// Event Log SCSS +#sys-log { + + // Dropwdowns filter + .dropdown__date.row { + padding: .5em; + } + + //Timezone select + .sys-log__timezone, + .sys-log__timezone button { + position: relative; + text-transform: uppercase; + color: $lightbg__primary; + font-size: .9em; + font-weight: 700; + border: 0; + } + .content__search { + float: none; + @include mediaQuery(x-large) { + @include fastTransition-all; + } + } +} + +.sort_button_wrapper { + padding-top:.3em; +} +.sys-log__filters { + position: relative; + padding-bottom: .5em; + padding-top: .5em; + + @media (min-width: 1333px) { + float: right; + display: inline-block; + } +} + +.accord-trigger { + position: absolute; + right: 1em; + top: .3em; +} + +#sys-log__events, +.sys-log__events { + display: block; + margin-top: 1.6em; + position: relative; + .header__actions-bar { + .btn-export, + .btn-delete, + .btn-resolve { + color: $white; + } + } + .inline__confirm { + height: auto; + margin-left: 0; + left: 0; + padding: 1em 2em 1em 2em + } + .inline__confirm-message { + margin-top: 5px; + margin-bottom: -10px; + } + .inline__confirm-buttons .btn-primary { + padding: .5em 2em; + min-height: 25px; + margin-top: .5em; + @include mediaQuery(medium) { + margin-top: 0; + } + } + .sys-log__col-wrapper { + word-wrap: break-word; + white-space: initial; + } + .col-logged-events { + span { + display: inline-block; + font-weight: 700; + margin-right: .3em; + } + } + .sys-log__col-id { + max-width: 10%; + } + .sys-log__col-sev { + max-width: 12%; + } + .sys-log__col-desc { + min-width: 30%; + max-width: 40%; + } + + .dropdown__button { + margin-bottom: 1.2em; + } + .dropdown__list { + margin-top: -17px; + } + .dropdown__button { + width: 25em; + margin-top: 0; + text-align: left; + border: 0.1em solid; + } + .dropdown__wrapper { + width: 25em; + margin-top: 0.4em; + text-align: left; + } + //Export log + .btn-export, .btn-meta-copy, + .btn-delete, + .btn-resolve { + color: black; + font-size: .9em; + font-weight: 700; + position: relative; + padding: 0 0 1em 2em; + &:hover { + text-decoration: underline; + } + } + .btn-resolve.disabled:hover, + .btn-delete.disabled:hover{ + cursor: default; + text-decoration: none; + } + .btn-export { + margin-top: 7px; + padding-bottom: 0; + } + .btn-export:before { + content: '\21E5'; + position: absolute; + font-size: 1.7em; + vertical-align: middle; + transform: rotate(90deg); + display: inline-block; + left: 2px; + top: -5px; + } + .btn-meta-copy, + .btn-delete, + .btn-resolve { + margin-left: 5px; + padding: .5em .5em; + } + + // Single event log card + .sys-log__single-event { + border: 1px solid $medgrey; + padding: 1em 1em 1em .7em; + margin: .5em 0 .5em 0; + position: relative; + overflow: hidden; + @include fastTransition-all; + &.active { + background-color: $lightgrey; + @include slowTransition-all; + } + } + .sys-log__event-info { + &:hover { + cursor: pointer; + } + } + + //Event description + .event__description { + font-weight: 400; + margin-left: 1em; + flex:35%; + @include mediaQuery(small) { + margin-left: 4.5em; + margin-right: 1em; + } + word-break: break-all; + } + + //Event ID + .event__id { + @include fontCourierBold; + font-size: .9em; + color: $darkgrey; + margin-right: 1em; + } + + .event__timestamp { + text-align: right; + @include fontCourierBold; + font-size: .9em; + color: $darkgrey; + max-width: 18em; + @media (min-width: 1105px) { + float: right; + } + } + + // Event metadata row + .event__metadata-row { + max-height: 0; + overflow: hidden; + transition: max-height .5s linear; + &.active { + max-height: 1000px; + @include mediaQuery(small) { + max-height: 1000px; + } + @include mediaQuery(medium) { + max-height: 1000px; + } + } + } + + //Event metadata + .event__metadata { + height: 200px; + overflow-y: scroll; + border: 1px solid $medgrey; + padding: .5em .5em 0; + background: $white; + display: block; + margin-bottom: 1.5em; + &::-webkit-scrollbar { + -webkit-appearance: none; + width: 7px; + } + &::-webkit-scrollbar-thumb { + border-radius: 4px; + background-color: rgba(0, 0, 0, .5); + -webkit-box-shadow: 0 0 1px rgba(255, 255, 255, .5); + } + } + + //Event Related Items + .event__related { + width: 100%; + } + .event__related-label { + font-weight: 700; + margin-right: 1.2em; + padding-top: .3em; + float: left; + } + .event__related-item { + margin-right: 1em; + padding-top: .3em; + display: inline-block; + float: left; + clear: both; + } + .event__actions { + margin-left: -1em; + margin-top: .5em; + @include mediaQuery(medium) { + float: right; + margin-top: -5px; + } + } + .event__icon { + width: 20px; + height: 20px; + font-weight: normal; + margin-right: .5em; + margin-top: -3px; + display: inline-block; + } + .sort-ascending { + display: block; + padding: 0; + transform: rotate(-90deg); + font-size: 1em; + color: lighten($darkgrey, 10%); + &:hover { + color: $darkbg__accent; + } + &:after { + content: '\276F' + } + &:focus { + outline: 0; + color: $darkbg__accent; + } + } + .sort-descending { + display: block; + padding: 0; + transform: rotate(-90deg); + font-size: 1em; + color: lighten($darkgrey, 10%); + &:hover { + color: $darkbg__accent; + } + &:after { + content: '\276e' + } + &:focus { + outline: 0; + color: $darkbg__accent; + } + } +} +//end sys-log__events |