Compare commits

..

No commits in common. "main" and "533a7993a996298b474a37f40afc74f123a591b9" have entirely different histories.

108 changed files with 171 additions and 266 deletions

3
.gitignore vendored
View File

@ -1,3 +0,0 @@
*.swp
app/static
app/dashboard/migrations/

View File

@ -1,34 +0,0 @@
### DEV ENV ###
FROM python:3.13.1-slim-bookworm AS app_dev
ARG APP_UID=1000
ARG APP_GID=1000
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
WORKDIR /app/
RUN apt update && apt install -y procps less netcat-traditional libmariadb-dev-compat libmariadb-dev mariadb-client gcc nginx-light pkg-config
COPY ./docker/nginx/updatesdashboard.conf /etc/nginx/sites-enabled/updatesdashboard.conf
RUN rm -f /etc/nginx/sites-enabled/default
RUN addgroup --system gunicorn --gid ${APP_GID} && adduser --uid ${APP_UID} --system --disabled-login --group gunicorn
RUN pip install --upgrade pip
COPY ./app/requirements.txt .
RUN pip install -r requirements.txt
COPY ./docker/scripts/entrypoint.dev.sh /usr/local/bin/entrypoint
RUN chmod +x /usr/local/bin/entrypoint
COPY ./app/ .
COPY ./app/updatesdashboard/.env.dev ./updatesdashboard/.env
RUN python /app/manage.py makemigrations
RUN python /app/manage.py makemigrations dashboard
RUN rm -f ./updatesdashboard/.env
ENTRYPOINT ["/usr/local/bin/entrypoint","mysql","3306"]

64
README.md Normal file
View File

@ -0,0 +1,64 @@
# Updates Dashboard
## Summary
This is a tool to have a clear view of the which servers are outdated, and keep trace of the updates.
## Technical information
It runs with Django. The information are daily generated by an ansible playbook, which is not located in this repo (infrastructure/updates-dashboard-ansible).
## Install
Dependencies in case of Debian 10.
```
apt install python3-venv libmariadb-dev-compat libmariadb-dev mariadb-client python3-dev gcc
```
Following procedure to install the apps.
```
useradd -d /var/www/updates-dashboard/ -g www-data -M -s /bin/false www-updash
cd /var/www/
git clone git@gitlab.infolegale.net:infrastructure/updates-dashboard.git updates-dashboard
chown -R www-updash:www-data updates-dashboard
touch /var/log/gunicorn.log
chown www-updash:www-data /var/log/gunicorn.log
cd updates-dashboard
python3 -m venv updash-venv
source updash-venv/bin/activate
(updash-venv) pip install -r requirements.txt
mkdir results results-packages
cp defaults/settings_local.py updatesdashboard/
cp defaults/gunicorn.service /etc/systemd/system/
cp defaults/updates-dashboard.conf /etc/nginx/sites-available
cd /etc/nginx/sites-enabled
ln -s /etc/nginx/sites-avaiable/updates-dashboard.conf .
```
* Set `settings_local.py` with correct values
* Set `gunicorn.service` with correct values
* Set `updates-dashboard.conf` with correct values
```shell
systemctl daemon-reload
systemctl enable gunicorn.service
nginx -t
systemctl reload nginx
```
To initialize the project:
```shell
(updash-venv) ./manage.py makemigrations
(updash-venv) ./manage.py makemigrations dashboard
(updash-venv) ./manage.py collectstatic
(updash-venv) ./manage.py migrate
(updash-venv) ./manage.py loaddata dashboard/fixtures/os.yaml
(updash-venv) ./manage.py loaddata dashboard/fixtures/teams.yaml
```
Vérifier les flux de mise à jour des données. Ansible->Dashboard
Vérifier le sql mode de la base de données
```shell
set @@global.sql_mode='NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';
```
## TODO
* playbook to install via ansible ?
* playbook to update via ansible ?
* buttons should be 'previous / next results' instead of 'previous / next day'

View File

