statistics and navigation on landing page
This commit is contained in:
parent
5eed3bed89
commit
162624672c
@ -1,7 +1,25 @@
|
||||
import configparser
|
||||
|
||||
from platform import uname
|
||||
from time import sleep
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
import psutil
|
||||
|
||||
|
||||
def sizeof_fmt(num, suffix='B'):
|
||||
for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']:
|
||||
if abs(num) < 1024.0:
|
||||
return "%3.1f%s%s" % (num, unit, suffix)
|
||||
num /= 1024.0
|
||||
return "%.1f%s%s" % (num, 'Yi', suffix)
|
||||
|
||||
|
||||
class IniParser(configparser.ConfigParser):
|
||||
"""
|
||||
config output as json
|
||||
"""
|
||||
|
||||
def as_dict(self):
|
||||
d = dict(self._sections)
|
||||
@ -9,3 +27,102 @@ class IniParser(configparser.ConfigParser):
|
||||
d[k] = dict(self._defaults, **d[k])
|
||||
d[k].pop('__name__', None)
|
||||
return d
|
||||
|
||||
|
||||
class SystemStats:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def all(self):
|
||||
return {
|
||||
**self.system(),
|
||||
**self.cpu(), **self.ram(), **self.swap(),
|
||||
**self.disk(), **self.net(), **self.net_speed()
|
||||
}
|
||||
|
||||
def system(self):
|
||||
return {
|
||||
'system': uname().system,
|
||||
'node': uname().node,
|
||||
'machine': uname().machine
|
||||
}
|
||||
|
||||
def cpu(self):
|
||||
return {
|
||||
'cpu_usage': psutil.cpu_percent(interval=1),
|
||||
'cpu_load': list(psutil.getloadavg())
|
||||
}
|
||||
|
||||
def ram(self):
|
||||
mem = psutil.virtual_memory()
|
||||
return {
|
||||
'ram_total': [mem.total, sizeof_fmt(mem.total)],
|
||||
'ram_used': [mem.used, sizeof_fmt(mem.used)],
|
||||
'ram_free': [mem.free, sizeof_fmt(mem.free)],
|
||||
'ram_cached': [mem.cached, sizeof_fmt(mem.cached)]
|
||||
}
|
||||
|
||||
def swap(self):
|
||||
swap = psutil.swap_memory()
|
||||
return {
|
||||
'swap_total': [swap.total, sizeof_fmt(swap.total)],
|
||||
'swap_used': [swap.used, sizeof_fmt(swap.used)],
|
||||
'swap_free': [swap.free, sizeof_fmt(swap.free)]
|
||||
}
|
||||
|
||||
def disk(self):
|
||||
root = psutil.disk_usage('/')
|
||||
return {
|
||||
'disk_total': [root.total, sizeof_fmt(root.total)],
|
||||
'disk_used': [root.used, sizeof_fmt(root.used)],
|
||||
'disk_free': [root.free, sizeof_fmt(root.free)]
|
||||
}
|
||||
|
||||
def net(self):
|
||||
net = psutil.net_io_counters()
|
||||
return {
|
||||
'net_send': [net.bytes_sent, sizeof_fmt(net.bytes_sent)],
|
||||
'net_recv': [net.bytes_recv, sizeof_fmt(net.bytes_recv)],
|
||||
'net_errin': net.errin,
|
||||
'net_errout': net.errout
|
||||
}
|
||||
|
||||
def net_speed(self):
|
||||
net = psutil.net_if_stats()
|
||||
|
||||
if settings.NET_INTERFACE not in net:
|
||||
return {
|
||||
'net_speed_send': 'no network interface set!',
|
||||
'net_speed_recv': 'no network interface set!'
|
||||
}
|
||||
|
||||
net = psutil.net_io_counters(pernic=True)[settings.NET_INTERFACE]
|
||||
|
||||
send_start = net.bytes_sent
|
||||
recv_start = net.bytes_recv
|
||||
|
||||
sleep(1)
|
||||
|
||||
net = psutil.net_io_counters(pernic=True)[settings.NET_INTERFACE]
|
||||
|
||||
send_end = net.bytes_sent
|
||||
recv_end = net.bytes_recv
|
||||
|
||||
send_sec = send_end - send_start
|
||||
recv_sec = recv_end - recv_start
|
||||
|
||||
return {
|
||||
'net_speed_send': [send_sec, sizeof_fmt(send_sec)],
|
||||
'net_speed_recv': [recv_sec, sizeof_fmt(recv_sec)]
|
||||
}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('ALL: ', SystemStats().all())
|
||||
exit()
|
||||
print('CPU: ', SystemStats.cpu())
|
||||
print('RAM: ', SystemStats.ram())
|
||||
print('SWAP: ', SystemStats.swap())
|
||||
print('DISK: ', SystemStats.disk())
|
||||
print('NET: ', SystemStats.net())
|
||||
print('SPEED: ', SystemStats.net_speed())
|
||||
|
@ -4,7 +4,7 @@ from django.conf import settings
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
|
||||
from .utils import IniParser
|
||||
from .utils import IniParser, SystemStats
|
||||
|
||||
|
||||
class Config(APIView):
|
||||
@ -26,3 +26,17 @@ class Config(APIView):
|
||||
"error": "ffpayout engine config file not found!"})
|
||||
else:
|
||||
return Response({"success": False})
|
||||
|
||||
|
||||
class Statistics(APIView):
|
||||
"""
|
||||
get system statistics: cpu, ram, etc.
|
||||
for reading, endpoint is: http://127.0.0.1:8000/api/stats/?stats=all
|
||||
"""
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if 'stats' in request.GET.dict() \
|
||||
and request.GET.dict()['stats'] == 'all':
|
||||
return Response(SystemStats().all())
|
||||
else:
|
||||
return Response({"success": False})
|
||||
|
@ -154,3 +154,6 @@ STATIC_URL = '/static/'
|
||||
|
||||
# path to ffplayout engine config
|
||||
FFPLAYOUT_CONFIG = '/etc/ffplayout/ffplayout.conf'
|
||||
|
||||
# used network interface for statistics: eth0, eno1, etc.
|
||||
NET_INTERFACE = 'br0'
|
||||
|
@ -30,6 +30,7 @@ urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('api/', include(router.urls)),
|
||||
path('api/config/', views.Config.as_view()),
|
||||
path('api/stats/', views.Statistics.as_view()),
|
||||
path('api-auth/', include(
|
||||
'rest_framework.urls', namespace='rest_framework')),
|
||||
path('auth/token/', TokenObtainPairView.as_view(),
|
||||
|
@ -8,7 +8,7 @@
|
||||
html, body {
|
||||
font-family: 'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
||||
Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
font-size: 15px;
|
||||
word-spacing: 1px;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
|
@ -1,33 +1,148 @@
|
||||
<template>
|
||||
<b-container class="login-container">
|
||||
<div>
|
||||
<div class="header">
|
||||
<h1>ffplayout</h1>
|
||||
</div>
|
||||
|
||||
<div v-if="!$store.state.auth.isLogin">
|
||||
<b-form @submit.prevent="login" class="login-form">
|
||||
<p v-if="formError" class="error">
|
||||
{{ formError }}
|
||||
</p>
|
||||
<b-form-group id="input-group-1" label="User:" label-for="input-user">
|
||||
<b-form-input id="input-user" v-model="formUsername" type="text" required placeholder="Username" />
|
||||
</b-form-group>
|
||||
<b-form-group id="input-group-1" label="Password:" label-for="input-pass">
|
||||
<b-form-input id="input-pass" v-model="formPassword" type="password" required placeholder="Password" />
|
||||
</b-form-group>
|
||||
<b-button type="submit" variant="primary">
|
||||
Login
|
||||
</b-button>
|
||||
</b-form>
|
||||
</div>
|
||||
<div v-else>
|
||||
<br>
|
||||
<br>
|
||||
<h3>Wellcome to ffplayout manager!</h3>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="!$store.state.auth.isLogin">
|
||||
<div class="logout-div" />
|
||||
<b-container class="login-container">
|
||||
<div>
|
||||
<div class="header">
|
||||
<h1>ffplayout</h1>
|
||||
</div>
|
||||
<b-form @submit.prevent="login" class="login-form">
|
||||
<p v-if="formError" class="error">
|
||||
{{ formError }}
|
||||
</p>
|
||||
<b-form-group id="input-group-1" label="User:" label-for="input-user">
|
||||
<b-form-input id="input-user" v-model="formUsername" type="text" required placeholder="Username" />
|
||||
</b-form-group>
|
||||
<b-form-group id="input-group-1" label="Password:" label-for="input-pass">
|
||||
<b-form-input id="input-pass" v-model="formPassword" type="password" required placeholder="Password" />
|
||||
</b-form-group>
|
||||
<b-button type="submit" variant="primary">
|
||||
Login
|
||||
</b-button>
|
||||
</b-form>
|
||||
</div>
|
||||
</b-container>
|
||||
</div>
|
||||
</b-container>
|
||||
<div v-else>
|
||||
<b-container class="login-container">
|
||||
<div>
|
||||
<b-row cols="3">
|
||||
<b-col cols="4" class="chart-col chart1">
|
||||
<br>
|
||||
<div class="stat-div">
|
||||
<div class="stat-center" style="text-align: left;">
|
||||
<h1>ffplayout</h1>
|
||||
<h3 v-if="stat.system">
|
||||
{{ stat.system }}<br>
|
||||
{{ stat.node }}<br>
|
||||
{{ stat.machine }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</b-col>
|
||||
<b-col cols="4" class="chart-col chart2">
|
||||
<div v-if="stat.cpu_usage">
|
||||
<div>
|
||||
<strong>CPU</strong>
|
||||
</div>
|
||||
<div class="stat-div">
|
||||
<div class="stat-center">
|
||||
<b-progress :value="stat.cpu_usage" max="100" variant="success" height="1rem" />
|
||||
<br>
|
||||
<div style="text-align: left;">
|
||||
<strong>Usage: </strong>{{ stat.cpu_usage }}%<br>
|
||||
<strong>Load: </strong> {{ stat.cpu_load[0] }} {{ stat.cpu_load[1] }} {{ stat.cpu_load[2] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</b-col>
|
||||
<b-col cols="4" class="chart-col chart3">
|
||||
<div v-if="stat.ram_total">
|
||||
<div>
|
||||
<strong>RAM</strong>
|
||||
</div>
|
||||
<div class="stat-div">
|
||||
<div class="stat-center">
|
||||
<div style="text-align: left;">
|
||||
<strong>Total: </strong> {{ stat.ram_total[1] }}<br>
|
||||
<strong>Used: </strong> {{ stat.ram_used[1] }}<br>
|
||||
<strong>Free: </strong> {{ stat.ram_free[1] }}<br>
|
||||
<strong>Cached: </strong> {{ stat.ram_cached[1] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</b-col>
|
||||
<b-col cols="4" class="chart-col chart4">
|
||||
<div v-if="stat.swap_total">
|
||||
<div>
|
||||
<strong>SWAP</strong>
|
||||
</div>
|
||||
<div class="stat-div">
|
||||
<div class="stat-center">
|
||||
<div style="text-align: left;">
|
||||
<strong>Total: </strong> {{ stat.swap_total[1] }}<br>
|
||||
<strong>Used: </strong> {{ stat.swap_used[1] }}<br>
|
||||
<strong>Free: </strong> {{ stat.swap_free[1] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</b-col>
|
||||
<b-col cols="4" class="chart-col chart5">
|
||||
<div v-if="stat.disk_total">
|
||||
<div>
|
||||
<strong>DISK</strong>
|
||||
</div>
|
||||
<div class="stat-div">
|
||||
<div class="stat-center">
|
||||
<div style="text-align: left;">
|
||||
<strong>Total: </strong> {{ stat.disk_total[1] }}<br>
|
||||
<strong>Used: </strong> {{ stat.disk_used[1] }}<br>
|
||||
<strong>Free: </strong> {{ stat.disk_free[1] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</b-col>
|
||||
<b-col cols="4" class="chart-col chart6">
|
||||
<div v-if="stat.net_send">
|
||||
<div>
|
||||
<strong>NET</strong>
|
||||
</div>
|
||||
<div class="stat-div">
|
||||
<div class="stat-center">
|
||||
<div style="text-align: left;">
|
||||
<strong>Download: </strong> {{ stat.net_speed_recv[1] }}/s<br>
|
||||
<strong>Upload: </strong> {{ stat.net_speed_send[1] }}/s<br>
|
||||
<strong>Downloaded: </strong> {{ stat.net_recv[1] }}<br>
|
||||
<strong>Uploaded: </strong> {{ stat.net_send[1] }}<br>
|
||||
<strong>Recived Errors: </strong> {{ stat.net_errin }}<br>
|
||||
<strong>Sended Errors: </strong> {{ stat.net_errout }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<div class="actions">
|
||||
<b-button-group class="actions-grp">
|
||||
<b-button>Control</b-button>
|
||||
<b-button>Media</b-button>
|
||||
<b-button>Logging</b-button>
|
||||
<b-button>Configure</b-button>
|
||||
<b-button @click="logout()">
|
||||
Logout
|
||||
</b-button>
|
||||
</b-button-group>
|
||||
</div>
|
||||
</div>
|
||||
</b-container>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@ -38,12 +153,19 @@ export default {
|
||||
return {
|
||||
formError: null,
|
||||
formUsername: '',
|
||||
formPassword: ''
|
||||
formPassword: '',
|
||||
interval: null,
|
||||
stat: {}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.init()
|
||||
},
|
||||
beforeDestroy () {
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async init () {
|
||||
await this.$store.dispatch('auth/inspectToken')
|
||||
@ -75,7 +197,16 @@ export default {
|
||||
checkLogin () {
|
||||
if (this.$store.state.auth.isLogin) {
|
||||
// this.$router.push('/player')
|
||||
this.sysStats()
|
||||
}
|
||||
},
|
||||
|
||||
sysStats () {
|
||||
this.interval = setInterval(async () => {
|
||||
await this.$store.dispatch('auth/inspectToken')
|
||||
const response = await this.$axios.get('api/stats/?stats=all', { headers: { Authorization: 'Bearer ' + this.$store.state.auth.jwtToken }, progress: false })
|
||||
this.stat = response.data
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -97,4 +228,62 @@ export default {
|
||||
.login-form {
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.manage-btn {
|
||||
margin: 0 auto 0 auto;
|
||||
}
|
||||
|
||||
.chart-col {
|
||||
text-align: center;
|
||||
min-width: 10em;
|
||||
min-height: 15em;
|
||||
border: solid #c3c3c3;
|
||||
}
|
||||
|
||||
.stat-div {
|
||||
padding-top: .5em;
|
||||
position: relative;
|
||||
height: 12em;
|
||||
}
|
||||
|
||||
.stat-center {
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 50%;
|
||||
-ms-transform: translateY(-50%);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.chart1 {
|
||||
background: rgba(210, 85, 23, 0.1);
|
||||
}
|
||||
.chart2 {
|
||||
background: rgba(122, 210, 23, 0.1);
|
||||
}
|
||||
.chart3 {
|
||||
background: rgba(23, 210, 149, 0.1);
|
||||
}
|
||||
.chart4 {
|
||||
background: rgba(23, 160, 210, 0.1);
|
||||
}
|
||||
.chart5 {
|
||||
background: rgba(122, 23, 210, 0.1);
|
||||
}
|
||||
.chart6 {
|
||||
background: rgba(210, 23, 74, 0.1);
|
||||
}
|
||||
|
||||
.actions {
|
||||
text-align: center;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
@media (max-width: 380px) {
|
||||
.actions-grp {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 2em 0 2em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -37,7 +37,6 @@ export const actions = {
|
||||
}
|
||||
await this.$axios.post('auth/token/', payload)
|
||||
.then((response) => {
|
||||
console.log('obtainToken: ', response)
|
||||
commit('UPADTE_TOKEN', { token: response.data.access, refresh: response.data.refresh })
|
||||
commit('UPDATE_IS_LOGIN', true)
|
||||
})
|
||||
@ -47,11 +46,11 @@ export const actions = {
|
||||
},
|
||||
async refreshToken ({ commit, state }) {
|
||||
const payload = {
|
||||
refresh: state.jwtRefresh
|
||||
refresh: state.jwtRefresh,
|
||||
progress: false
|
||||
}
|
||||
const response = await this.$axios.post('auth/token/refresh/', payload)
|
||||
|
||||
console.log('refreshToken: ', response)
|
||||
commit('UPADTE_TOKEN', { token: response.data.access })
|
||||
commit('UPDATE_IS_LOGIN', true)
|
||||
},
|
||||
@ -68,18 +67,14 @@ export const actions = {
|
||||
if (expire_token - timestamp > 0) {
|
||||
// DO NOTHING, DO NOT REFRESH
|
||||
commit('UPDATE_IS_LOGIN', true)
|
||||
console.log('token is valid, for: ' + Math.floor(expire_token - timestamp) + ' seconds')
|
||||
} else if (expire_refresh - timestamp > 0) {
|
||||
await dispatch('refreshToken')
|
||||
console.log('update token')
|
||||
} else {
|
||||
// PROMPT USER TO RE-LOGIN, THIS ELSE CLAUSE COVERS THE CONDITION WHERE A TOKEN IS EXPIRED AS WELL
|
||||
commit('UPDATE_IS_LOGIN', false)
|
||||
console.log('new login')
|
||||
}
|
||||
} else {
|
||||
commit('UPDATE_IS_LOGIN', false)
|
||||
console.log('new login')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user