Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[C / Docker] 412 websocket #519

Draft
wants to merge 119 commits into
base: development
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
119 commits
Select commit Hold shift + click to select a range
9a3cb69
merge multiple experiments
felix-20 Apr 11, 2022
44a03d6
first attempt for websocket
felix-20 Apr 20, 2022
bb09c62
websocket on docker site working
felix-20 Apr 21, 2022
6bcf3e7
just send all of it if things have changed
felix-20 Apr 21, 2022
b743f07
started on webserver site
felix-20 Apr 21, 2022
467cabb
Merge branch 'development' into 412-push-notification
felix-20 Apr 28, 2022
d69b18c
webserver sends push notification to user about stopped container
felix-20 Apr 29, 2022
1a9cb1a
merge development
felix-20 May 2, 2022
b3c949f
commit
felix-20 May 2, 2022
729b241
Merge branch 'development' into 412-push-notification
felix-20 May 2, 2022
031bd27
Merge branch 'development' into 412-push-notification
felix-20 May 4, 2022
4870f7d
websocket to ssl
felix-20 May 4, 2022
70ff2f0
fix
felix-20 May 5, 2022
b0d635c
try
felix-20 May 5, 2022
1d44bec
started on database manager
felix-20 May 6, 2022
793ac2f
more db
felix-20 May 7, 2022
ae0ba13
fix?
felix-20 May 7, 2022
3a66b6a
debug statements
felix-20 May 7, 2022
9d90cb6
more types
felix-20 May 7, 2022
b4e4af4
does table exist fix
felix-20 May 7, 2022
ebb3ef9
colorful terminal output
felix-20 May 7, 2022
b6e92a8
commit
felix-20 May 7, 2022
18668b5
debug
felix-20 May 7, 2022
ff25213
extra health checker
felix-20 May 8, 2022
8e90287
debug for force stop
felix-20 May 8, 2022
ee15b4a
some logging
felix-20 May 8, 2022
c2a83db
Merge branch 'development' into 412-push-notification
felix-20 May 8, 2022
df787ab
fix tests
felix-20 May 8, 2022
63058a0
telegram notifications
felix-20 May 9, 2022
c6340af
better logging
felix-20 May 9, 2022
eecfb9c
better logging
felix-20 May 9, 2022
f87ee93
fix test?
felix-20 May 9, 2022
406e22a
different logging level
felix-20 May 9, 2022
1077a9c
small fixes
felix-20 May 9, 2022
f5ec9f6
Merge branch 'development' into 412-push-notification
felix-20 May 12, 2022
4d50a8b
started on system monitoring
felix-20 May 15, 2022
c8b82c5
system monitoring
felix-20 May 16, 2022
c00513f
csv possibility for system data
felix-20 May 16, 2022
df06b6f
Merge branch 'development' into 412-push-notification
NikkelM May 16, 2022
10a60bd
two buttons in webserver
felix-20 May 16, 2022
f677c8f
more fail prove
felix-20 May 16, 2022
aa9643f
Merge branch 'development' into 412-push-notification
felix-20 May 17, 2022
90908a0
Merge branch 'development' into 412-push-notification
felix-20 May 18, 2022
1948b60
fix configuration form
felix-20 May 18, 2022
124224c
silent_starter
felix-20 May 19, 2022
295b8dc
logging statements for silent starter
felix-20 May 19, 2022
845a974
Merge branch 'development' into 412-push-notification
felix-20 May 19, 2022
c3d0227
debug
felix-20 May 19, 2022
a00b486
debug
felix-20 May 19, 2022
884fd79
more debugging
felix-20 May 19, 2022
a37f384
fix error
felix-20 May 19, 2022
f22258a
more debugging
felix-20 May 19, 2022
c276105
Merge branch 'development' into 412-push-notification
felix-20 May 19, 2022
1fac0a6
Merge branch 'development' into 412-push-notification
felix-20 May 20, 2022
9ee85b1
more local monitoring
felix-20 May 20, 2022
cf6a170
adjusted logging in silent starter
felix-20 May 21, 2022
57dad1f
gpu
felix-20 May 24, 2022
57be822
os.system
felix-20 May 24, 2022
757e4be
subprocess
felix-20 May 24, 2022
01c2c60
Merge branch 'development' into 412-push-notification
felix-20 Jun 4, 2022
1e91f48
Squashed commit of the following:
felix-20 Jun 8, 2022
a1e7bef
merge dev
felix-20 Jun 9, 2022
6aead4a
automatically write template files
felix-20 Jun 9, 2022
5ef2b10
Merge branch 'development' into 412-push-notification
felix-20 Jun 15, 2022
c4c867f
try fix pre-commit
felix-20 Jun 15, 2022
0f542c1
try fix pre-commit
felix-20 Jun 15, 2022
7cad7b0
try except
felix-20 Jun 16, 2022
ca3becb
Merge branch 'development' into 412-push-notification
felix-20 Jun 16, 2022
048e8db
more debug
felix-20 Jun 16, 2022
e6dfcd9
more debug
felix-20 Jun 16, 2022
a51c6cd
debugging :)
felix-20 Jun 16, 2022
3573878
hotfix for qlearning file
felix-20 Jun 16, 2022
8ac1a25
adopting to the given agent works again
felix-20 Jun 17, 2022
c33ac90
Merge branch 'development' into 497-refactor-views
felix-20 Jun 17, 2022
f533393
restore docker manager and app.py
felix-20 Jun 17, 2022
1aa0b80
try restore again
felix-20 Jun 17, 2022
57bd8d0
webserver tests are running again
felix-20 Jun 17, 2022
8b17621
Merge branch '497-refactor-views' into 412-push-notification
felix-20 Jul 6, 2022
3aa1ea2
new rl_config
felix-20 Jul 6, 2022
3e0a512
precommit?
felix-20 Jul 6, 2022
3a04eb1
precommit!
felix-20 Jul 6, 2022
05bd62e
assert print
felix-20 Jul 6, 2022
fd2a0db
remove print
felix-20 Jul 6, 2022
b923838
new regex for matching ce agents
felix-20 Jul 6, 2022
86be826
Merge branch 'regex-fun' into 412-push-notification
felix-20 Jul 6, 2022
3b64ad5
fixed fixed price agent
felix-20 Jul 6, 2022
874474d
Merge branch 'fixed-agent-argument' into 412-push-notification
felix-20 Jul 6, 2022
63a1461
comment out assert
felix-20 Jul 6, 2022
006d8ed
delete some files
felix-20 Jul 8, 2022
fc3acb8
bcolors back
felix-20 Jul 8, 2022
0cd99e3
readme websocket
felix-20 Jul 8, 2022
8f37787
debug websocket
felix-20 Jul 8, 2022
0a86fc6
fixed websocket
felix-20 Jul 8, 2022
b768fb2
small websocket fix
felix-20 Jul 8, 2022
62e08a2
logging for websocket
felix-20 Jul 8, 2022
7b8c242
more logging?
felix-20 Jul 8, 2022
2bbd5f1
logging
felix-20 Jul 8, 2022
700a65f
logging relevant content
felix-20 Jul 8, 2022
75e4c8f
reset?
felix-20 Jul 8, 2022
d9645d1
reset webserver
felix-20 Jul 8, 2022
0c31639
Merge branch 'development' into logging-for-api
felix-20 Jul 8, 2022
04cd9f8
relevant gitignore stuff
felix-20 Jul 8, 2022
ac799e1
some resets in docker manager
felix-20 Jul 8, 2022
949612c
init files again?
felix-20 Jul 8, 2022
412c425
separate_markets again
felix-20 Jul 8, 2022
e11bb2d
Merge branch 'add-init-files-for-webserver' into logging-for-api
felix-20 Jul 8, 2022
592ea18
remove unnecessary code
felix-20 Jul 8, 2022
1d3c7b9
Merge branch 'add-init-files-for-webserver' into logging-for-api
felix-20 Jul 8, 2022
43309e9
fixed docker tests, introduce Mocked logger
felix-20 Jul 8, 2022
4e9341c
Merge branch 'development' into 412-push-notification
felix-20 Jul 8, 2022
bce3cea
remove unnecessary files
felix-20 Jul 8, 2022
7ee6012
Merge branch 'logging-for-api' into 412-websocket
felix-20 Jul 8, 2022
0ca0704
does it work again?
felix-20 Jul 8, 2022
435b862
create `log_file` folder on websocket startup
felix-20 Jul 8, 2022
fe0be64
`check_health_of_all_container`
felix-20 Jul 8, 2022
0494e09
ajax url for websocket
felix-20 Jul 11, 2022
a9dfeab
Merge branch 'development' into 412-websocket
felix-20 Jul 20, 2022
949d7ad
container_parser is now called container_helper
felix-20 Jul 20, 2022
1612651
Merge branch 'development' into 412-websocket
felix-20 Jul 26, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -440,11 +440,11 @@ To run tests you have written for the Django webserver go into the *webserver* f
python3 ./manage.py test -v 2
```

### 6.3. Docker API
### 6.3. API

There is a RESTful API written with the python libary FastAPI for communicating with docker containers that can be found in `/docker`.

The API needs to run on `127.0.0.1:8000`. To start the API go to `/docker` and run
The API needs to run on `0.0.0.0:8000`. To start the API go to `/docker` and run

```terminal
uvicorn app:app --reload
Expand All @@ -456,7 +456,23 @@ You can just run the `app.py` with python from the docker folder as well.