@ -1,9 +0,0 @@
server1;Debian;8;0;5
server2;Debian;9;0;9
server3;Debian;10;0;147
server4;Debian;11;0;147
server5;Ubuntu;20.04;0;308
server6;Ubuntu;22.04;0;147
server7;OpenBSD;6.4;3;119
server8;Ubuntu;18.04;0;28
server9;Debian;12;0;147
1 server1 Debian 8 0 5
2 server2 Debian 9 0 9
3 server3 Debian 10 0 147
4 server4 Debian 11 0 147
5 server5 Ubuntu 20.04 0 308
6 server6 Ubuntu 22.04 0 147
7 server7 OpenBSD 6.4 3 119
8 server8 Ubuntu 18.04 0 28
9 server9 Debian 12 0 147

View File

@ -1,10 +0,0 @@
GUNICORN_CMD_ARGS=--bind=127.0.0.1:3001 --workers=3 --timeout=300 --error-logfile=/var/log/gunicorn-error.log
DJANGO_SUPERUSER_PASSWORD=admin
SECRET_KEY=uv88xpv8kb2r6j7rubtnhkps
DATABASE_NAME=infra_dashboard
DATABASE_USER=infra_dashboard
DATABASE_PASSWORD=sebisdown
DATABASE_HOST=mysql
DATABASE_PORT=3306
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1
DJANGO_CSRF_TRUSTED_ORIGINS=http://localhost

View File

@ -1,19 +0,0 @@
import environ
env = environ.Env()
# reading .env file
environ.Env.read_env()
ALLOWED_HOSTS = env("DJANGO_ALLOWED_HOSTS").split(" ")
CSRF_TRUSTED_ORIGINS = env("DJANGO_CSRF_TRUSTED_ORIGINS").split(" ")
SECRET_KEY = env("SECRET_KEY")
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': env("DATABASE_NAME"),
'USER': env("DATABASE_USER"),
'PASSWORD': env("DATABASE_PASSWORD"),
'HOST': env("DATABASE_HOST"),
'PORT': int(env("DATABASE_PORT")),
}
}

View File

@ -1,5 +1,5 @@
from django.contrib import admin from django.contrib import admin
from .models import Os, ServerStatus, Server, PackageStatus from .models import Os, ServerStatus, Server, PackageStatus, Team
class OsAdmin(admin.ModelAdmin): class OsAdmin(admin.ModelAdmin):
@ -10,3 +10,4 @@ admin.site.register(Os, OsAdmin)
admin.site.register(ServerStatus) admin.site.register(ServerStatus)
admin.site.register(PackageStatus) admin.site.register(PackageStatus)
admin.site.register(Server) admin.site.register(Server)
admin.site.register(Team)

View File

@ -72,13 +72,6 @@
version: '11', version: '11',
end_of_support: 2026-06-30 end_of_support: 2026-06-30
} }
- model: dashboard.os
pk: null
fields: {
distribution: Debian,
version: '12',
end_of_support: 2028-06-30
}
# Ubuntu Server # Ubuntu Server
- model: dashboard.os - model: dashboard.os
@ -214,13 +207,6 @@
version: '22.04', version: '22.04',
end_of_support: 2027-04-01 end_of_support: 2027-04-01
} }
- model: dashboard.os
pk: null
fields: {
distribution: Ubuntu,
version: '24.04',
end_of_support: 2029-04-01
}
# CentOS # CentOS
- model: dashboard.os - model: dashboard.os

View File

@ -0,0 +1,8 @@
# This is initial data for prod teams
- model: dashboard.team
pk: null
fields: {
name: System,
color: pink
}

View File

@ -32,9 +32,32 @@ class Os(models.Model):
class Group(models.Model):
name = models.CharField(max_length=50, unique=True)
full_name = models.CharField(max_length=50, null=True)
def __str__(self):
if self.full_name:
return self.full_name
else:
return self.name
class Team(models.Model):
name = models.CharField(max_length=20)
color = models.CharField(max_length=20, unique=True)
def __str__(self):
return self.name
class Server(models.Model): class Server(models.Model):
hostname = models.CharField(max_length=200, unique=True) hostname = models.CharField(max_length=200, unique=True)
os = models.ForeignKey(Os, null=True, related_name="servers", on_delete=models.SET_NULL) os = models.ForeignKey(Os, null=True, related_name="servers", on_delete=models.SET_NULL)
group = models.ForeignKey(Group, null=True, blank=True, related_name="groups", on_delete=models.SET_NULL)
team = models.ForeignKey(Team, null=True, related_name="teams", on_delete=models.SET_NULL)
def __str__(self): def __str__(self):
return self.hostname return self.hostname

View File

Before

