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

Initial Proof of Concept #26

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
69 changes: 55 additions & 14 deletions esi_ui/api/esi_api.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import asyncio
from websockets.asyncio.server import serve
from websockets.exceptions import ConnectionClosed
from threading import Thread
import json
import concurrent.futures
from collections import defaultdict
import itertools
import ssl

from django.conf import settings

Expand All @@ -10,40 +14,47 @@
from metalsmith import _provisioner
from metalsmith import instance_config
from openstack import config
from horizon.utils.memoized import memoized # noqa

from esi import connection
from esi.lib import nodes


DEFAULT_OPENSTACK_KEYSTONE_URL = 'http://127.0.0.1/identity/v3'

DEFAULT_WEBSOCKET_PORT = 10000

@memoized
def get_session_from_request(request):
DEFAULT_POLL_INTERVAL_SECONDS = 10


def get_session(token, project_id):
auth_url = getattr(settings, 'OPENSTACK_KEYSTONE_URL', DEFAULT_OPENSTACK_KEYSTONE_URL)
region = config.get_cloud_region(load_yaml_config=False,
load_envvars=False,
auth_type='token',
token=request.user.token.id,
token=token,
auth_url=auth_url)
user_session = region.get_session()
if not user_session.auth.get_access(user_session).project_scoped:
auth = v3_token.Token(
auth_url=auth_url,
token=user_session.get_token(),
project_id=request.user.project_id)
project_id=project_id)
user_session = session.Session(auth=auth)
return user_session


@memoized
def esiclient(request):
return connection.ESIConnection(session=get_session_from_request(request))
def esiclient(request, from_websocket=False):
if from_websocket:
token = request['token']
project_id = request['project_id']
else:
token = request.user.token.id
project_id = request.user.project_id
return connection.ESIConnection(session=get_session(token, project_id))


def node_list(request):
connection = esiclient(request)
def node_list(request, from_websocket=False):
connection = esiclient(request, from_websocket)

with concurrent.futures.ThreadPoolExecutor() as executor:
f1 = executor.submit(connection.lease.nodes)
Expand Down Expand Up @@ -142,7 +153,7 @@ def node_list(request):
def deploy_node(request, node):
kwargs = json.loads(request.body.decode('utf-8'))

provisioner = _provisioner.Provisioner(session=get_session_from_request(request))
provisioner = _provisioner.Provisioner(session=get_session(request.user.token.id, request.user.project_id))

if 'ssh_keys' in kwargs:
kwargs['config'] = instance_config.GenericConfig(ssh_keys=kwargs['ssh_keys'])
Expand All @@ -152,7 +163,7 @@ def deploy_node(request, node):


def undeploy_node(request, node):
provisioner = _provisioner.Provisioner(session=get_session_from_request(request))
provisioner = _provisioner.Provisioner(session=get_session(request.user.token.id, request.user.project_id))

provisioner.unprovision_node(node, wait=None)

Expand Down Expand Up @@ -242,9 +253,39 @@ def create_lease(request):
del lease_params['start_time']
if lease_params['end_time'] is None:
del lease_params['end_time']

return esiclient(request).lease.create_lease(**lease_params)


def delete_lease(request, lease):
return esiclient(request).lease.delete_lease(lease)


async def websocket_listen(ssl_context):
async def on_websocket_connect(websocket):
try:
token = await websocket.recv(decode=True)
project_id = await websocket.recv(decode=True)

except ConnectionClosed:
return
request = {"token": token, "project_id": project_id}

while True:
list_of_nodes = node_list(request, from_websocket=True)

try:
await websocket.send(json.dumps(list_of_nodes))
except ConnectionClosed:
break

await asyncio.sleep(getattr(settings, 'DEFAULT_POLL_INTERVAL_SECONDS', DEFAULT_POLL_INTERVAL_SECONDS))

async with serve(on_websocket_connect, '0.0.0.0', getattr(settings, 'DEFAULT_WEBSOCKET_PORT', DEFAULT_WEBSOCKET_PORT), ssl=ssl_context):
await asyncio.get_running_loop().create_future()

ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS)
ssl_context.load_cert_chain(settings.SSL_CERTIFICATE_PATH, keyfile=settings.SSL_KEY_PATH)
ssl_context.load_verify_locations(cafile=settings.SSL_CERTIFICATE_PATH)