If you want to use the API, you need to provide an `AUTHORIZATION_TOKEN` in your environment variables. For each API request the value at the authorization header will be checked. You can only perform actions on the API, when this value is the same, as the value in your environment variable.

WARNING: Please keep in mind, that the `AUTHORIZATION_TOKEN` must be kept a secret, if it is revealed, you need to revoke it and set a new secret. Furthermore, think about using transport encryption to ensure that the token won't get stolen on the way.
**WARNING**: Please keep in mind, that the `AUTHORIZATION_TOKEN` must be kept a secret, if it is revealed, you need to revoke it and set a new secret. Furthermore, think about using transport encryption to ensure that the token won't get stolen on the way.

### 6.4. Websocket

You can receive notifications on your website about stopped container by our websocket.
Just run the `container_notification_websocket.py` file in `/docker`.
The websocket will run on port 8001.
Your webserver will automatically connect to this websocket.
Whenever a container is exited you will receive a push notification in your interface.

#### Troubleshooting Websocket

Check in the developer console of your favorite browser if the connection is established.
It might read "cannot connect to the websocket due to `ERR_CERT_AUTHORITY_INVALID` or similar".
This is caused by self signed certificates.
Open a new tab and enter the address of the websocket server, but with `https` protocol instead of `wss`.
Accept the risk and continue.

