diff --git a/README.rst b/README.rst index 6c6ab16..545ff88 100644 --- a/README.rst +++ b/README.rst @@ -50,6 +50,8 @@ And enable it in Horizon:: cp ../zun-ui/zun_ui/enabled/_1330_project_container_panelgroup.py openstack_dashboard/local/enabled cp ../zun-ui/zun_ui/enabled/_1331_project_container_containers_panel.py openstack_dashboard/local/enabled + cp ../zun-ui/zun_ui/enabled/_2330_project_container_panelgroup.py openstack_dashboard/local/enabled + cp ../zun-ui/zun_ui/enabled/_2331_project_container_images_panel.py openstack_dashboard/local/enabled To run horizon with the newly enabled Zun UI plugin run:: diff --git a/doc/source/index.rst b/doc/source/index.rst index 71c7aaf..84d4f60 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -50,6 +50,8 @@ And enable it in Horizon:: cp ../zun-ui/zun_ui/enabled/_1330_project_container_panelgroup.py openstack_dashboard/local/enabled cp ../zun-ui/zun_ui/enabled/_1331_project_container_containers_panel.py openstack_dashboard/local/enabled + cp ../zun-ui/zun_ui/enabled/_2330_project_container_panelgroup.py openstack_dashboard/local/enabled + cp ../zun-ui/zun_ui/enabled/_2331_project_container_images_panel.py openstack_dashboard/local/enabled To run horizon with the newly enabled Zun UI plugin run:: diff --git a/zun_ui/api/client.py b/zun_ui/api/client.py index 3ec3a08..a416c94 100644 --- a/zun_ui/api/client.py +++ b/zun_ui/api/client.py @@ -21,6 +21,7 @@ from zunclient.v1 import client as zun_client LOG = logging.getLogger(__name__) CONTAINER_CREATE_ATTRS = zun_client.containers.CREATION_ATTRIBUTES +IMAGE_PULL_ATTRS = zun_client.images.PULL_ATTRIBUTES @memoized @@ -133,3 +134,22 @@ def container_kill(request, id, signal=None): def container_attach(request, id): return zunclient(request).containers.attach(id) + + +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, + sort_dir, False) + + +def image_create(request, **kwargs): + args = {} + for (key, value) in kwargs.items(): + + if key in IMAGE_PULL_ATTRS: + args[str(key)] = str(value) + else: + raise exceptions.BadRequest( + "Key must be in %s" % ",".join(IMAGE_PULL_ATTRS)) + + return zunclient(request).images.create(**args) diff --git a/zun_ui/api/rest_api.py b/zun_ui/api/rest_api.py index e5fb1d8..0421c80 100644 --- a/zun_ui/api/rest_api.py +++ b/zun_ui/api/rest_api.py @@ -118,3 +118,30 @@ class Containers(generic.View): return rest_utils.CreatedResponse( '/api/zun/container/%s' % new_container.uuid, new_container.to_dict()) + + +@urls.register +class Images(generic.View): + """API for Zun Images""" + url_regex = r'zun/images/$' + + @rest_utils.ajax() + def get(self, request): + """Get a list of the Images for admin users. + + The returned result is an object with property 'items' and each + item under this is a Image. + """ + result = client.image_list(request) + return {'items': [change_to_id(i.to_dict()) for i in result]} + + @rest_utils.ajax(data_required=True) + def post(self, request): + """Create a new Image. + + Returns the new Image object on success. + """ + new_image = client.image_create(request, **request.DATA) + return rest_utils.CreatedResponse( + '/api/zun/image/%s' % new_image.uuid, + new_image.to_dict()) diff --git a/zun_ui/content/container/images/__init__.py b/zun_ui/content/container/images/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/zun_ui/content/container/images/panel.py b/zun_ui/content/container/images/panel.py new file mode 100644 index 0000000..c62a573 --- /dev/null +++ b/zun_ui/content/container/images/panel.py @@ -0,0 +1,19 @@ +# 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.utils.translation import ugettext_lazy as _ +import horizon + + +class Images(horizon.Panel): + name = _("Images") + slug = "container.images" diff --git a/zun_ui/content/container/images/urls.py b/zun_ui/content/container/images/urls.py new file mode 100644 index 0000000..2620e7f --- /dev/null +++ b/zun_ui/content/container/images/urls.py @@ -0,0 +1,20 @@ +# 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.urls import url +from django.utils.translation import ugettext_lazy as _ +from horizon.browsers import views + +title = _("Images") +urlpatterns = [ + url('', views.AngularIndexView.as_view(title=title), name='index'), +] diff --git a/zun_ui/enabled/_2330_admin_container_panelgroup.py b/zun_ui/enabled/_2330_admin_container_panelgroup.py new file mode 100644 index 0000000..a871611 --- /dev/null +++ b/zun_ui/enabled/_2330_admin_container_panelgroup.py @@ -0,0 +1,20 @@ +# 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.utils.translation import ugettext_lazy as _ + +# The slug of the panel group to be added to HORIZON_CONFIG. Required. +PANEL_GROUP = 'container' +# The display name of the PANEL_GROUP. Required. +PANEL_GROUP_NAME = _('Container') +# The slug of the dashboard the PANEL_GROUP associated with. Required. +PANEL_GROUP_DASHBOARD = 'admin' diff --git a/zun_ui/enabled/_2331_admin_container_images_panel.py b/zun_ui/enabled/_2331_admin_container_images_panel.py new file mode 100644 index 0000000..a18f1f8 --- /dev/null +++ b/zun_ui/enabled/_2331_admin_container_images_panel.py @@ -0,0 +1,21 @@ +# 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. + +# The slug of the panel to be added to HORIZON_CONFIG. Required. +PANEL = 'container.images' +# The slug of the panel group the PANEL is associated with. +PANEL_GROUP = 'container' +# The slug of the dashboard the PANEL associated with. Required. +PANEL_DASHBOARD = 'admin' + +# Python panel class of the PANEL to be added. +ADD_PANEL = 'zun_ui.content.container.images.panel.Images' diff --git a/zun_ui/static/dashboard/container/container.module.js b/zun_ui/static/dashboard/container/container.module.js index eb8a504..d6b1e30 100644 --- a/zun_ui/static/dashboard/container/container.module.js +++ b/zun_ui/static/dashboard/container/container.module.js @@ -24,6 +24,7 @@ angular .module('horizon.dashboard.container', [ 'horizon.dashboard.container.containers', + 'horizon.dashboard.container.images', 'ngRoute' ]) .config(config); diff --git a/zun_ui/static/dashboard/container/images/actions.module.js b/zun_ui/static/dashboard/container/images/actions.module.js new file mode 100644 index 0000000..061c23f --- /dev/null +++ b/zun_ui/static/dashboard/container/images/actions.module.js @@ -0,0 +1,58 @@ +/** + * 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 + * @ngname horizon.dashboard.container.images.actions + * + * @description + * Provides all of the actions for images. + */ + angular.module('horizon.dashboard.container.images.actions', + [ + 'horizon.framework', + 'horizon.dashboard.container' + ]) + .run(registerImageActions); + + registerImageActions.$inject = [ + 'horizon.framework.conf.resource-type-registry.service', + 'horizon.framework.util.i18n.gettext', + 'horizon.dashboard.container.images.actions.create.service', + 'horizon.dashboard.container.images.resourceType' + ]; + + function registerImageActions( + registry, + gettext, + createImageService, + resourceType + ) { + var imagesResourceType = registry.getResourceType(resourceType); + + imagesResourceType.globalActions + .append({ + id: 'createImageAction', + service: createImageService, + template: { + type: 'create', + text: gettext('Pull Image') + } + }); + } + +})(); diff --git a/zun_ui/static/dashboard/container/images/actions/create.service.js b/zun_ui/static/dashboard/container/images/actions/create.service.js new file mode 100644 index 0000000..c00637e --- /dev/null +++ b/zun_ui/static/dashboard/container/images/actions/create.service.js @@ -0,0 +1,84 @@ +/** + * 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 factory + * @name horizon.dashboard.container.images.create.service + * @description + * Service for the pull image modal + */ + angular + .module('horizon.dashboard.container.images.actions') + .factory('horizon.dashboard.container.images.actions.create.service', createImageService); + + createImageService.$inject = [ + 'horizon.app.core.openstack-service-api.policy', + 'horizon.app.core.openstack-service-api.zun', + 'horizon.dashboard.container.images.actions.workflow', + 'horizon.dashboard.container.images.resourceType', + 'horizon.framework.util.actions.action-result.service', + 'horizon.framework.util.i18n.gettext', + 'horizon.framework.util.q.extensions', + 'horizon.framework.widgets.form.ModalFormService', + 'horizon.framework.widgets.toast.service' + ]; + + function createImageService( + policy, zun, workflow, resourceType, + actionResult, gettext, $qExtensions, modal, toast + ) { + + var message = { + success: gettext('Image %s was successfully pulled.') + }; + + var service = { + initAction: initAction, + perform: perform, + allowed: allowed + }; + + return service; + + ////////////// + + function initAction() { + } + + function perform() { + var title, submitText; + title = gettext('Pull Image'); + submitText = gettext('Pull'); + var config = workflow.init('create', title, submitText); + return modal.open(config).then(submit); + } + + function allowed() { + return policy.ifAllowed({ rules: [['image', 'pull_image']] }); + } + + function submit(context) { + return zun.pullImage(context.model, true).then(success, true); + } + + function success(response) { + toast.add('success', interpolate(message.success, [response.data.id])); + var result = actionResult.getActionResult().created(resourceType, response.data.name); + return result.result; + } + } +})(); diff --git a/zun_ui/static/dashboard/container/images/actions/workflow.service.js b/zun_ui/static/dashboard/container/images/actions/workflow.service.js new file mode 100644 index 0000000..d497884 --- /dev/null +++ b/zun_ui/static/dashboard/container/images/actions/workflow.service.js @@ -0,0 +1,88 @@ +/** + * 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 factory + * @name horizon.dashboard.container.images.workflow + * @description + * Workflow for pulling image + */ + angular + .module('horizon.dashboard.container.images.actions') + .factory('horizon.dashboard.container.images.actions.workflow', workflow); + + workflow.$inject = [ + 'horizon.framework.util.i18n.gettext' + ]; + + function workflow(gettext) { + var workflow = { + init: init + }; + + function init(actionType, title, submitText) { + var schema, form, model; + + // schema + schema = { + type: 'object', + properties: { + repo: { + title: gettext('Image'), + type: 'string' + } + } + }; + + // form + form = [ + { + type: 'section', + htmlClass: 'row', + items: [ + { + type: 'section', + htmlClass: 'col-sm-12', + items: [ + { + key: 'repo', + placeholder: gettext('Name of the image.') + } + ] + } + ] + } + ]; // form + + model = { + repo: '' + }; + + var config = { + title: title, + submitText: submitText, + schema: schema, + form: form, + model: model + }; + + return config; + } + + return workflow; + } +})(); diff --git a/zun_ui/static/dashboard/container/images/drawer.html b/zun_ui/static/dashboard/container/images/drawer.html new file mode 100644 index 0000000..812e6d9 --- /dev/null +++ b/zun_ui/static/dashboard/container/images/drawer.html @@ -0,0 +1,5 @@ + + diff --git a/zun_ui/static/dashboard/container/images/images.module.js b/zun_ui/static/dashboard/container/images/images.module.js new file mode 100644 index 0000000..73fa7d1 --- /dev/null +++ b/zun_ui/static/dashboard/container/images/images.module.js @@ -0,0 +1,142 @@ +/** + * 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.dashboard.container.images + * @ngModule + * @description + * Provides all the services and widgets require to display the images + * panel + */ + angular + .module('horizon.dashboard.container.images', [ + 'ngRoute', + 'horizon.dashboard.container.images.actions' + ]) + .constant('horizon.dashboard.container.images.events', events()) + .constant('horizon.dashboard.container.images.resourceType', 'OS::Zun::Image') + .run(run) + .config(config); + + /** + * @ngdoc constant + * @name horizon.dashboard.container.images.events + * @description A list of events used by Images + * @returns {Object} Event constants + */ + function events() { + return { + CREATE_SUCCESS: 'horizon.dashboard.container.images.CREATE_SUCCESS', + DELETE_SUCCESS: 'horizon.dashboard.container.images.DELETE_SUCCESS' + }; + } + + run.$inject = [ + 'horizon.framework.conf.resource-type-registry.service', + 'horizon.app.core.openstack-service-api.zun', + 'horizon.dashboard.container.images.basePath', + 'horizon.dashboard.container.images.resourceType', + 'horizon.dashboard.container.images.service' + ]; + + function run(registry, zun, basePath, resourceType, imageService) { + registry.getResourceType(resourceType) + .setNames(gettext('Image'), gettext('Images')) + // for detail summary view on table row. + .setSummaryTemplateUrl(basePath + 'drawer.html') + // for table row items and detail summary view. + .setProperties(imageProperties()) + .setListFunction(imageService.getImagesPromise) + .tableColumns + .append({ + id: 'id', + priority: 2 + }) + .append({ + id: 'repo', + priority: 1, + sortDefault: true + }) + .append({ + id: 'tag', + priority: 1 + }) + .append({ + id: 'size', + priority: 1 + }) + .append({ + id: 'image_id', + priority: 3 + }); + // for magic-search + registry.getResourceType(resourceType).filterFacets + .append({ + 'label': gettext('Image'), + 'name': 'repo', + 'singleton': true + }) + .append({ + 'label': gettext('Tag'), + 'name': 'tag', + 'singleton': true + }) + .append({ + 'label': gettext('ID'), + 'name': 'id', + 'singleton': true + }) + .append({ + 'label': gettext('Image ID'), + 'name': 'image_id', + 'singleton': true + }); + } + + function imageProperties() { + return { + 'id': {label: gettext('ID'), filters: ['noValue'] }, + 'repo': { label: gettext('Image'), filters: ['noValue'] }, + 'tag': { label: gettext('Tag'), filters: ['noValue'] }, + 'size': { label: gettext('Size'), filters: ['noValue', 'bytes'] }, + 'image_id': { label: gettext('Image ID'), filters: ['noValue'] } + }; + } + + config.$inject = [ + '$provide', + '$windowProvider', + '$routeProvider' + ]; + + /** + * @name config + * @param {Object} $provide + * @param {Object} $windowProvider + * @param {Object} $routeProvider + * @description Routes used by this module. + * @returns {undefined} Returns nothing + */ + function config($provide, $windowProvider, $routeProvider) { + var path = $windowProvider.$get().STATIC_URL + 'dashboard/container/images/'; + $provide.constant('horizon.dashboard.container.images.basePath', path); + $routeProvider.when('/admin/container/images', { + templateUrl: path + 'panel.html' + }); + } +})(); diff --git a/zun_ui/static/dashboard/container/images/images.service.js b/zun_ui/static/dashboard/container/images/images.service.js new file mode 100644 index 0000000..97f35f8 --- /dev/null +++ b/zun_ui/static/dashboard/container/images/images.service.js @@ -0,0 +1,60 @@ +/* + * 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.dashboard.container.images') + .factory('horizon.dashboard.container.images.service', imagesService); + + imagesService.$inject = [ + 'horizon.app.core.detailRoute', + 'horizon.app.core.openstack-service-api.zun' + ]; + + /* + * @ngdoc factory + * @name horizon.dashboard.container.images.service + * + * @description + * This service provides functions that are used through + * the images of container features. + */ + function imagesService(detailRoute, zun) { + return { + getImagesPromise: getImagesPromise + }; + + /* + * @ngdoc function + * @name getImagesPromise + * @description + * Given filter/query parameters, returns a promise for the matching + * images. This is used in displaying lists of images. + */ + function getImagesPromise(params) { + return zun.getImages(params).then(modifyResponse); + } + + function modifyResponse(response) { + return {data: {items: response.data.items.map(modifyItem)}}; + + function modifyItem(item) { + var timestamp = new Date(); + item.trackBy = item.id.concat(timestamp.getTime()); + return item; + } + } + } +})(); diff --git a/zun_ui/static/dashboard/container/images/panel.html b/zun_ui/static/dashboard/container/images/panel.html new file mode 100644 index 0000000..40dfdd0 --- /dev/null +++ b/zun_ui/static/dashboard/container/images/panel.html @@ -0,0 +1,4 @@ + + + diff --git a/zun_ui/static/dashboard/container/zun.service.js b/zun_ui/static/dashboard/container/zun.service.js index 277febf..9754bc1 100644 --- a/zun_ui/static/dashboard/container/zun.service.js +++ b/zun_ui/static/dashboard/container/zun.service.js @@ -26,6 +26,7 @@ function ZunAPI(apiService, toast, gettext) { var containersPath = '/api/zun/containers/'; + var imagesPath = '/api/zun/images/'; var service = { createContainer: createContainer, getContainer: getContainer, @@ -40,7 +41,9 @@ pauseContainer: pauseContainer, unpauseContainer: unpauseContainer, executeContainer: executeContainer, - killContainer: killContainer + killContainer: killContainer, + pullImage: pullImage, + getImages: getImages }; return service; @@ -126,6 +129,20 @@ return apiService.post(containersPath + id + '/kill', params).error(error(msg)); } + //////////// + // Images // + //////////// + + function pullImage(params) { + var msg = gettext('Unable to pull Image.'); + return apiService.post(imagesPath, params).error(error(msg)); + } + + function getImages() { + var msg = gettext('Unable to retrieve the Images.'); + return apiService.get(imagesPath).error(error(msg)); + } + function error(message) { return function() { toast.add('error', message);