Thread(target=asyncio.run, args=(websocket_listen(ssl_context),)).start()
42 changes: 40 additions & 2 deletions esi_ui/static/dashboard/esi/esi.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,49 @@
esiService.$inject = [
'horizon.framework.util.http.service',
'horizon.framework.widgets.toast.service',
'horizon.app.core.openstack-service-api.keystone',
'horizon.app.core.openstack-service-api.network',
];

function esiService(apiService, toastService, networkAPI) {
function esiService(apiService, toastService, keystoneAPI, networkAPI) {
var stack = [];
var onmessageDefer;
var socket = {
socket: new WebSocket('wss://' + window.location.hostname + ':10000'),
send: function(data) {
data = JSON.stringify(data);
if (socket.socket.readyState === WebSocket.OPEN) {
socket.socket.send(data);
} else {
stack.push(data);
}
},
onmessage: function(callback) {
if (socket.socket.readyState === WebSocket.OPEN) {
socket.socket.onmessage = callback;
} else {
onmessageDefer = callback;
}
},
};
socket.socket.onopen = function() {
for (const i in stack) {
socket.socket.send(stack[i]);
}
stack = [];
if (onmessageDefer) {
socket.socket.onmessage = onmessageDefer;
onmessageDefer = null;
}
};

keystoneAPI.getCurrentUserSession().then(function(response) {
socket.send(response.data.token);
socket.send(response.data.project_id);
});

var service = {
socket: function() { return socket; },
nodeList: nodeList,
setPowerState: setPowerState,
createLease: createLease,
Expand Down Expand Up @@ -146,4 +184,4 @@
}
}

})();
})();
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
.controller('horizon.dashboard.esi.lessee.nodes.EsiNodesTableController', controller);

controller.$inject = [
'$scope',
'$q',
'$timeout',
'horizon.app.core.openstack-service-api.esiService',
Expand Down Expand Up @@ -58,7 +59,7 @@
'rescue'
]);

function controller($q, $timeout, esiService, config,
function controller($scope, $q, $timeout, esiService, config,
filterFacets, manageNetworksModalService,
manageFloatingIPsModalService, provisioningModalService,
unprovisioningModalService, deleteLeasesModalService,
Expand All @@ -80,42 +81,24 @@

////////////////

spinnerService.showModalSpinner('Getting nodes');
spinnerService.showModalSpinner('Getting Nodes');
init();

function init() {
var promises = [keystoneAPI.getCurrentUserSession(), esiService.nodeList()];
return $q.all(promises)
.then(function(responses) {
ctrl.project_name = responses[0].data.project_name;
ctrl.nodesSrc = responses[1].data.nodes.filter(function(node) {
return node.leases.length === 0
|| node.leases[0].project === ctrl.project_name
|| (node.owner === ctrl.project_name && node.leases[0].status === 'created');
});
ctrl.nodesDisplay = ctrl.nodesSrc;
console.log(ctrl.nodesSrc);

var in_provision_transition = false;
ctrl.nodesSrc.forEach(function(node) {
node.network_operation = null;
if (PROVISION_ERROR_STATES.has(node.provision_state)) {
return;
}

if (PROVISION_STABLE_STATES.has(node.target_provision_state)) {
in_provision_transition = true;
}
});

if (in_provision_transition) {
$timeout(init, REFRESH_RATE);
}
keystoneAPI.getCurrentUserSession().then(function(response) {
ctrl.project_name = response.data.project_name;
});

esiService.socket().onmessage(function(message) {
spinnerService.hideModalSpinner();
})
.catch(function(response) {
spinnerService.hideModalSpinner();
$scope.$apply(function() {
ctrl.nodesSrc = JSON.parse(message.data);
ctrl.nodesDisplay = ctrl.nodesSrc.filter(function(node) {
return node.leases.length === 0
|| node.leases[0].project === ctrl.project_name
|| (node.owner === ctrl.project_name && node.leases[0].status === 'created');
});
});
});
}

Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ esisdk>=0.5.0 # Apache-2.0
horizon>=24.0.0
metalsmith>=2.3.0
keystoneauth1>=4.3.1 # Apache-2.0
websockets>=13.1