## 7. Tensorboard

Expand Down
65 changes: 65 additions & 0 deletions docker/container_notification_websocket.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import asyncio
import json
import logging
import os

import uvicorn
from docker_manager import DockerManager
from fastapi import FastAPI, WebSocket

logger = logging.getLogger('uvicorn.error')
path_to_log_files = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'log_files')
if not os.path.isdir(path_to_log_files):
os.makedirs(path_to_log_files)


class ConnectionManager:
def __init__(self):
self.active_connections = []

async def connect(self, websocket: WebSocket):
await websocket.accept()
self.active_connections.append(websocket)
logger.info(f'got new connection of {websocket}, current connections: {self.active_connections}')

def disconnect(self, websocket: WebSocket):
self.active_connections.remove(websocket)

async def broadcast(self, message: str):
for connection in self.active_connections:
await connection.send_json(message)


manager = DockerManager(logger)
connection_manager = ConnectionManager()
app = FastAPI()


@app.on_event('startup')
async def startup_event():
logger.info('started websocket')


@app.websocket('/wss')
async def websocket_endpoint(websocket: WebSocket):
await connection_manager.connect(websocket)
last_docker_info = None
try:
while True:
await asyncio.sleep(5)
is_exited, docker_info = manager.check_health_of_all_container()
if is_exited and last_docker_info != docker_info:
logger.info(f'Sending information about stopped container {vars(docker_info)}')
await connection_manager.broadcast(json.dumps(vars(docker_info)))
last_docker_info = docker_info
except Exception:
connection_manager.disconnect(websocket)


if __name__ == '__main__':
uvicorn.run('container_notification_websocket:app',
host='0.0.0.0',
port=8001,
log_config='./log_websocket.ini',
ssl_keyfile='/etc/sslzertifikat/api_cert.key',
ssl_certfile='/etc/sslzertifikat/api_cert.crt')
21 changes: 21 additions & 0 deletions docker/log_websocket.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[loggers]
keys=root

[handlers]
keys=logfile

[formatters]
keys=logfileformatter

[logger_root]
level=INFO
handlers=logfile

[formatter_logfileformatter]
format=[%(asctime)s.%(msecs)03d] %(levelname)s [%(thread)d] - %(message)s