Width:  |  Height:  |  Size: 280 KiB

After

Width:  |  Height:  |  Size: 280 KiB

View File

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

View File

Before

Width:  |  Height:  |  Size: 170 KiB

After

Width:  |  Height:  |  Size: 170 KiB

View File

@ -20,7 +20,7 @@
<table id="os-list" class="table table-bordered table-hover table-striped"> <table id="os-list" class="table table-bordered table-hover table-striped">
<thead> <thead>
<tr> <tr>
<th>Distribution</th> <th>Hostname</th>
<th>Number</th> <th>Number</th>
<th>Percentage</th> <th>Percentage</th>
</tr> </tr>

View File

@ -26,7 +26,7 @@
<!-- Navigation --> <!-- Navigation -->
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation"> <nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="navbar-header"> <div class="navbar-header">
<a class="navbar-brand" href="{% url 'index' %}">Hyrule - Servers information</a> <a class="navbar-brand" href="{% url 'index' %}">Infolegale - Servers information</a>
</div> </div>
<div class="nav navbar-right top-nav"> <div class="nav navbar-right top-nav">
<!-- <button class="btn btn&#45;lg btn&#45;danger disabled">Confidential information</button> --> <!-- <button class="btn btn&#45;lg btn&#45;danger disabled">Confidential information</button> -->

View File

@ -31,7 +31,7 @@
<!-- Navigation --> <!-- Navigation -->
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation"> <nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="navbar-header"> <div class="navbar-header">
<a class="navbar-brand" href="{% url 'index' %}">Hyrule - Servers Informations</a> <a class="navbar-brand" href="{% url 'index' %}">Infolegale - Servers Informations</a>
</div> </div>
<div class="nav navbar-right top-nav"> <div class="nav navbar-right top-nav">
<!-- <button class="btn btn&#45;lg btn&#45;danger disabled">Confidential information</button> --> <!-- <button class="btn btn&#45;lg btn&#45;danger disabled">Confidential information</button> -->

View File

