Merge "Add Cloud Shell feature"
This commit is contained in:
commit
17c720878f
1
_0330_cloud_shell_settings.py.sample
Normal file
1
_0330_cloud_shell_settings.py.sample
Normal file
@ -0,0 +1 @@
|
||||
CLOUD_SHELL_IMAGE = "gbraad/openstack-client:alpine"
|
@ -2,9 +2,19 @@
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Zun UI has no configuration option.
|
||||
Image for Cloud Shell
|
||||
---------------------
|
||||
|
||||
For more configurations, see
|
||||
The image for Cloud Shell is set as `gbraad/openstack-client:alpine`
|
||||
by default. If you want to use other image, edit `CLOUD_SHELL_IMAGE`
|
||||
variable in file `_0330_cloud_shell_settings.py.sample`, and copy
|
||||
it to `horizon/openstack_dashboard/local/local_settings.d/_0330_cloud_shell_settings.py`,
|
||||
and restart Horizon.
|
||||
|
||||
For more configurations
|
||||
-----------------------
|
||||
|
||||
See
|
||||
`Configuration Guide
|
||||
<https://docs.openstack.org/horizon/latest/configuration/index.html>`__
|
||||
in the Horizon documentation.
|
||||
|
@ -208,6 +208,10 @@ def container_attach(request, id):
|
||||
return zunclient(request).containers.attach(id)
|
||||
|
||||
|
||||
def container_resize(request, id, width, height):
|
||||
return zunclient(request).containers.resize(id, width, height)
|
||||
|
||||
|
||||
def image_list(request, limit=None, marker=None, sort_key=None,
|
||||
sort_dir=None, detail=True):
|
||||
return zunclient(request).images.list(limit, marker, sort_key,
|
||||
|
@ -82,6 +82,10 @@ class ContainerActions(generic.View):
|
||||
return client.container_kill(request, id, signal)
|
||||
elif action == 'attach':
|
||||
return client.container_attach(request, id)
|
||||
elif action == 'resize':
|
||||
width = request.DATA.get("width") or 500
|
||||
height = request.DATA.get("height") or 400
|
||||
return client.container_resize(request, id, width, height)
|
||||
|
||||
@rest_utils.ajax(data_required=True)
|
||||
def delete(self, request, id, action):
|
||||
|
0
zun_ui/content/cloud_shell/__init__.py
Normal file
0
zun_ui/content/cloud_shell/__init__.py
Normal file
27
zun_ui/content/cloud_shell/views.py
Normal file
27
zun_ui/content/cloud_shell/views.py
Normal file
@ -0,0 +1,27 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from horizon import views
|
||||
|
||||
|
||||
class CloudShellView(views.HorizonTemplateView):
|
||||
template_name = 'cloud_shell/cloud_shell.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(CloudShellView, self).get_context_data(**kwargs)
|
||||
if hasattr(settings, "CLOUD_SHELL_IMAGE"):
|
||||
context['CLOUD_SHELL_IMAGE'] = settings.CLOUD_SHELL_IMAGE
|
||||
else:
|
||||
context['CLOUD_SHELL_IMAGE'] = "gbraad/openstack-client:alpine"
|
||||
return context
|
26
zun_ui/enabled/_0330_cloud_shell.py
Normal file
26
zun_ui/enabled/_0330_cloud_shell.py
Normal file
@ -0,0 +1,26 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
FEATURE = True
|
||||
|
||||
ADD_ANGULAR_MODULES = [
|
||||
'horizon.cloud-shell'
|
||||
]
|
||||
|
||||
ADD_SCSS_FILES = [
|
||||
'cloud-shell/cloud-shell.scss'
|
||||
]
|
||||
|
||||
# A list of extensible header views to be displayed
|
||||
ADD_HEADER_SECTIONS = [
|
||||
'zun_ui.content.cloud_shell.views.CloudShellView',
|
||||
]
|
147
zun_ui/static/cloud-shell/cloud-shell.controller.js
Normal file
147
zun_ui/static/cloud-shell/cloud-shell.controller.js
Normal file
@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License. You may obtain
|
||||
* a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
"use strict";
|
||||
|
||||
angular
|
||||
.module('horizon.cloud-shell')
|
||||
.controller('horizon.cloud-shell.controller', cloudShellController);
|
||||
|
||||
cloudShellController.$inject = [
|
||||
'$scope',
|
||||
'horizon.app.core.openstack-service-api.zun',
|
||||
'horizon.dashboard.container.webRoot',
|
||||
'horizon.framework.util.http.service'
|
||||
];
|
||||
|
||||
function cloudShellController(
|
||||
$scope,
|
||||
zun,
|
||||
webRoot,
|
||||
http
|
||||
) {
|
||||
var ctrl = this;
|
||||
ctrl.openInNewWindow = openInNewWindow;
|
||||
ctrl.close = closeShell;
|
||||
ctrl.consoleUrl = null;
|
||||
ctrl.container = {};
|
||||
ctrl.resizeTerminal = resizeTerminal;
|
||||
|
||||
// close existing shell
|
||||
closeShell();
|
||||
|
||||
// default size for shell
|
||||
var cols = 80;
|
||||
var rows = 24;
|
||||
|
||||
// get openrc v3 for OpenStack Client
|
||||
var cloudsYaml;
|
||||
http.get('/project/api_access/clouds.yaml/').then(function(response) {
|
||||
// cloud.yaml to be set to .config/openstack/clouds.yaml in container
|
||||
cloudsYaml = response.data;
|
||||
|
||||
ctrl.user = cloudsYaml.match(/username: "(.+)"/)[1];
|
||||
ctrl.project = cloudsYaml.match(/project_name: "(.+)"/)[1];
|
||||
ctrl.userDomain = cloudsYaml.match(/user_domain_name: "(.+)"/);
|
||||
ctrl.projectDomain = cloudsYaml.match(/project_domain_name: "(.+)"/);
|
||||
ctrl.domain = (ctrl.userDomain.length === 2) ? ctrl.userDomain[1] : ctrl.projectDomain[1];
|
||||
ctrl.region = cloudsYaml.match(/region_name: "(.+)"/)[1];
|
||||
|
||||
// container name
|
||||
ctrl.container.name = "cloud-shell-" + ctrl.user + "-" + ctrl.project +
|
||||
"-" + ctrl.domain + "-" + ctrl.region;
|
||||
|
||||
// get container
|
||||
zun.getContainer(ctrl.container.name, true).then(onGetContainer, onFailGetContainer);
|
||||
});
|
||||
|
||||
function onGetContainer(response) {
|
||||
ctrl.container = response.data;
|
||||
|
||||
// attach console to existing container
|
||||
ctrl.consoleUrl = webRoot + "containers/" + ctrl.container.id + "/console";
|
||||
var console = $("<p>To display console, interactive mode needs to be enabled " +
|
||||
"when this container was created.</p>");
|
||||
if (ctrl.container.status !== "Running") {
|
||||
console = $("<p>Container is not running. Please wait for starting up container.</p>");
|
||||
} else if (ctrl.container.interactive) {
|
||||
console = $("<iframe id=\"console_embed\" src=\"" + ctrl.consoleUrl +
|
||||
"\" style=\"width:100%;height:100%\"></iframe>");
|
||||
|
||||
// execute openrc.sh on the container
|
||||
var command = "sh -c 'printf \"" + cloudsYaml + "\" > ~/.config/openstack/clouds.yaml'";
|
||||
zun.executeContainer(ctrl.container.id, {command: command}).then(function() {
|
||||
var command = "sh -c 'printf \"export OS_CLOUD=openstack\" > ~/.bashrc'";
|
||||
zun.executeContainer(ctrl.container.id, {command: command}).then(function() {
|
||||
angular.noop();
|
||||
});
|
||||
});
|
||||
}
|
||||
// append shell content
|
||||
angular.element("#shell-content").append(console);
|
||||
}
|
||||
|
||||
// watcher for iframe contents loading, seems to emit once.
|
||||
$scope.$watch(function() {
|
||||
return angular.element("#shell-content > iframe").contents()
|
||||
.find("#terminalNode").attr("termCols");
|
||||
}, resizeTerminal);
|
||||
// event handler to resize console according to window resize.
|
||||
angular.element(window).bind('resize', resizeTerminal);
|
||||
// also, add resizeTerminal into callback attribute for resizer directive
|
||||
function resizeTerminal() {
|
||||
var shellIframe = angular.element("#shell-content > iframe");
|
||||
var newCols = shellIframe.contents().find("#terminalNode").attr("termCols");
|
||||
var newRows = shellIframe.contents().find("#terminalNode").attr("termRows");
|
||||
if ((newCols !== cols || newRows !== rows) && newCols > 0 && newRows > 0) {
|
||||
// resize tty
|
||||
zun.resizeContainer(ctrl.container.id, {width: newCols, height: newRows}).then(function() {
|
||||
cols = newCols;
|
||||
rows = newRows;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function onFailGetContainer() {
|
||||
// create new container and attach console to it.
|
||||
var image = angular.element("#cloud-shell-menu").attr("cloud-shell-image");
|
||||
var model = {
|
||||
name: ctrl.container.name,
|
||||
image: image,
|
||||
command: "/bin/bash",
|
||||
interactive: true,
|
||||
run: true,
|
||||
environment: "OS_CLOUD=openstack",
|
||||
labels: "cloud-shell=" + ctrl.container.name
|
||||
};
|
||||
zun.createContainer(model).then(function (response) {
|
||||
// attach
|
||||
onGetContainer({data: {id: response.data.id}});
|
||||
});
|
||||
}
|
||||
|
||||
function openInNewWindow() {
|
||||
// open shell in new window
|
||||
window.open(ctrl.consoleUrl, "_blank");
|
||||
closeShell();
|
||||
}
|
||||
|
||||
function closeShell() {
|
||||
// close shell
|
||||
angular.element("#cloud-shell").remove();
|
||||
angular.element("#cloud-shell-resizer").remove();
|
||||
}
|
||||
}
|
||||
})();
|
23
zun_ui/static/cloud-shell/cloud-shell.html
Normal file
23
zun_ui/static/cloud-shell/cloud-shell.html
Normal file
@ -0,0 +1,23 @@
|
||||
<div ng-controller="horizon.cloud-shell.controller as ctrl">
|
||||
<resizer id="cloud-shell-resizer"
|
||||
direction="horizontal"
|
||||
height="6"
|
||||
bottom="#cloud-shell"
|
||||
callback="ctrl.resizeTerminal()">
|
||||
</resizer>
|
||||
<div id="cloud-shell">
|
||||
<div id="shell-header">
|
||||
{$ ctrl.container.name $}
|
||||
<!--
|
||||
<a class="cloud-shell-external" ng-click="ctrl.openInNewWindow()">
|
||||
<span class="fa fa-external-link"></span>
|
||||
</a>
|
||||
-->
|
||||
<a class="cloud-shell-close" ng-click="ctrl.close()">
|
||||
<span class="fa fa-times"></span>
|
||||
</a>
|
||||
</div>
|
||||
<div id="shell-content">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
37
zun_ui/static/cloud-shell/cloud-shell.module.js
Normal file
37
zun_ui/static/cloud-shell/cloud-shell.module.js
Normal file
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License. You may obtain
|
||||
* a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @ngdoc overview
|
||||
* @name horizon.cloud-shell
|
||||
* @description
|
||||
* cloud_shell module to host container for cloud shell.
|
||||
*/
|
||||
angular
|
||||
.module('horizon.cloud-shell', [
|
||||
'horizon.cloud-shell.resizer',
|
||||
'ngRoute'
|
||||
])
|
||||
.config(config);
|
||||
|
||||
config.$inject = ['$provide', '$windowProvider'];
|
||||
|
||||
function config($provide, $windowProvider) {
|
||||
var path = $windowProvider.$get().STATIC_URL + 'cloud-shell/';
|
||||
$provide.constant('horizon.cloud-shell.basePath', path);
|
||||
}
|
||||
})();
|
51
zun_ui/static/cloud-shell/cloud-shell.scss
Normal file
51
zun_ui/static/cloud-shell/cloud-shell.scss
Normal file
@ -0,0 +1,51 @@
|
||||
#cloud-shell {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
#shell-header {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background-color: gray;
|
||||
padding-left: 3px;
|
||||
}
|
||||
#shell-content {
|
||||
height: calc(100% - 20px);
|
||||
}
|
||||
.cloud-shell-external {
|
||||
position: relative;
|
||||
top: 0px;
|
||||
padding-left: 3px;
|
||||
color: cyan;
|
||||
}
|
||||
.cloud-shell-external:hover {
|
||||
color: red;
|
||||
}
|
||||
.cloud-shell-close {
|
||||
position: relative;
|
||||
top: 0px;
|
||||
float: right;
|
||||
padding-right: 3px;
|
||||
color: white;
|
||||
}
|
||||
.cloud-shell-close:hover {
|
||||
color: red;
|
||||
}
|
||||
#cloud-shell-resizer {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
bottom: 200px;
|
||||
height: 6px;
|
||||
width: 100%;
|
||||
background-color: lightgray;
|
||||
cursor: n-resize;
|
||||
}
|
||||
#cloud-shell-resize-holder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
56
zun_ui/static/cloud-shell/cloud-shell.service.js
Normal file
56
zun_ui/static/cloud-shell/cloud-shell.service.js
Normal file
@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License. You may obtain
|
||||
* a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
"use strict";
|
||||
|
||||
angular
|
||||
.module('horizon.cloud-shell')
|
||||
.factory('horizon.cloud-shell.service', cloudShellService);
|
||||
|
||||
cloudShellService.$inject = [
|
||||
'$rootScope',
|
||||
'$templateRequest',
|
||||
'horizon.cloud-shell.basePath'
|
||||
];
|
||||
|
||||
function cloudShellService(
|
||||
$rootScope,
|
||||
$templateRequest,
|
||||
basePath
|
||||
) {
|
||||
|
||||
var service = {
|
||||
init: init
|
||||
};
|
||||
|
||||
return service;
|
||||
|
||||
function init () {
|
||||
// remove existing cloud shell
|
||||
angular.element(".cloud_shell").remove();
|
||||
|
||||
// load html for cloud shell
|
||||
$templateRequest(basePath + 'cloud-shell.html').then(function (html) {
|
||||
var scope = $rootScope.$new();
|
||||
var template = angular.element(html);
|
||||
// compile html
|
||||
angular.element(document.body).injector().invoke(['$compile', function ($compile) {
|
||||
$compile(template)(scope);
|
||||
angular.element('body').append(template);
|
||||
}]);
|
||||
});
|
||||
}
|
||||
}
|
||||
})();
|
94
zun_ui/static/cloud-shell/resizer.directive.js
Normal file
94
zun_ui/static/cloud-shell/resizer.directive.js
Normal file
@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License. You may obtain
|
||||
* a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('horizon.cloud-shell.resizer', [])
|
||||
.directive('resizer', resizer);
|
||||
|
||||
resizer.$inject = ['$document'];
|
||||
|
||||
function resizer($document) {
|
||||
|
||||
var directive = {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
direction: '@',
|
||||
max: '@',
|
||||
left: '@',
|
||||
right: '@',
|
||||
top: '@',
|
||||
bottom: '@',
|
||||
width: '@',
|
||||
height: '@',
|
||||
callback: '&'
|
||||
},
|
||||
link: link
|
||||
};
|
||||
|
||||
return directive;
|
||||
|
||||
////////////////////
|
||||
|
||||
function link($scope, $element) {
|
||||
$element.on('mousedown', function(event) {
|
||||
event.preventDefault();
|
||||
$document.on('mousemove', mousemove);
|
||||
$document.on('mouseup', mouseup);
|
||||
});
|
||||
|
||||
function mousemove(event) {
|
||||
if ($scope.direction === 'vertical') {
|
||||
// Handle vertical resizer
|
||||
var x = event.pageX;
|
||||
|
||||
if ($scope.max && x > $scope.max) {
|
||||
x = parseInt($scope.max, 10);
|
||||
}
|
||||
$element.css({
|
||||
left: x + 'px'
|
||||
});
|
||||
$($scope.left).css({
|
||||
width: x + 'px'
|
||||
});
|
||||
$($scope.right).css({
|
||||
left: (x + parseInt($scope.width, 10)) + 'px'
|
||||
});
|
||||
} else {
|
||||
// Handle horizontal resizer
|
||||
var y = window.innerHeight - event.pageY;
|
||||
$element.css({
|
||||
bottom: y + 'px'
|
||||
});
|
||||
$($scope.top).css({
|
||||
bottom: (y + parseInt($scope.height, 10)) + 'px'
|
||||
});
|
||||
$($scope.bottom).css({
|
||||
height: y + 'px'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function mouseup() {
|
||||
$document.unbind('mousemove', mousemove);
|
||||
$document.unbind('mouseup', mouseup);
|
||||
if (typeof $scope.callback === "function") {
|
||||
$scope.callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
@ -3,7 +3,7 @@
|
||||
}
|
||||
.console {
|
||||
margin-top: 10px;
|
||||
height: 500px;
|
||||
height: calc(100vh - 300px);
|
||||
}
|
||||
textarea#output {
|
||||
height: 25em;
|
||||
|
@ -44,6 +44,7 @@
|
||||
unpauseContainer: unpauseContainer,
|
||||
executeContainer: executeContainer,
|
||||
killContainer: killContainer,
|
||||
resizeContainer: resizeContainer,
|
||||
pullImage: pullImage,
|
||||
getImages: getImages
|
||||
};
|
||||
@ -64,9 +65,12 @@
|
||||
return apiService.patch(containersPath + id, params).error(error(msg));
|
||||
}
|
||||
|
||||
function getContainer(id) {
|
||||
var msg = gettext('Unable to retrieve the Container.');
|
||||
return apiService.get(containersPath + id).error(error(msg));
|
||||
function getContainer(id, suppressError) {
|
||||
var promise = apiService.get(containersPath + id);
|
||||
return suppressError ? promise : promise.error(function() {
|
||||
var msg = gettext('Unable to retrieve the Container.');
|
||||
toastService.add('error', msg);
|
||||
});
|
||||
}
|
||||
|
||||
function getContainers() {
|
||||
@ -144,6 +148,11 @@
|
||||
return apiService.post(containersPath + id + '/kill', params).error(error(msg));
|
||||
}
|
||||
|
||||
function resizeContainer(id, params) {
|
||||
var msg = gettext('Unable to resize console.');
|
||||
return apiService.post(containersPath + id + '/resize', params).error(error(msg));
|
||||
}
|
||||
|
||||
////////////
|
||||
// Images //
|
||||
////////////
|
||||
|
10
zun_ui/templates/cloud_shell/cloud_shell.html
Normal file
10
zun_ui/templates/cloud_shell/cloud_shell.html
Normal file
@ -0,0 +1,10 @@
|
||||
{% load i18n %}
|
||||
|
||||
<!-- Menu Item for Extensible Header -->
|
||||
<span
|
||||
id="cloud-shell-menu"
|
||||
class="fa fa-terminal"
|
||||
cloud-shell-image="{{ CLOUD_SHELL_IMAGE }}"
|
||||
onclick="angular.element(document.body).injector().get('horizon.cloud-shell.service').init();">
|
||||
{% trans "Cloud Shell" %}
|
||||
</span>
|
Loading…
x
Reference in New Issue
Block a user