[handler_logfile]
class=handlers.RotatingFileHandler
level=INFO
args=('log_files/websocket.log','a')
formatter=logfileformatter
2 changes: 1 addition & 1 deletion webserver/alpha_business_app/buttons.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from .config_merger import ConfigMerger
from .config_parser import ConfigFlatDictParser
from .container_parser import parse_response_to_database
from .container_helper import parse_response_to_database
from .handle_files import download_config, download_file
from .handle_requests import DOCKER_API, send_get_request, send_get_request_with_streaming, send_post_request, stop_container
from .models.config import Config
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import copy
import json

import names

from .config_parser import ConfigModelParser
from .models.container import Container
from .models.container import Container, update_container


def parse_response_to_database(api_response, config_dict: dict, given_name: str, user) -> None:
Expand All @@ -13,7 +14,8 @@ def parse_response_to_database(api_response, config_dict: dict, given_name: str,
Args:
api_response (APIResponse): The converted response from the docker API.
config_dict (dict): The dict the container have been started with.
given_name (str): the name the user put into the field
given_name (str): the name the user put into the field.
user (User): The user who did the request.
"""
started_container = api_response.content
# check if the api response is correct
Expand All @@ -22,7 +24,7 @@ def parse_response_to_database(api_response, config_dict: dict, given_name: str,
return False, [], 'The API answer was wrong, please try'

num_experiments = len(started_container)
name = names.get_first_name() if not given_name else given_name
name = names.get_first_name() if not given_name else str(given_name)

# save the used config
config_object = ConfigModelParser().parse_config(copy.deepcopy(config_dict))
Expand Down Expand Up @@ -53,3 +55,38 @@ def parse_response_to_database(api_response, config_dict: dict, given_name: str,
name=current_container_name,
user=user)
return True, [], name


def get_actually_stopped_container_from_api_notification(api_response: str) -> str:
"""
Parses the notification from the websocket to a notification in view.

Args:
api_response (str): _description_

Returns:
str: _description_
"""
try:
raw_data = json.loads(json.loads(api_response))
except json.decoder.JSONDecodeError:
print(f'Could not parse API Response to valid JSON {api_response}')
return False, []

polished_data = [item[1:-1].split(',') for item in raw_data['status'].split(';')]
polished_data = [(container_id[1:-1].strip(), exit_code.strip()) for container_id, exit_code in polished_data]
result = []
for container_id, exit_code in polished_data:
try:
wanted_container = Container.objects.get(id=container_id)
except Container.DoesNotExist:
continue
if wanted_container.health_status.startswith('exit'):
continue
update_container(container_id, {'health_status': f'exited({exit_code})'})
result += [{'container_name': wanted_container.name, 'exit_code': exit_code}]
print(result)
if not result:
return False, []

return True, result
6 changes: 5 additions & 1 deletion webserver/alpha_business_app/handle_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def _get_api_token() -> str:
except FileNotFoundError:
print('No .env file found, using environment variable instead.')
try:
master_secret = os.environ['API_TOKEN']
master_secret = os.environ['API_TOKEN'].strip()
except KeyError:
print('Could not get API key')
return 'abc'
Expand Down Expand Up @@ -154,6 +154,10 @@ def get_api_status() -> dict:
return {'api_docker_timeout': f'Docker unavailable - {current_time}'}


def websocket_url() -> str:
return 'wss://vm-midea03.eaalab.hpi.uni-potsdam.de:8001/wss'


def _error_handling_API(response) -> APIResponse:
"""
Defines error codes and appropriate response messages if we get error codes from the API.
Expand Down
56 changes: 49 additions & 7 deletions webserver/alpha_business_app/static/js/custom.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ $(document).ready(function() {
$(this).addClass("d-none")
});
}
}).trigger('change');
}).trigger("change");