@ -8,7 +8,7 @@ from . import views
urlpatterns = [ urlpatterns = [
# home # home
re_path(r'^$', re_path(r'^/?$',
views.index, views.index,
name='index'), name='index'),
@ -19,6 +19,12 @@ urlpatterns = [
re_path(r'^server-list/(?P<year>[0-9]{4})/(?P<month>[0-9]{1,2})/(?P<day>[0-9]{1,2})/?$', re_path(r'^server-list/(?P<year>[0-9]{4})/(?P<month>[0-9]{1,2})/(?P<day>[0-9]{1,2})/?$',
views.server_list, views.server_list,
name='server-list-by-date'), name='server-list-by-date'),
re_path(r'^server-list/(?P<group>[a-z0-9\-_]*)/(?P<year>[0-9]{4})/(?P<month>[0-9]{1,2})/(?P<day>[0-9]{1,2})/?$',
views.server_list,
name='server-list-by-group'),
re_path(r'^server-list/team/(?P<team>[a-z]*)/(?P<year>[0-9]{4})/(?P<month>[0-9]{1,2})/(?P<day>[0-9]{1,2})/?$',
views.server_list,
name='server-list-by-team'),
# package list # package list
re_path(r'^packages/?$', re_path(r'^packages/?$',
@ -60,9 +66,9 @@ urlpatterns = [
re_path(r'^manage/upload_csv_results', re_path(r'^manage/upload_csv_results',
views.upload_csv_results, views.upload_csv_results,
name='upload_csv_results'), name='upload_csv_results'),
re_path(r'^manage-packages/upload_csv_results_packages', # re_path(r'^manage/update-groups/?$',
views.upload_csv_results_packages, # views.update_groups,
name='upload_csv_results_packages'), # name='update_groups'),
# manage packages # manage packages
re_path(r'^manage-packages/?$', re_path(r'^manage-packages/?$',

View File

@ -16,7 +16,7 @@ from django.core.cache import cache
from django.db import IntegrityError from django.db import IntegrityError
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
# project # project
from .models import Os, Server, ServerStatus, PackageStatus, Document_Servers, Document_Packages from .models import Os, Group, Server, ServerStatus, PackageStatus, Team, Document_Servers, Document_Packages
from .forms import DocumentForm from .forms import DocumentForm
@ -36,7 +36,7 @@ def index(request):
## ----------------------------------------------------------------------------- ## -----------------------------------------------------------------------------
@login_required @login_required
def server_list(request, year=None, month=None, day=None): def server_list(request, year=None, month=None, day=None, group=None, team=None):
# TODO: use date.today? # TODO: use date.today?
now = datetime.now() now = datetime.now()
if not year or not month or not day: if not year or not month or not day:
@ -64,10 +64,23 @@ def server_list(request, year=None, month=None, day=None):
next_result_date = next_result.date next_result_date = next_result.date
results_date = current_date results_date = current_date
# status_list = ServerStatus.objects.filter(date=current_date).order_by('server__hostname') if group:
status_list = ServerStatus.objects.filter(date=current_date).order_by('server__hostname').select_related('server', 'server__os') group = get_object_or_404(Group, name=group)
status_list = ServerStatus.objects.filter(date=current_date, server__group=group).order_by('server__hostname')
if previous_result and not status_list: if previous_result and not status_list:
status_list = ServerStatus.objects.filter(date=previous_result_date).order_by('server__hostname').select_related('server', 'server__os') status_list = ServerStatus.objects.filter(date=previous_result_date, server__group=group).order_by('server__hostname')
results_date = previous_result_date
elif team:
team = get_object_or_404(Team, color=team)
status_list = ServerStatus.objects.filter(date=current_date, server__team=team).order_by('server__hostname')
if previous_result and not status_list:
status_list = ServerStatus.objects.filter(date=previous_result_date, server__team=team).order_by('server__hostname')
results_date = previous_result_date
else:
# status_list = ServerStatus.objects.filter(date=current_date).order_by('server__hostname')
status_list = ServerStatus.objects.filter(date=current_date).order_by('server__hostname').select_related('server', 'server__group', 'server__os')
if previous_result and not status_list:
status_list = ServerStatus.objects.filter(date=previous_result_date).order_by('server__hostname').select_related('server', 'server__group', 'server__os')
results_date = previous_result_date results_date = previous_result_date
if not status_list: if not status_list:
@ -77,6 +90,7 @@ def server_list(request, year=None, month=None, day=None):
return render(request, 'server-list.html', return render(request, 'server-list.html',
{ {
'group': group,
'status_list': status_list, 'status_list': status_list,
'results_date': results_date, 'results_date': results_date,
'previous_result_date': previous_result_date, 'previous_result_date': previous_result_date,
@ -318,6 +332,7 @@ def purge_all(request):
ServerStatus.objects.all().delete() ServerStatus.objects.all().delete()
Os.objects.all().delete() Os.objects.all().delete()
PackageStatus.objects.all().delete() PackageStatus.objects.all().delete()
Group.objects.all().delete()
content = "<div class='alert alert-danger' role='alert'>Everything has been purged.</div>" content = "<div class='alert alert-danger' role='alert'>Everything has been purged.</div>"
@ -575,7 +590,7 @@ def manage_packages(request):
## ----------------------------------------------------------------------------- ## -----------------------------------------------------------------------------
## UPLOAD FILE ## UPLOAD FILE
## Upload csv file for servers informations ## Upload csv file
## ----------------------------------------------------------------------------- ## -----------------------------------------------------------------------------
@login_required @login_required
@ -601,32 +616,3 @@ def upload_csv_results(request):
# Render list page with the documents and the form # Render list page with the documents and the form
context = {'documents': documents, 'form': form, 'message': message} context = {'documents': documents, 'form': form, 'message': message}
return render(request, 'manage.html', context) return render(request, 'manage.html', context)
## -----------------------------------------------------------------------------
## UPLOAD FILE
## Upload csv file for packages informations
## -----------------------------------------------------------------------------
@login_required
def upload_csv_results_packages(request):
message = 'File must be name YYYY-MM-DD.csv'
# Handle file upload
if request.method == 'POST':
form = DocumentForm(request.POST, request.FILES)
if form.is_valid():
newdoc = Document_Packages(docfile=request.FILES['docfile'])
newdoc.save()
# Redirect to the document list after POST
return redirect('manage-packages')
else:
message = 'The form is not valid. Fix the following error:'
else:
form = DocumentForm() # An empty, unbound form
# Load documents for the list page
documents = Document_Packages.objects.all()
# Render list page with the documents and the form
context = {'documents': documents, 'form': form, 'message': message}
return render(request, 'manage-packages.html', context)

View File

@ -37,9 +37,37 @@ INSTALLED_APPS_LOCAL = [
# LDAP AUTH # LDAP AUTH
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS = (
'django_python3_ldap.auth.LDAPBackend',
'django.contrib.auth.backends.ModelBackend', 'django.contrib.auth.backends.ModelBackend',
) )
LDAP_AUTH_URL = "ldaps://SERVER:PORT"
LDAP_AUTH_USE_TLS = True
LDAP_AUTH_SEARCH_BASE = "ou=USERS,dc=MY,dc=ORG"
LDAP_AUTH_OBJECT_CLASS = "inetOrgPerson"
LDAP_AUTH_USER_FIELDS = {
"username": "uid",
"first_name": "givenName",
"last_name": "sn",
"email": "mail",
}
LDAP_AUTH_FORMAT_SEARCH_FILTERS = "dashboard.module.custom_format_search_filters"
LDAP_AUTH_CUSTOM_OBJECT_CLASS = "ACLASS"
LDAP_AUTH_CUSTOM_FILTERS = "(SOMEFIELD=SOMEVALUE)"
MIDDLEWARE_LOCAL = [ MIDDLEWARE_LOCAL = [
'debug_toolbar.middleware.DebugToolbarMiddleware', 'debug_toolbar.middleware.DebugToolbarMiddleware',
] ]
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, './static')
# custom
RESULT_DIR = os.path.join(BASE_DIR, 'results')
RESULT_PACKAGES_DIR = os.path.join(BASE_DIR, 'results-packages')
INVENTORY_DIR = os.path.join(BASE_DIR, 'inventory')
LOGIN_URL = '/login'
LOGIN_REDIRECT_URL = 'index'

View File

@ -1,19 +0,0 @@
services:
web:
container_name: updatesdashboard.front
build:
context: ./
target: app_dev
ports:
- "3000:3000"
volumes:
- ./app/:/app/
env_file:
- ./app/updatesdashboard/.env.dev
networks:
- infra-dashboard
networks:
infra-dashboard:
name: infra-dashboard_infra-dashboard
external: true

View File

@ -1,25 +0,0 @@
server {
listen 3001;
server_name localhost;
# css, js…
location /static {
alias /app/static;
}
if ($request_uri !~ "^/admin.*") {
# don't rewrite admin
rewrite ^/(.*)/$ /$1 permanent;
}
location / {
include proxy_params;
proxy_pass http://127.0.0.1:3000;
proxy_connect_timeout 120s;
proxy_read_timeout 300s;
}
error_log /var/log/updatesdashboard-error.log;
access_log /var/log/updatesdashboard-access.log;
}

View File

@ -1,25 +0,0 @@
server {
listen 3000;
server_name updates-dashboard.hyrule.ovh;
# css, js…
location /static {
alias /app/static;
}
if ($request_uri !~ "^/admin.*") {
# don't rewrite admin
rewrite ^/(.*)/$ /$1 permanent;
}
location / {
include proxy_params;
proxy_pass http://127.0.0.1:3001;
proxy_connect_timeout 120s;
proxy_read_timeout 300s;
}
error_log /var/log/updatesdashboard-error.log;
access_log /var/log/updatesdashboard-access.log;
}

View File

@ -1,11 +0,0 @@
#!/bin/sh
until nc -vz $1 $2; do echo "Waiting for MySQL $1:$2..."; sleep 3; done;
python /app/manage.py migrate
python /app/manage.py collectstatic --clear --no-input
python /app/manage.py loaddata /app/dashboard/fixtures/os.yaml
python /app/manage.py createsuperuser --noinput --username admin --email test@example.com
service nginx start
gunicorn updatesdashboard.wsgi:application
exec "$@"

View File

@ -1,11 +0,0 @@
#!/bin/sh
until nc -vz $1 $2; do echo "Waiting for MySQL $1:$2..."; sleep 3; done;
python /app/manage.py migrate
python /app/manage.py collectstatic
python /app/manage.py loaddata /app/dashboard/fixtures/os.yaml
python /app/manage.py createsuperuser --noinput --username admin --email test@example.com
service nginx start
gunicorn updatesdashboard.wsgi:application
exec "$@"

Some files were not shown because too many files have changed in this diff Show More