$("select.marketplace-selection").change(function () {
// will be called when another marketplace has been selected
Expand Down Expand Up @@ -101,25 +101,34 @@ $(document).ready(function() {

function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
if (document.cookie && document.cookie !== "") {
const cookies = document.cookie.split(";");
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
if (cookie.substring(0, name.length + 1) === (name + "=")) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}


function replaceOrInsert(element, identifier, data) {
if (element.length > 0) {
element.replaceWith(data);
} else {
const endOfContent = document.getElementById(identifier);
endOfContent.insertAdjacentHTML("afterend", data);
}
}

$("button.form-check").click(function () {
var self = $(this);
var formdata = getFormData();

const csrftoken = getCookie('csrftoken');
const csrftoken = getCookie("csrftoken");
$.ajax({
type: "POST",
url: self.data("url"),
Expand All @@ -128,8 +137,41 @@ $(document).ready(function() {
formdata
},
success: function (data) {
$("p.notice-field").replaceWith(data);
replaceOrInsert($("#notice-field"), "end-of-content", data);
}
});
});
// var url = "";
// var url = "ws://192.168.159.134:8001/ws";
// var url = "wss://vm-midea03.eaalab.hpi.uni-potsdam.de:8001/wss";
$.ajax({url: "/api_info",
success: function (data) {
var url = data["url"];
console.log('ajax return', data["url"])
var ws = new WebSocket(url);
ws.onopen = function (_) {
console.log("connection to ", url, "open");
};
ws.onmessage = function(event) {
const csrftoken = getCookie("csrftoken");
$.ajax({
type: "POST",
url: "/container_notification",
data: {
csrfmiddlewaretoken: csrftoken,
api_response: event.data
},
success: function (data) {
const endOfContent = document.getElementById("main-nav-bar");
endOfContent.insertAdjacentHTML("afterend", data);
}
});
console.log(event.data);
};
ws.onclose = function(_) {
console.log("connection to ", url, "closed");
};
}
});

});
2 changes: 2 additions & 0 deletions webserver/alpha_business_app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
path('api_availability', views.api_availability, name='api_availability'),
path('marketplace_changed', views.marketplace_changed, name='marketplace'),
path('validate_config', views.config_validation, name='config_validation'),
path('container_notification', views.container_notification, name='container_notification'),
path('api_info', views.get_api_url, name='api_info'),

# User relevant urls
path('accounts/', include('django.contrib.auth.urls'))
Expand Down
16 changes: 14 additions & 2 deletions webserver/alpha_business_app/views.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
from uuid import uuid4

from django.contrib.auth.decorators import login_required
from django.http import Http404, HttpResponse
from django.http import Http404, HttpResponse, JsonResponse
from django.shortcuts import render

from recommerce.configuration.config_validation import validate_config

from .buttons import ButtonHandler
from .config_parser import ConfigFlatDictParser
from .container_helper import get_actually_stopped_container_from_api_notification
from .forms import UploadFileForm
from .handle_files import handle_uploaded_file
from .handle_requests import get_api_status
from .handle_requests import get_api_status, websocket_url
from .models.config import Config
from .models.container import Container
from .selection_manager import SelectionManager
Expand Down Expand Up @@ -145,6 +146,17 @@ def config_validation(request) -> HttpResponse:
return render(request, 'notice_field.html', {'success': 'This config is valid'})


@login_required
def container_notification(request):
if request.method == 'POST':
is_notification_necessary, result = get_actually_stopped_container_from_api_notification(request.POST['api_response'])
return render(request, 'alert_field.html', {'warning': result, 'should_render': is_notification_necessary})


def get_api_url(request):
return JsonResponse({'url': websocket_url()}, status=200, content_type='application/json')


@login_required
def marketplace_changed(request) -> HttpResponse:
if not request.user.is_authenticated:
Expand Down
9 changes: 9 additions & 0 deletions webserver/templates/alert_field.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% if should_render %}
<div class="alert alert-warning alert-dismissible fade show {% if not warning %}d-none{% endif %} m-3" id="alert-field" role="alert">
<strong>The following container stopped:</strong>
{% for item in warning %}
{{item.container_name}} exited({{item.exit_code}})
{% endfor %}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
7 changes: 4 additions & 3 deletions webserver/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
</head>
<body>
<div class="container">
<nav class="navbar navbar-expand-md navbar-light bg-light">
<nav class="navbar navbar-expand-md navbar-light bg-light" id="main-nav-bar">
<div class="container-fluid">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
Expand Down Expand Up @@ -58,10 +58,11 @@
</div>
</div>
</nav>
{% include "alert_field.html" %}
{% block content %}
{% endblock content %}
<br>
{% include "notice_field.html" %}
<br id="end-of-content">
{% include "notice_field.html"%}

<footer class="footer footer-light mt-5">
<table class="table table-responsive align-middle">
Expand Down
Loading