commit d7d59584e7d86b351e5a2273ef99b2fb25675840 Author: Tung Doan Date: Mon May 7 14:02:57 2018 +0200 Update APMEC Horizon code diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..812aed7 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,16 @@ +If you would like to contribute to the development of OpenStack, +you must follow the steps in this page: + + http://docs.openstack.org/infra/manual/developers.html + +Once those steps have been completed, changes to OpenStack +should be submitted for review via the Gerrit tool, following +the workflow documented at: + + http://docs.openstack.org/infra/manual/developers.html#development-workflow + +Pull requests submitted through GitHub will be ignored. + +Bugs should be filed on Launchpad, not GitHub: + + https://bugs.launchpad.net/apmec diff --git a/HACKING.rst b/HACKING.rst new file mode 100644 index 0000000..fc0a270 --- /dev/null +++ b/HACKING.rst @@ -0,0 +1,19 @@ +Apmec Style Commandments +========================= + +- Step 1: Read the OpenStack Style Commandments + http://docs.openstack.org/developer/hacking/ +- Step 2: Read on + +Apmec Specific Commandments +---------------------------- + +- [N320] Validate that LOG messages, except debug ones, have translations + +Creating Unit Tests +------------------- +For every new feature, unit tests should be created that both test and +(implicitly) document the usage of said feature. If submitting a patch for a +bug that had no unit test, a new passing unit test should be added. If a +submitted bug fix does have a unit test, be sure to add a new one that fails +without the patch and passes with the patch. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..b9c0258 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,12 @@ +include AUTHORS +include ChangeLog +include LICENSE +include manage.py +include README.rst + +recursive-include apmec_horizon *.html *.css *.js + +exclude .gitignore +exclude .gitreview + +global-exclude *.pyc diff --git a/README.md b/README.md new file mode 100644 index 0000000..6a7b066 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# apmec-gui \ No newline at end of file diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..163e546 --- /dev/null +++ b/README.rst @@ -0,0 +1,44 @@ +======================== +Team and repository tags +======================== + +.. image:: http://governance.openstack.org/badges/apmec-horizon.svg + :target: http://governance.openstack.org/reference/tags/index.html + +.. Change things from this point on + + +Apmec Horizon UI +================= + +Horizon UI for Apmec MEA Manager + +Installation +============ + +1. Install module + + :: + + sudo python setup.py install + + +2. Copy files to Horizon tree + + :: + + cp apmec_horizon/enabled/* /opt/stack/horizon/openstack_dashboard/enabled/ + + +3. Restart the apache webserver + + :: + + sudo service apache2 restart + + +More Information +================ + +Apmec Wiki: +https://wiki.openstack.org/wiki/Apmec diff --git a/apmec_horizon/__init__.py b/apmec_horizon/__init__.py new file mode 100644 index 0000000..1438744 --- /dev/null +++ b/apmec_horizon/__init__.py @@ -0,0 +1,16 @@ +# Copyright 2015 Brocade Communications System, Inc. +# +# 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. + + +__import__('pkg_resources').declare_namespace(__name__) diff --git a/apmec_horizon/enabled/_80_mec.py b/apmec_horizon/enabled/_80_mec.py new file mode 100644 index 0000000..d11cf41 --- /dev/null +++ b/apmec_horizon/enabled/_80_mec.py @@ -0,0 +1,19 @@ +# Copyright 2017 99Cloud Inc. +# +# 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. + +DASHBOARD = 'mec' +DISABLED = False +ADD_INSTALLED_APPS = [ + 'apmec_horizon.openstack_dashboard.dashboards.mec', +] diff --git a/apmec_horizon/enabled/__init__.py b/apmec_horizon/enabled/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apmec_horizon/openstack_dashboard/__init__.py b/apmec_horizon/openstack_dashboard/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apmec_horizon/openstack_dashboard/api/__init__.py b/apmec_horizon/openstack_dashboard/api/__init__.py new file mode 100644 index 0000000..a88096d --- /dev/null +++ b/apmec_horizon/openstack_dashboard/api/__init__.py @@ -0,0 +1,33 @@ +# Copyright 2015 Brocade Communications System, Inc. +# +# 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. +""" +Methods and interface objects used to interact with external APIs. + +API method calls return objects that are in many cases objects with +attributes that are direct maps to the data returned from the API http call. +Unfortunately, these objects are also often constructed dynamically, making +it difficult to know what data is available from the API object. Because of +this, all API calls should wrap their returned object in one defined here, +using only explicitly defined attributes and/or methods. + +In other words, Horizon developers not working on openstack_dashboard.api +shouldn't need to understand the finer details of APIs for +Keystone/Nova/Glance/Swift et. al. +""" +from apmec_horizon.openstack_dashboard.api import apmec + + +__all__ = [ + "apmec", +] diff --git a/apmec_horizon/openstack_dashboard/api/apmec.py b/apmec_horizon/openstack_dashboard/api/apmec.py new file mode 100644 index 0000000..6b8ee1f --- /dev/null +++ b/apmec_horizon/openstack_dashboard/api/apmec.py @@ -0,0 +1,160 @@ +# Copyright 2015 Brocade Communications System, Inc. +# +# 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 __future__ import absolute_import + +from django.conf import settings +from oslo_log import log as logging +from apmecclient.v1_0 import client as apmec_client + +from horizon.utils.memoized import memoized # noqa +from openstack_dashboard.api import base + + +LOG = logging.getLogger(__name__) + + +@memoized +def apmecclient(request): + insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False) + cacert = getattr(settings, 'OPENSTACK_SSL_CACERT', None) + c = apmec_client.Client( + token=request.user.token.id, + auth_url=base.url_for(request, 'identity'), + endpoint_url=base.url_for(request, 'mec-orchestration'), + insecure=insecure, ca_cert=cacert) + return c + + +def mea_list(request, **params): + LOG.debug("mea_list(): params=%s", params) + meas = apmecclient(request).list_meas(**params).get('meas') + return meas + + +def mead_list(request, **params): + LOG.debug("mead_list(): params=%s", params) + meads = apmecclient(request).list_meads(**params).get('meads') + return meads + + +def create_mead(request, tosca_body=None, **params): + LOG.debug("create_mead(): params=%s", params) + mead_instance = apmecclient(request).create_mead(body=tosca_body) + return mead_instance + + +def create_mea(request, mea_arg, **params): + LOG.debug("create_mea(): mea_arg=%s", str(mea_arg)) + mea_instance = apmecclient(request).create_mea(body=mea_arg) + return mea_instance + + +def get_mead(request, mead_id): + LOG.debug("mead_get(): mead_id=%s", str(mead_id)) + mead = apmecclient(request).show_mead(mead_id) + return mead + + +def get_mea(request, mea_id): + LOG.debug("mea_get(): mea_id=%s", str(mea_id)) + mea_instance = apmecclient(request).show_mea(mea_id) + return mea_instance + + +def delete_mea(request, mea_id): + LOG.debug("delete_mea():mea_id=%s", str(mea_id)) + apmecclient(request).delete_mea(mea_id) + + +def delete_mead(request, mead_id): + LOG.debug("delete_mead():mead_id=%s", str(mead_id)) + apmecclient(request).delete_mead(mead_id) + + +def create_vim(request, vim_arg): + LOG.debug("create_vim(): vim_arg=%s", str(vim_arg)) + vim_instance = apmecclient(request).create_vim(body=vim_arg) + return vim_instance + + +def get_vim(request, vim_id): + LOG.debug("vim_get(): vim_id=%s", str(vim_id)) + vim_instance = apmecclient(request).show_vim(vim_id) + return vim_instance + + +def delete_vim(request, vim_id): + LOG.debug("delete_vim():vim_id=%s", str(vim_id)) + apmecclient(request).delete_vim(vim_id) + + +def vim_list(request, **params): + LOG.debug("vim_list(): params=%s", params) + vims = apmecclient(request).list_vims(**params).get('vims') + return vims + + +def events_list(request, resource_id): + params = {'resource_id': resource_id} + events = apmecclient(request).list_events(**params).get('events') + LOG.debug("events_list() params=%s events=%s l=%s", params, events, + len(events)) + return events + + +def create_mesd(request, tosca_body=None, **params): + LOG.debug("create_mesd(): params=%s", params) + mesd_instance = apmecclient(request).create_mesd(body=tosca_body) + return mesd_instance + + +def mesd_list(request, **params): + LOG.debug("mesd_list(): params=%s", params) + mesds = apmecclient(request).list_mesds(**params).get('mesds') + return mesds + + +def get_mesd(request, mesd_id): + LOG.debug("mesd_get(): mesd_id=%s", str(mesd_id)) + mesd = apmecclient(request).show_mesd(mesd_id) + return mesd + + +def delete_mesd(request, mesd_id): + LOG.debug("delete_mesd():mesd_id=%s", str(mesd_id)) + apmecclient(request).delete_mesd(mesd_id) + + +def get_mes(request, mes_id): + LOG.debug("mes_get(): mes_id=%s", str(mes_id)) + mes_instance = apmecclient(request).show_mes(mes_id) + return mes_instance + + +def delete_mes(request, mes_id): + LOG.debug("delete_mes():mes_id=%s", str(mes_id)) + apmecclient(request).delete_ns(mes_id) + + +def mes_list(request, **params): + LOG.debug("mes_list(): params=%s", params) + mess = apmecclient(request).list_mess(**params).get('mess') + return mess + + +def create_mes(request, mes_arg, **params): + LOG.debug("create_mes(): mes_arg=%s", str(mes_arg)) + mes_instance = apmecclient(request).create_mes(body=mes_arg) + return mes_instance diff --git a/apmec_horizon/openstack_dashboard/dashboards/__init__.py b/apmec_horizon/openstack_dashboard/dashboards/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/__init__.py b/apmec_horizon/openstack_dashboard/dashboards/mec/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/dashboard.py b/apmec_horizon/openstack_dashboard/dashboards/mec/dashboard.py new file mode 100644 index 0000000..dd28d5a --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/dashboard.py @@ -0,0 +1,40 @@ +# Copyright 2015 Brocade Communications System, Inc. +# +# 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 Meamgroup(horizon.PanelGroup): + slug = "mecgroup" + name = _("MEA Management") + panels = ('meacatalog', 'meamanager',) + + +class Mecogroup(horizon.PanelGroup): + slug = "meogroup" + name = _("MEC Orchestration") + panels = ('vim', 'mescatalog', 'mesmanager') + + +class Mec(horizon.Dashboard): + name = _("MEC") + slug = "mec" + panels = (Meamgroup, Mecogroup,) # Add your panels here. + default_panel = 'meacatalog' # Specify the slug of the dashboard's + # default panel. + +horizon.register(Mec) diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/__init__.py b/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/forms.py b/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/forms.py new file mode 100644 index 0000000..28dd9eb --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/forms.py @@ -0,0 +1,107 @@ +# Copyright 2015 Brocade Communications System, Inc. +# +# 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.forms import ValidationError +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon import messages + +from apmec_horizon.openstack_dashboard import api + + +class OnBoardMEA(forms.SelfHandlingForm): + name = forms.CharField(max_length=255, label=_("Name")) + description = forms.CharField(widget=forms.widgets.Textarea( + attrs={'rows': 4}), + label=_("Description"), + required=False) + source_type = forms.ChoiceField( + label=_('TOSCA Template Source'), + required=False, + choices=[('file', _('TOSCA Template File')), + ('raw', _('Direct Input'))], + widget=forms.Select( + attrs={'class': 'switchable', 'data-slug': 'source'})) + + toscal_file = forms.FileField( + label=_("TOSCA Template File"), + help_text=_("A local TOSCA template file to upload."), + widget=forms.FileInput( + attrs={'class': 'switched', 'data-switch-on': 'source', + 'data-source-file': _('TOSCA Template File')}), + required=False) + + direct_input = forms.CharField( + label=_('TOSCA YAML'), + help_text=_('The YAML formatted contents of a TOSCA template.'), + widget=forms.widgets.Textarea( + attrs={'class': 'switched', 'data-switch-on': 'source', + 'data-source-raw': _('TOSCA YAML')}), + required=False) + + def __init__(self, request, *args, **kwargs): + super(OnBoardMEA, self).__init__(request, *args, **kwargs) + + def clean(self): + data = super(OnBoardMEA, self).clean() + + # The key can be missing based on particular upload + # conditions. Code defensively for it here... + toscal_file = data.get('toscal_file', None) + toscal_raw = data.get('direct_input', None) + source_type = data.get("source_type") + if source_type == "file" and not toscal_file: + raise ValidationError( + _("No TOSCA template file selected.")) + if source_type == "raw" and not toscal_raw: + raise ValidationError( + _("No direct input specified.")) + + if toscal_file and not toscal_file.name.endswith(('.yaml', '.csar')): + raise ValidationError(_("Only .yaml or .csar file uploads \ + are supported")) + + try: + if toscal_file: + toscal_str = self.files['toscal_file'].read() + else: + toscal_str = data['direct_input'] + # toscal = yaml.loads(toscal_str) + data['tosca'] = toscal_str + except Exception as e: + msg = _('There was a problem loading the namespace: %s.') % e + raise forms.ValidationError(msg) + + return data + + def handle(self, request, data): + try: + toscal = data['tosca'] + mead_name = data['name'] + mead_description = data['description'] + tosca_arg = {'mead': {'name': mead_name, + 'description': mead_description, + 'attributes': {'mead': toscal}}} + mead_instance = api.apmec.create_mead(request, tosca_arg) + messages.success(request, + _('MEA Catalog entry %s has been created.') % + mead_instance['mead']['name']) + return toscal + except Exception as e: + msg = _('Unable to create TOSCA. %s') + msg %= e.message.split('Failed validating', 1)[0] + exceptions.handle(request, message=msg) + return False diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/panel.py b/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/panel.py new file mode 100644 index 0000000..26000f7 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/panel.py @@ -0,0 +1,27 @@ +# Copyright 2015 Brocade Communications System, Inc. +# +# 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 +from apmec_horizon.openstack_dashboard.dashboards.mec import dashboard + + +class Meacatalog(horizon.Panel): + name = _("MEA Catalog") + slug = "meacatalog" + + +dashboard.Mec.register(Meacatalog) diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/tables.py b/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/tables.py new file mode 100644 index 0000000..5474658 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/tables.py @@ -0,0 +1,72 @@ +# Copyright 2015 Brocade Communications System, Inc. +# +# 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 _ +from django.utils.translation import ungettext_lazy + +from horizon import tables + +from openstack_dashboard import policy +from apmec_horizon.openstack_dashboard import api + + +class MyFilterAction(tables.FilterAction): + name = "myfilter" + + +class DeleteMEAD(policy.PolicyTargetMixin, tables.DeleteAction): + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Delete MEA", + u"Delete MEAs", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Delete MEA", + u"Delete MEAs", + count + ) + + def action(self, request, obj_id): + api.apmec.delete_mead(request, obj_id) + + +class OnBoardMEA(tables.LinkAction): + name = "onboardmea" + verbose_name = _("Onboard MEA") + classes = ("ajax-modal",) + icon = "plus" + url = "horizon:mec:meacatalog:onboardmea" + + +class MEACatalogTable(tables.DataTable): + name = tables.Column('name', + link="horizon:mec:meacatalog:detail", + verbose_name=_("Name")) + description = tables.Column('description', + verbose_name=_("Description")) + services = tables.Column('service types', + verbose_name=_("Service Types")) + id = tables.Column('id', + verbose_name=_("Catalog Id")) + + class Meta(object): + name = "meacatalog" + verbose_name = _("MEACatalog") + table_actions = (OnBoardMEA, DeleteMEAD, MyFilterAction,) diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/tabs.py b/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/tabs.py new file mode 100644 index 0000000..ad57068 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/tabs.py @@ -0,0 +1,122 @@ +# Copyright 2015 Brocade Communications System, Inc. +# +# 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 _ + +from horizon import exceptions +from horizon import tabs + +from apmec_horizon.openstack_dashboard import api +from apmec_horizon.openstack_dashboard.dashboards.mec import utils +from apmec_horizon.openstack_dashboard.dashboards.mec.meacatalog import tables + + +class MEACatalogItem(object): + def __init__(self, name, description, services, mead_id): + self.id = mead_id + self.name = name + self.description = description + self.services = services + + +class MEACatalogTab(tabs.TableTab): + name = _("MEACatalog Tab") + slug = "meacatalog_tab" + table_classes = (tables.MEACatalogTable,) + template_name = ("horizon/common/_detail_table.html") + preload = False + + def has_more_data(self, table): + return self._has_more + + def get_meacatalog_data(self): + try: + # marker = self.request.GET.get( + # tables.MEACatalogTable._meta.pagination_param, None) + + self._has_more = False + catalogs = [] + meads = api.apmec.mead_list(self.request, + template_source="onboarded") + for mead in meads: + s_types = [s_type for s_type in mead['service_types'] + if s_type != 'mead'] + s_types_string = "" + if len(s_types) > 0: + s_types_string = ', '.join( + [str(item) for item in s_types]) + item = MEACatalogItem(mead['name'], + mead['description'], + s_types_string, mead['id']) + catalogs.append(item) + return catalogs + except Exception: + self._has_more = False + error_message = _('Unable to get mea catalogs') + exceptions.handle(self.request, error_message) + + return [] + + +class MEACatalogTabs(tabs.TabGroup): + slug = "meacatalog_tabs" + tabs = (MEACatalogTab,) + sticky = True + + +class TemplateTab(tabs.Tab): + name = _("Template") + slug = "template" + template_name = ("mec/meacatalog/template.html") + + def get_context_data(self, request): + return {'mead': self.tab_group.kwargs['mead']} + + +class MEADEventsTab(tabs.TableTab): + name = _("Events Tab") + slug = "events_tab" + table_classes = (utils.EventsTable,) + template_name = ("horizon/common/_detail_table.html") + preload = False + + def has_more_data(self, table): + return self._has_more + + def get_events_data(self): + try: + self._has_more = True + utils.EventItemList.clear_list() + events = api.apmec.events_list(self.request, + self.tab_group.kwargs['mead_id']) + for event in events: + evt_obj = utils.EventItem( + event['id'], event['resource_state'], + event['event_type'], + event['timestamp'], + event['event_details']) + utils.EventItemList.add_item(evt_obj) + return utils.EventItemList.EVTLIST_P + except Exception as e: + self._has_more = False + error_message = _('Unable to get events %s') % e + exceptions.handle(self.request, error_message) + return [] + + +class MEADDetailTabs(tabs.TabGroup): + slug = "MEAD_details" + tabs = (TemplateTab, MEADEventsTab) + sticky = True diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/templates/meacatalog/_onboardmea.html b/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/templates/meacatalog/_onboardmea.html new file mode 100644 index 0000000..b2bd9b0 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/templates/meacatalog/_onboardmea.html @@ -0,0 +1,9 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block form_attrs %}enctype="multipart/form-data"{% endblock %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Onboards a MEA." %}

+{% endblock %} diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/templates/meacatalog/detail.html b/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/templates/meacatalog/detail.html new file mode 100644 index 0000000..12b135e --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/templates/meacatalog/detail.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "MEAD Details" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=page_title %} +{% endblock page_header %} + +{% block main %} +
+
+ {{ tab_group.render }} +
+
+{% endblock %} + diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/templates/meacatalog/index.html b/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/templates/meacatalog/index.html new file mode 100644 index 0000000..d787808 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/templates/meacatalog/index.html @@ -0,0 +1,17 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "MEA Catalog" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("MEA Catalog") %} +{% endblock page_header %} + +{% block main %} +
+
+ {{ tab_group.render }} +
+
+{% endblock %} + + diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/templates/meacatalog/onboardmea.html b/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/templates/meacatalog/onboardmea.html new file mode 100644 index 0000000..1ca50de --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/templates/meacatalog/onboardmea.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Onboard MEA" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Onboard a MEA") %} +{% endblock page_header %} + +{% block main %} + {% include 'mec/meacatalog/_onboardmea.html' %} +{% endblock %} diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/templates/meacatalog/template.html b/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/templates/meacatalog/template.html new file mode 100644 index 0000000..0fa497a --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/templates/meacatalog/template.html @@ -0,0 +1,5 @@ +{% load i18n %} +

{% trans "MEAD Template" %}

+
+{{ mead.template }}
+
diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/tests.py b/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/tests.py new file mode 100644 index 0000000..2d824b7 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/tests.py @@ -0,0 +1,22 @@ +# Copyright 2015 Brocade Communications System, Inc. +# +# 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 openstack_dashboard.test import helpers as test + + +class MeacatalogTests(test.TestCase): + # Unit tests for meacatalog. + def test_me(self): + self.assertTrue(1 + 1 == 2) diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/urls.py b/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/urls.py new file mode 100644 index 0000000..71364df --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/urls.py @@ -0,0 +1,24 @@ +# Copyright 2015 Brocade Communications System, Inc. +# +# 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 apmec_horizon.openstack_dashboard.dashboards.mec.meacatalog import views + +urlpatterns = [ + url(r'^$', views.IndexView.as_view(), name='index'), + url(r'^onboardmea', views.OnBoardMEAView.as_view(), name='onboardmea'), + url(r'^(?P[^/]+)/$', views.DetailView.as_view(), name='detail'), +] diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/views.py b/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/views.py new file mode 100644 index 0000000..a97d9c9 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/meacatalog/views.py @@ -0,0 +1,112 @@ +# Copyright 2015 Brocade Communications System, Inc. +# +# 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.core.urlresolvers import reverse +from django.core.urlresolvers import reverse_lazy +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon import tabs + +from horizon.utils import memoized + +from openstack_dashboard import api + +from apmec_horizon.openstack_dashboard import api as apmec_api +from apmec_horizon.openstack_dashboard.dashboards.mec.meacatalog \ + import tabs as mec_tabs + +from apmec_horizon.openstack_dashboard.dashboards.mec.meacatalog \ + import forms as project_forms + + +class IndexView(tabs.TabbedTableView): + # A very simple class-based view... + tab_group_class = mec_tabs.MEACatalogTabs + template_name = 'mec/meacatalog/index.html' + + def get_data(self, request, context, *args, **kwargs): + # Add data to the context here... + return context + + +class OnBoardMEAView(forms.ModalFormView): + form_class = project_forms.OnBoardMEA + template_name = 'mec/meacatalog/onboardmea.html' + success_url = reverse_lazy("horizon:mec:meacatalog:index") + modal_id = "onboardmea_modal" + modal_header = _("OnBoard MEA") + submit_label = _("OnBoard MEA") + submit_url = "horizon:mec:meacatalog:onboardmea" + + @memoized.memoized_method + def get_object(self): + try: + return api.nova.server_get(self.request, + self.kwargs["instance_id"]) + except Exception: + exceptions.handle(self.request, + _("Unable to retrieve instance.")) + + def get_initial(self): + # return {"instance_id": self.kwargs["instance_id"]} + return {} + + def get_context_data(self, **kwargs): + context = super(OnBoardMEAView, self).get_context_data(**kwargs) + # instance_id = self.kwargs['instance_id'] + # context['instance_id'] = instance_id + # context['instance'] = self.get_object() + context['submit_url'] = reverse(self.submit_url) + return context + + +class DetailView(tabs.TabView): + tab_group_class = mec_tabs.MEADDetailTabs + template_name = 'mec/meacatalog/detail.html' + redirect_url = 'horizon:mec:meacatalog:index' + page_title = _("MEAD Details: {{ mead_id }}") + + def get_context_data(self, **kwargs): + context = super(DetailView, self).get_context_data(**kwargs) + mead = self.get_data() + context['mead'] = mead + context['mead_id'] = kwargs['mead_id'] + context['url'] = reverse(self.redirect_url) + return context + + @memoized.memoized_method + def get_data(self): + mead_id = self.kwargs['mead_id'] + + try: + template = None + mead = apmec_api.apmec.get_mead(self.request, mead_id) + attributes_json = mead['mead']['attributes'] + template = attributes_json.get('mead', None) + mead['template'] = template + except Exception: + redirect = reverse(self.redirect_url) + exceptions.handle(self.request, + _('Unable to retrieve details for ' + 'MEAD "%s".') % mead_id, + redirect=redirect) + raise exceptions.Http302(redirect) + return mead + + def get_tabs(self, request, *args, **kwargs): + mead = self.get_data() + return self.tab_group_class(request, mead=mead, **kwargs) diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/__init__.py b/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/forms.py b/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/forms.py new file mode 100644 index 0000000..7cb8d2c --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/forms.py @@ -0,0 +1,239 @@ +# Copyright 2015 Brocade Communications System, Inc. +# +# 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. + +import yaml + +from django.forms import ValidationError +from django.utils.translation import ugettext_lazy as _ +from oslo_log import log as logging + +from horizon import exceptions +from horizon import forms +from horizon import messages + +from apmec_horizon.openstack_dashboard import api + +LOG = logging.getLogger(__name__) + + +class DeployMEA(forms.SelfHandlingForm): + mea_name = forms.CharField(max_length=255, label=_("MEA Name")) + description = forms.CharField(widget=forms.widgets.Textarea( + attrs={'rows': 4}), + label=_("Description"), + required=False) + mead_id = forms.ChoiceField(label=_("MEA Catalog Name"), + required=False) + template_source = forms.ChoiceField( + label=_('MEAD template Source'), + required=False, + choices=[('file', _('File')), + ('raw', _('Direct Input'))], + widget=forms.Select( + attrs={'class': 'switchable', 'data-slug': 'template'})) + template_file = forms.FileField( + label=_('MEAD template File'), + help_text=_('MEAD template to create MEA'), + widget=forms.FileInput( + attrs={'class': 'switched', 'data-switch-on': 'template', + 'data-template-file': _('TOSCA Template File')}), + required=False) + template_input = forms.CharField( + label=_('MEAD template'), + help_text=_('The YAML formatted contents of MEAD template.'), + widget=forms.widgets.Textarea( + attrs={'class': 'switched', 'data-switch-on': 'template', + 'data-template-raw': _('MEAD template')}), + required=False) + vim_id = forms.ChoiceField(label=_("VIM Name"), required=False) + region_name = forms.CharField(label=_("Region Name"), required=False) + source_type = forms.ChoiceField( + label=_('Parameter Value Source'), + required=False, + choices=[('file', _('File')), + ('raw', _('Direct Input'))], + widget=forms.Select( + attrs={'class': 'switchable', 'data-slug': 'source'})) + + param_file = forms.FileField( + label=_('Parameter Value File'), + help_text=_('A local Parameter Value file to upload.'), + widget=forms.FileInput( + attrs={'class': 'switched', 'data-switch-on': 'source', + 'data-source-file': _('Parameter Value File')}), + required=False) + + direct_input = forms.CharField( + label=_('Parameter Value YAML'), + help_text=_('The YAML formatted contents of Parameter Values.'), + widget=forms.widgets.Textarea( + attrs={'class': 'switched', 'data-switch-on': 'source', + 'data-source-raw': _('Parameter Values')}), + required=False) + + config_type = forms.ChoiceField( + label=_('Configuration Value Source'), + required=False, + choices=[('file', _('File')), + ('raw', _('Direct Input'))], + widget=forms.Select( + attrs={'class': 'switchable', 'data-slug': 'config'})) + + config_file = forms.FileField( + label=_('Configuration Value File'), + help_text=_('MEA Configuration file with YAML ' + 'formatted contents to upload.'), + widget=forms.FileInput( + attrs={'class': 'switched', 'data-switch-on': 'config', + 'data-config-file': _('Configuration Value File')}), + required=False) + + config_input = forms.CharField( + label=_('Configuration Value YAML'), + help_text=_('YAML formatted MEA configuration text.'), + widget=forms.widgets.Textarea( + attrs={'class': 'switched', 'data-switch-on': 'config', + 'data-config-raw': _('Configuration Values')}), + required=False) + + def __init__(self, request, *args, **kwargs): + super(DeployMEA, self).__init__(request, *args, **kwargs) + + try: + mead_list = api.apmec.mead_list(request, + template_source='onboarded') + available_choices_mead = [(mea['id'], mea['name']) for mea in + mead_list] + except Exception as e: + available_choices_mead = [] + msg = _('Failed to retrieve available MEA Catalog names: %s') % e + LOG.error(msg) + + try: + vim_list = api.apmec.vim_list(request) + available_choices_vims = [(vim['id'], vim['name']) for vim in + vim_list] + + except Exception as e: + available_choices_vims = [] + msg = _('Failed to retrieve available VIM names: %s') % e + LOG.error(msg) + + self.fields['mead_id'].choices = [('', _('Select a MEA Catalog Name')) + ]+available_choices_mead + self.fields['vim_id'].choices = [('', + _('Select a VIM Name')) + ]+available_choices_vims + + def clean(self): + data = super(DeployMEA, self).clean() + + template_file = data.get('template_file', None) + template_raw = data.get('template_input', None) + + if template_raw and template_file: + raise ValidationError( + _("Cannot specify both file and direct input.")) + + if template_file and not template_file.name.endswith('.yaml'): + raise ValidationError( + _("Please upload .yaml file only.")) + + if template_file: + data['mead_template'] = yaml.load(template_file, + Loader=yaml.SafeLoader) + elif template_raw: + data['mead_template'] = yaml.load(data['template_input'], + Loader=yaml.SafeLoader) + else: + data['mead_template'] = None + + param_file = data.get('param_file', None) + param_raw = data.get('direct_input', None) + + if param_raw and param_file: + raise ValidationError( + _("Cannot specify both file and direct input.")) + + if param_file and not param_file.name.endswith('.yaml'): + raise ValidationError( + _("Please upload .yaml file only.")) + + if param_file: + data['param_values'] = self.files['param_file'].read() + elif param_raw: + data['param_values'] = data['direct_input'] + else: + data['param_values'] = None + + config_file = data.get('config_file', None) + config_raw = data.get('config_input', None) + + if config_file and config_raw: + raise ValidationError( + _("Cannot specify both file and direct input.")) + + if config_file and not config_file.name.endswith('.yaml'): + raise ValidationError(_("Only .yaml file uploads supported")) + + if config_file: + data['config_values'] = self.files['config_file'].read() + elif config_raw: + data['config_values'] = data['config_input'] + else: + data['config_values'] = None + + return data + + def handle(self, request, data): + try: + mea_name = data['mea_name'] + description = data['description'] + mead_id = data.get('mead_id') + mead_template = data.get('mead_template') + vim_id = data['vim_id'] + region_name = data['region_name'] + param_val = data['param_values'] + config_val = data['config_values'] + + if (mead_id == '') and (mead_template is None): + raise ValidationError(_("Both MEAD id and template cannot be " + "empty. Please specify one of them")) + + if (mead_id != '') and (mead_template is not None): + raise ValidationError(_("Both MEAD id and template cannot be " + "specified. Please specify any one")) + + mea_arg = {'mea': {'mead_id': mead_id, 'name': mea_name, + 'description': description, + 'vim_id': vim_id, + 'mead_template': mead_template}} + if region_name: + mea_arg.setdefault('placement_attr', {})[ + region_name] = region_name + mea_attr = mea_arg['mea'].setdefault('attributes', {}) + if param_val: + mea_attr['param_values'] = param_val + if config_val: + mea_attr['config'] = config_val + + api.apmec.create_mea(request, mea_arg) + messages.success(request, + _('MEA %s create operation initiated.') % + mea_name) + return True + except Exception as e: + exceptions.handle(request, + _('Failed to create MEA: %s') % + e.message) diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/panel.py b/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/panel.py new file mode 100644 index 0000000..c5af5eb --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/panel.py @@ -0,0 +1,27 @@ +# Copyright 2015 Brocade Communications System, Inc. +# +# 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 +from apmec_horizon.openstack_dashboard.dashboards.mec import dashboard + + +class Meamanager(horizon.Panel): + name = _("MEA Manager") + slug = "meamanager" + + +dashboard.Mec.register(Meamanager) diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/tables.py b/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/tables.py new file mode 100644 index 0000000..ee4ee86 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/tables.py @@ -0,0 +1,261 @@ +# Copyright 2015 Brocade Communications System, Inc. +# +# 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.http import Http404 +from django.utils.translation import pgettext_lazy +from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ungettext_lazy + +from horizon import messages +from horizon import tables + +from openstack_dashboard import policy +from apmec_horizon.openstack_dashboard import api +from apmecclient.common.exceptions import NotFound + + +class MEAManagerItem(object): + def __init__(self, name, description, meas, vim, status, + stack_status, stack_id, error_reason): + self.name = name + self.description = description + self.meas = meas + self.vim = vim + self.status = status + self.stack_status = stack_status + self.id = stack_id + self.error_reason = error_reason + + +class MEAManagerItemList(object): + MEALIST_P = [] + + @classmethod + def get_obj_given_stack_id(cls, mea_id): + for obj in cls.MEALIST_P: + if obj.id == mea_id: + return obj + + @classmethod + def add_item(cls, item): + cls.MEALIST_P.append(item) + + @classmethod + def clear_list(cls): + cls.MEALIST_P = [] + + +class MyFilterAction(tables.FilterAction): + name = "myfilter" + + +class StacksUpdateRow(tables.Row): + ajax = True + + def can_be_selected(self, datum): + return datum.status != 'DELETE_COMPLETE' + + def get_data(self, request, stack_id): + try: + stack = api.heat.stack_get(request, stack_id) + if stack.stack_status == 'DELETE_COMPLETE': + # returning 404 to the ajax call removes the + # row from the table on the ui + raise Http404 + item = MEAManagerItemList.get_obj_given_stack_id(stack_id) + item.status = stack.status + item.stack_status = stack.stack_status + return item + except Http404: + raise + except Exception as e: + messages.error(request, e) + raise + + +class MEAUpdateRow(tables.Row): + ajax = True + + def can_be_selected(self, datum): + return datum.status != 'DELETE_COMPLETE' + + def get_data(self, request, mea_id): + try: + # stack = api.heat.stack_get(request, stack_id) + # if stack.stack_status == 'DELETE_COMPLETE': + # returning 404 to the ajax call removes the + # row from the table on the ui + # raise Http404 + item = MEAManagerItemList.get_obj_given_stack_id(mea_id) + mea_instance = api.apmec.get_mea(request, mea_id) + + if not mea_instance and not item: + # TODO(NAME) - bail with error + return None + + if not mea_instance and item: + # API failure, just keep the current state + return item + + mea = mea_instance['mea'] + try: + mea_services_str = mea['attributes']['service_type'] + except KeyError: + mea_services_str = "" + try: + mea_desc_str = mea['description'] + except KeyError: + mea_desc_str = "" + + vim = mea['placement_attr']['vim_name'] + if not item: + # Add an item entry + item = MEAManagerItem(mea['name'], mea_desc_str, + mea_services_str, str(vim), + mea['status'], mea['status'], mea['id'], + mea['error_reason']) + else: + item.description = mea_desc_str + item.meas = mea_services_str + item.status = mea['status'] + item.stack_status = mea['status'] + return item + except (Http404, NotFound): + raise Http404 + except Exception as e: + messages.error(request, e) + raise + + +class DeleteMEA(policy.PolicyTargetMixin, tables.DeleteAction): + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Terminate MEA", + u"Terminate MEAs", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Terminate MEA", + u"Terminate MEAs", + count + ) + + def action(self, request, obj_id): + api.apmec.delete_mea(request, obj_id) + + +class DeployMEA(tables.LinkAction): + name = "deploymea" + verbose_name = _("Deploy MEA") + classes = ("ajax-modal",) + icon = "plus" + url = "horizon:mec:meamanager:deploymea" + + +class MEAManagerTable(tables.DataTable): + STATUS_CHOICES = ( + ("ACTIVE", True), + ("ERROR", False), + ) + STACK_STATUS_DISPLAY_CHOICES = ( + ("init_in_progress", pgettext_lazy("current status of stack", + u"Init In Progress")), + ("init_complete", pgettext_lazy("current status of stack", + u"Init Complete")), + ("init_failed", pgettext_lazy("current status of stack", + u"Init Failed")), + ("create_in_progress", pgettext_lazy("current status of stack", + u"Create In Progress")), + ("create_complete", pgettext_lazy("current status of stack", + u"Create Complete")), + ("create_failed", pgettext_lazy("current status of stack", + u"Create Failed")), + ("delete_in_progress", pgettext_lazy("current status of stack", + u"Delete In Progress")), + ("delete_complete", pgettext_lazy("current status of stack", + u"Delete Complete")), + ("delete_failed", pgettext_lazy("current status of stack", + u"Delete Failed")), + ("update_in_progress", pgettext_lazy("current status of stack", + u"Update In Progress")), + ("update_complete", pgettext_lazy("current status of stack", + u"Update Complete")), + ("update_failed", pgettext_lazy("current status of stack", + u"Update Failed")), + ("rollback_in_progress", pgettext_lazy("current status of stack", + u"Rollback In Progress")), + ("rollback_complete", pgettext_lazy("current status of stack", + u"Rollback Complete")), + ("rollback_failed", pgettext_lazy("current status of stack", + u"Rollback Failed")), + ("suspend_in_progress", pgettext_lazy("current status of stack", + u"Suspend In Progress")), + ("suspend_complete", pgettext_lazy("current status of stack", + u"Suspend Complete")), + ("suspend_failed", pgettext_lazy("current status of stack", + u"Suspend Failed")), + ("resume_in_progress", pgettext_lazy("current status of stack", + u"Resume In Progress")), + ("resume_complete", pgettext_lazy("current status of stack", + u"Resume Complete")), + ("resume_failed", pgettext_lazy("current status of stack", + u"Resume Failed")), + ("adopt_in_progress", pgettext_lazy("current status of stack", + u"Adopt In Progress")), + ("adopt_complete", pgettext_lazy("current status of stack", + u"Adopt Complete")), + ("adopt_failed", pgettext_lazy("current status of stack", + u"Adopt Failed")), + ("snapshot_in_progress", pgettext_lazy("current status of stack", + u"Snapshot In Progress")), + ("snapshot_complete", pgettext_lazy("current status of stack", + u"Snapshot Complete")), + ("snapshot_failed", pgettext_lazy("current status of stack", + u"Snapshot Failed")), + ("check_in_progress", pgettext_lazy("current status of stack", + u"Check In Progress")), + ("check_complete", pgettext_lazy("current status of stack", + u"Check Complete")), + ("check_failed", pgettext_lazy("current status of stack", + u"Check Failed")), + ) + name = tables.Column("name", + link="horizon:mec:meamanager:detail", + verbose_name=_("MEA Name")) + description = tables.Column("description", + verbose_name=_("Description")) + meas = tables.Column("meas", + verbose_name=_("Deployed Services")) + vim = tables.Column("vim", verbose_name=_("VIM")) + status = tables.Column("status", + hidden=True, + status=True, + status_choices=STATUS_CHOICES) + stack_status = tables.Column("stack_status", + verbose_name=_("Status"), + display_choices=STACK_STATUS_DISPLAY_CHOICES) + error_reason = tables.Column("error_reason", + verbose_name=_("Error Reason")) + + class Meta(object): + name = "meamanager" + verbose_name = _("MEAManager") + status_columns = ["status", ] + row_class = MEAUpdateRow + table_actions = (DeployMEA, DeleteMEA, MyFilterAction,) diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/tabs.py b/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/tabs.py new file mode 100644 index 0000000..e1ad587 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/tabs.py @@ -0,0 +1,124 @@ +# Copyright 2015 Brocade Communications System, Inc. +# +# 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 _ +from horizon import exceptions +from horizon import tabs + +from apmec_horizon.openstack_dashboard import api +from apmec_horizon.openstack_dashboard.dashboards.mec import utils +from apmec_horizon.openstack_dashboard.dashboards.mec.meamanager import tables + + +class MEAManagerTab(tabs.TableTab): + name = _("MEAManager Tab") + slug = "meamanager_tab" + table_classes = (tables.MEAManagerTable,) + template_name = ("horizon/common/_detail_table.html") + preload = False + + def has_more_data(self, table): + return self._has_more + + def get_meamanager_data(self): + try: + # marker = self.request.GET.get( + # tables.MEAManagerTable._meta.pagination_param, None) + + # instances, self._has_more = api.nova.server_list( + # self.request, + # search_opts={'marker': marker, 'paginate': True}) + self._has_more = True + tables.MEAManagerItemList.clear_list() + meas = api.apmec.mea_list(self.request) + for mea in meas: + try: + mea_services_str = mea['attributes']['service_type'] + except KeyError: + mea_services_str = "" + try: + mea_desc_str = mea['description'] + except KeyError: + mea_desc_str = "" + + vim = mea['placement_attr']['vim_name'] + obj = tables.MEAManagerItem( + mea['name'], + mea_desc_str, + mea_services_str, + vim, + mea['status'], + mea['status'], + mea['id'], + mea['error_reason']) + tables.MEAManagerItemList.add_item(obj) + return tables.MEAManagerItemList.MEALIST_P + except Exception: + self._has_more = False + error_message = _('Unable to get instances') + exceptions.handle(self.request, error_message) + + return [] + + +class MEAManagerTabs(tabs.TabGroup): + slug = "meamanager_tabs" + tabs = (MEAManagerTab,) + sticky = True + + +class VDUDetailTab(tabs.Tab): + name = _("VDU Detail") + slug = "VDU_Details" + template_name = "mec/meamanager/vdu_details.html" + + def get_context_data(self, request): + return {'mea': self.tab_group.kwargs['mea']} + + +class MEAEventsTab(tabs.TableTab): + name = _("Events Tab") + slug = "events_tab" + table_classes = (utils.EventsTable,) + template_name = ("horizon/common/_detail_table.html") + preload = False + + def has_more_data(self, table): + return self._has_more + + def get_events_data(self): + try: + self._has_more = True + utils.EventItemList.clear_list() + events = api.apmec.events_list(self.request, + self.tab_group.kwargs['mea_id']) + for event in events: + evt_obj = utils.EventItem( + event['id'], event['resource_state'], + event['event_type'], + event['timestamp'], + event['event_details']) + utils.EventItemList.add_item(evt_obj) + return utils.EventItemList.EVTLIST_P + except Exception as e: + self._has_more = False + error_message = _('Unable to get events %s') % e + exceptions.handle(self.request, error_message) + return [] + + +class MEADetailsTabs(tabs.TabGroup): + slug = "MEA_details" + tabs = (VDUDetailTab, MEAEventsTab) + sticky = True diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/templates/meamanager/_deploy_mea.html b/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/templates/meamanager/_deploy_mea.html new file mode 100644 index 0000000..80a6ac1 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/templates/meamanager/_deploy_mea.html @@ -0,0 +1,15 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block form_attrs %}enctype="multipart/form-data"{% endblock %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% blocktrans %} Deploys a MEA.
+ If the MEAD template is parameterized, + upload a yaml file with values for those parameters.
+ If the MEAD template is not parameterized, any + yaml file uploaded will be ignored.
+ If a configuration yaml file is uploaded, it will be + applied to the MEA post its successful creation.{% endblocktrans %}

+{% endblock %} diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/templates/meamanager/deploy_mea.html b/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/templates/meamanager/deploy_mea.html new file mode 100644 index 0000000..0869997 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/templates/meamanager/deploy_mea.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Deploy MEA" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Deploy a MEA") %} +{% endblock page_header %} + +{% block main %} + {% include 'mec/meamanager/_deploy_mea.html' %} +{% endblock %} diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/templates/meamanager/detail.html b/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/templates/meamanager/detail.html new file mode 100644 index 0000000..e64b829 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/templates/meamanager/detail.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "MEA Details" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=page_title %} +{% endblock page_header %} + +{% block main %} +
+
+ {{ tab_group.render }} +
+
+{% endblock %} + diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/templates/meamanager/index.html b/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/templates/meamanager/index.html new file mode 100644 index 0000000..3e31c9e --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/templates/meamanager/index.html @@ -0,0 +1,17 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "MEA Manager" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("MEA Manager") %} +{% endblock page_header %} + +{% block main %} +
+
+ {{ tab_group.render }} +
+
+{% endblock %} + + diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/templates/meamanager/vdu_details.html b/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/templates/meamanager/vdu_details.html new file mode 100644 index 0000000..28b8886 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/templates/meamanager/vdu_details.html @@ -0,0 +1,59 @@ +{% load i18n %} + + + +

{% trans "MEA information" %}

+
+
+
+
{% trans "Name" %}
+
{{ mea.mea.name }}
+
{% trans "ID" %}
+
{{ mea.mea.id }}
+
{% trans "Tenant ID" %}
+
{{ mea.mea.tenant_id }}
+
{% trans "Description" %}
+
{{ mea.mea.description }}
+
{% trans "Status" %}
+
{{ mea.mea.status|title }}
+
{% trans "Created" %}
+
{{ mea.mea.created_at|parse_isotime }}
+
{% trans "Updated" %}
+
{{ mea.mea.updated_at|parse_isotime }}
+
{% trans "Stack ID" %}
+
{{ mea.mea.instance_id }}
+
{% trans "MEAD ID" %}
+
{{ mea.mea.mead_id }}
+
{% trans "VIM ID" %}
+
{{ mea.mea.vim_id }}
+
{% trans "Error reason" %}
+
{{ mea.mea.error_reason }}
+
+ +

{% trans "Mgmt IP Addresses" %}

+
+
+ {% for key, value in mea.mea.mgmt_url.items %} +
{{ key|title }}
+
{{ value }}
+ {% endfor %} +
+ +

{% trans "Placement Attributes" %}

+
+
+ {% for key, value in mea.mea.placement_attr.items %} +
{{ key|title }}
+
{{ value }}
+ {% endfor %} +
+ +

{% trans "Attributes" %}

+
+
+      {{ mea.mea.attributes }}
+    
+ +
+ + diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/tests.py b/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/tests.py new file mode 100644 index 0000000..a0af3f3 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/tests.py @@ -0,0 +1,22 @@ +# Copyright 2015 Brocade Communications System, Inc. +# +# 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 openstack_dashboard.test import helpers as test + + +class MeamanagerTests(test.TestCase): + # Unit tests for meamanager. + def test_me(self): + self.assertTrue(1 + 1 == 2) diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/urls.py b/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/urls.py new file mode 100644 index 0000000..5404735 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/urls.py @@ -0,0 +1,24 @@ +# Copyright 2015 Brocade Communications System, Inc. +# +# 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 apmec_horizon.openstack_dashboard.dashboards.mec.meamanager import views + +urlpatterns = [ + url(r'^$', views.IndexView.as_view(), name='index'), + url(r'^deploymea$', views.DeployMEAView.as_view(), name='deploymea'), + url(r'^(?P[^/]+)/$', views.DetailView.as_view(), name='detail'), +] diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/views.py b/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/views.py new file mode 100644 index 0000000..5e26cd0 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/meamanager/views.py @@ -0,0 +1,114 @@ +# Copyright 2015 Brocade Communications System, Inc. +# +# 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. + +import json + +from django.core.urlresolvers import reverse +from django.core.urlresolvers import reverse_lazy +from django.utils.translation import ugettext_lazy as _ +from oslo_log import log as logging + +from horizon import exceptions +from horizon import forms +from horizon import tabs +from horizon.utils import memoized + +from apmec_horizon.openstack_dashboard import api as apmec_api +from apmec_horizon.openstack_dashboard.dashboards.mec.meamanager \ + import forms as project_forms + +from apmec_horizon.openstack_dashboard.dashboards.mec.meamanager \ + import tabs as mec_tabs + +LOG = logging.getLogger(__name__) + + +class IndexView(tabs.TabbedTableView): + # A very simple class-based view... + tab_group_class = mec_tabs.MEAManagerTabs + template_name = 'mec/meamanager/index.html' + + def get_data(self, request, context, *args, **kwargs): + # Add data to the context here... + return context + + +class DeployMEAView(forms.ModalFormView): + form_class = project_forms.DeployMEA + template_name = 'mec/meamanager/deploy_mea.html' + success_url = reverse_lazy("horizon:mec:meamanager:index") + modal_id = "deploy_mea_modal" + modal_header = _("Deploy MEA") + submit_label = _("Deploy MEA") + submit_url = "horizon:mec:meamanager:deploymea" + + # @memoized.memoized_method + # def get_object(self): + # try: + # return api.nova.server_get(self.request, + # self.kwargs["instance_id"]) + # except Exception: + # exceptions.handle(self.request, + # _("Unable to retrieve instance.")) + + def get_initial(self): + # return {"instance_id": self.kwargs["instance_id"]} + return {} + + def get_context_data(self, **kwargs): + context = super(DeployMEAView, self).get_context_data(**kwargs) + # instance_id = self.kwargs['instance_id'] + # context['instance_id'] = instance_id + # context['instance'] = self.get_object() + context['submit_url'] = reverse(self.submit_url) + return context + + +class DetailView(tabs.TabView): + tab_group_class = mec_tabs.MEADetailsTabs + template_name = 'mec/meamanager/detail.html' + redirect_url = 'horizon:mec:meamanager:index' + page_title = _("MEA Details: {{ mea_id }}") + + def get_context_data(self, **kwargs): + context = super(DetailView, self).get_context_data(**kwargs) + mea = self.get_data() + context['mea'] = mea + context['mea_id'] = kwargs['mea_id'] + context['url'] = reverse(self.redirect_url) + return context + + @memoized.memoized_method + def get_data(self): + mea_id = self.kwargs['mea_id'] + + try: + mea = apmec_api.apmec.get_mea(self.request, mea_id) + mea["mea"]["mgmt_url"] = json.loads(mea["mea"]["mgmt_url"]) if \ + mea["mea"]["mgmt_url"] else None + return mea + except ValueError as e: + msg = _('Cannot decode json : %s') % e + LOG.error(msg) + except Exception: + redirect = reverse(self.redirect_url) + exceptions.handle(self.request, + _('Unable to retrieve details for ' + 'MEA "%s".') % mea_id, + redirect=redirect) + raise exceptions.Http302(redirect) + + def get_tabs(self, request, *args, **kwargs): + mea = self.get_data() + return self.tab_group_class(request, mea=mea, **kwargs) diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/__init__.py b/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/forms.py b/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/forms.py new file mode 100644 index 0000000..720441c --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/forms.py @@ -0,0 +1,105 @@ +# 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.forms import ValidationError +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon import messages + +from apmec_horizon.openstack_dashboard import api + + +class OnBoardMES(forms.SelfHandlingForm): + name = forms.CharField(max_length=255, label=_("Name")) + description = forms.CharField(widget=forms.widgets.Textarea( + attrs={'rows': 4}), + label=_("Description"), + required=False) + source_type = forms.ChoiceField( + label=_('TOSCA Template Source'), + required=False, + choices=[('file', _('TOSCA Template File')), + ('raw', _('Direct Input'))], + widget=forms.Select( + attrs={'class': 'switchable', 'data-slug': 'source'})) + + toscal_file = forms.FileField( + label=_("TOSCA Template File"), + help_text=_("A local TOSCA template file to upload."), + widget=forms.FileInput( + attrs={'class': 'switched', 'data-switch-on': 'source', + 'data-source-file': _('TOSCA Template File')}), + required=False) + + direct_input = forms.CharField( + label=_('TOSCA YAML'), + help_text=_('The YAML formatted contents of a TOSCA template.'), + widget=forms.widgets.Textarea( + attrs={'class': 'switched', 'data-switch-on': 'source', + 'data-source-raw': _('TOSCA YAML')}), + required=False) + + def __init__(self, request, *args, **kwargs): + super(OnBoardMES, self).__init__(request, *args, **kwargs) + + def clean(self): + data = super(OnBoardMES, self).clean() + + # The key can be missing based on particular upload + # conditions. Code defensively for it here... + toscal_file = data.get('toscal_file', None) + toscal_raw = data.get('direct_input', None) + source_type = data.get("source_type") + if source_type == "file" and not toscal_file: + raise ValidationError( + _("No TOSCA template file selected.")) + if source_type == "raw" and not toscal_raw: + raise ValidationError( + _("No direct input specified.")) + + if toscal_file and not toscal_file.name.endswith(('.yaml', '.csar')): + raise ValidationError(_("Only .yaml or .csar file uploads \ + are supported")) + + try: + if toscal_file: + toscal_str = self.files['toscal_file'].read() + else: + toscal_str = data['direct_input'] + # toscal = yaml.loads(toscal_str) + data['tosca'] = toscal_str + except Exception as e: + msg = _('There was a problem loading the namespace: %s.') % e + raise forms.ValidationError(msg) + + return data + + def handle(self, request, data): + try: + toscal = data['tosca'] + mesd_name = data['name'] + mesd_description = data['description'] + tosca_arg = {'mesd': {'name': mesd_name, + 'description': mesd_description, + 'attributes': {'mesd': toscal}}} + mesd_instance = api.apmec.create_mesd(request, tosca_arg) + messages.success(request, + _('MES Catalog entry %s has been created.') % + mesd_instance['mesd']['name']) + return toscal + except Exception as e: + msg = _('Unable to create TOSCA. %s') + msg %= e.message.split('Failed validating', 1)[0] + exceptions.handle(request, message=msg) + return False diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/panel.py b/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/panel.py new file mode 100644 index 0000000..1beba34 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/panel.py @@ -0,0 +1,25 @@ +# 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 +from apmec_horizon.openstack_dashboard.dashboards.mec import dashboard + + +class Mescatalog(horizon.Panel): + name = _("MES Catalog") + slug = "mescatalog" + + +dashboard.Mec.register(Mescatalog) diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/tables.py b/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/tables.py new file mode 100644 index 0000000..7190f4a --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/tables.py @@ -0,0 +1,68 @@ +# 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 _ +from django.utils.translation import ungettext_lazy + +from horizon import tables + +from openstack_dashboard import policy +from apmec_horizon.openstack_dashboard import api + + +class MyFilterAction(tables.FilterAction): + name = "myfilter" + + +class DeleteMESD(policy.PolicyTargetMixin, tables.DeleteAction): + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Delete MES", + u"Delete MESs", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Delete MES", + u"Delete MESs", + count + ) + + def action(self, request, obj_id): + api.apmec.delete_mesd(request, obj_id) + + +class OnBoardMES(tables.LinkAction): + name = "onboardmes" + verbose_name = _("Onboard MES") + classes = ("ajax-modal",) + icon = "plus" + url = "horizon:mec:mescatalog:onboardmes" + + +class MESCatalogTable(tables.DataTable): + name = tables.Column('name', + link="horizon:mec:mescatalog:detail", + verbose_name=_("Name")) + description = tables.Column('description', + verbose_name=_("Description")) + id = tables.Column('id', + verbose_name=_("Catalog Id")) + + class Meta(object): + name = "mescatalog" + verbose_name = _("MESCatalog") + table_actions = (OnBoardMES, DeleteMESD, MyFilterAction,) diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/tabs.py b/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/tabs.py new file mode 100644 index 0000000..717c5b6 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/tabs.py @@ -0,0 +1,109 @@ +# 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 _ + +from horizon import exceptions +from horizon import tabs + +from apmec_horizon.openstack_dashboard import api +from apmec_horizon.openstack_dashboard.dashboards.mec.mescatalog import tables +from apmec_horizon.openstack_dashboard.dashboards.mec import utils + + +class MESCatalogItem(object): + def __init__(self, name, description, mesd_id): + self.id = mesd_id + self.name = name + self.description = description + + +class MESCatalogTab(tabs.TableTab): + name = _("MESCatalog Tab") + slug = "mescatalog_tab" + table_classes = (tables.MESCatalogTable,) + template_name = ("horizon/common/_detail_table.html") + preload = False + + def has_more_data(self, table): + return self._has_more + + def get_mescatalog_data(self): + try: + self._has_more = False + instances = [] + mesds = api.apmec.mesd_list(self.request) + for mesd in mesds: + item = MESCatalogItem(mesd['name'], + mesd['description'], + mesd['id']) + instances.append(item) + return instances + except Exception: + self._has_more = False + error_message = _('Unable to get instances') + exceptions.handle(self.request, error_message) + + return [] + + +class MESCatalogTabs(tabs.TabGroup): + slug = "mescatalog_tabs" + tabs = (MESCatalogTab,) + sticky = True + + +class TemplateTab(tabs.Tab): + name = _("Template") + slug = "template" + template_name = ("mec/mescatalog/template.html") + + def get_context_data(self, request): + return {'mesd': self.tab_group.kwargs['mesd']} + + +class MESDEventsTab(tabs.TableTab): + name = _("Events Tab") + slug = "events_tab" + table_classes = (utils.EventsTable,) + template_name = ("horizon/common/_detail_table.html") + preload = False + + def has_more_data(self, table): + return self._has_more + + def get_events_data(self): + try: + self._has_more = True + utils.EventItemList.clear_list() + events = api.apmec.events_list(self.request, + self.tab_group.kwargs['mesd_id']) + for event in events: + evt_obj = utils.EventItem( + event['id'], event['resource_state'], + event['event_type'], + event['timestamp'], + event['event_details']) + utils.EventItemList.add_item(evt_obj) + return utils.EventItemList.EVTLIST_P + except Exception as e: + self._has_more = False + error_message = _('Unable to get events %s') % e + exceptions.handle(self.request, error_message) + return [] + + +class MESDDetailTabs(tabs.TabGroup): + slug = "MESD_details" + tabs = (TemplateTab, MESDEventsTab) + sticky = True diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/templates/mescatalog/_onboardmes.html b/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/templates/mescatalog/_onboardmes.html new file mode 100644 index 0000000..e53318a --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/templates/mescatalog/_onboardmes.html @@ -0,0 +1,9 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block form_attrs %}enctype="multipart/form-data"{% endblock %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Onboards a MES." %}

+{% endblock %} diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/templates/mescatalog/detail.html b/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/templates/mescatalog/detail.html new file mode 100644 index 0000000..836f002 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/templates/mescatalog/detail.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "MESD Details" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=page_title %} +{% endblock page_header %} + +{% block main %} +
+
+ {{ tab_group.render }} +
+
+{% endblock %} + diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/templates/mescatalog/index.html b/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/templates/mescatalog/index.html new file mode 100644 index 0000000..55dee91 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/templates/mescatalog/index.html @@ -0,0 +1,17 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "MES Catalog" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("MES Catalog") %} +{% endblock page_header %} + +{% block main %} +
+
+ {{ tab_group.render }} +
+
+{% endblock %} + + diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/templates/mescatalog/onboardmes.html b/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/templates/mescatalog/onboardmes.html new file mode 100644 index 0000000..01c1db4 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/templates/mescatalog/onboardmes.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Onboard MES" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Onboard a MES") %} +{% endblock page_header %} + +{% block main %} + {% include 'mec/mescatalog/_onboardmes.html' %} +{% endblock %} diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/templates/mescatalog/template.html b/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/templates/mescatalog/template.html new file mode 100644 index 0000000..fe8ae44 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/templates/mescatalog/template.html @@ -0,0 +1,5 @@ +{% load i18n %} +

{% trans "MESD Template" %}

+
+{{ mesd.template }}
+
diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/tests.py b/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/tests.py new file mode 100644 index 0000000..39502dd --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/tests.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 openstack_dashboard.test import helpers as test + + +class MescatalogTests(test.TestCase): + # Unit tests for mescatalog. + def test_me(self): + self.assertTrue(1 + 1 == 2) diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/urls.py b/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/urls.py new file mode 100644 index 0000000..86348b9 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/urls.py @@ -0,0 +1,22 @@ +# 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 apmec_horizon.openstack_dashboard.dashboards.mec.mescatalog import views + +urlpatterns = [ + url(r'^$', views.IndexView.as_view(), name='index'), + url(r'^onboardmes', views.OnBoardMESView.as_view(), name='onboardmes'), + url(r'^(?P[^/]+)/$', views.DetailView.as_view(), name='detail'), +] diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/views.py b/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/views.py new file mode 100644 index 0000000..b7ad999 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/mescatalog/views.py @@ -0,0 +1,110 @@ +# 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.core.urlresolvers import reverse +from django.core.urlresolvers import reverse_lazy +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon import tabs + +from horizon.utils import memoized + +from openstack_dashboard import api + +from apmec_horizon.openstack_dashboard import api as apmec_api +from apmec_horizon.openstack_dashboard.dashboards.mec.mescatalog \ + import tabs as mec_tabs + +from apmec_horizon.openstack_dashboard.dashboards.mec.mescatalog \ + import forms as project_forms + + +class IndexView(tabs.TabbedTableView): + # A very simple class-based view... + tab_group_class = mec_tabs.MESCatalogTabs + template_name = 'mec/mescatalog/index.html' + + def get_data(self, request, context, *args, **kwargs): + # Add data to the context here... + return context + + +class OnBoardMESView(forms.ModalFormView): + form_class = project_forms.OnBoardMES + template_name = 'mec/mescatalog/onboardmes.html' + success_url = reverse_lazy("horizon:mec:mescatalog:index") + modal_id = "onboardmes_modal" + modal_header = _("OnBoard MES") + submit_label = _("OnBoard MES") + submit_url = "horizon:mec:mescatalog:onboardmes" + + @memoized.memoized_method + def get_object(self): + try: + return api.nova.server_get(self.request, + self.kwargs["instance_id"]) + except Exception: + exceptions.handle(self.request, + _("Unable to retrieve instance.")) + + def get_initial(self): + # return {"instance_id": self.kwargs["instance_id"]} + return {} + + def get_context_data(self, **kwargs): + context = super(OnBoardMESView, self).get_context_data(**kwargs) + # instance_id = self.kwargs['instance_id'] + # context['instance_id'] = instance_id + # context['instance'] = self.get_object() + context['submit_url'] = reverse(self.submit_url) + return context + + +class DetailView(tabs.TabView): + tab_group_class = mec_tabs.MESDDetailTabs + template_name = 'mec/mescatalog/detail.html' + redirect_url = 'horizon:mec:mescatalog:index' + page_title = _("MESD Details: {{ mesd_id }}") + + def get_context_data(self, **kwargs): + context = super(DetailView, self).get_context_data(**kwargs) + mesd = self.get_data() + context['mesd'] = mesd + context['mesd_id'] = kwargs['mesd_id'] + context['url'] = reverse(self.redirect_url) + return context + + @memoized.memoized_method + def get_data(self): + mesd_id = self.kwargs['mesd_id'] + + try: + template = None + mesd = apmec_api.apmec.get_mesd(self.request, mesd_id) + attributes_json = mesd['mesd']['attributes'] + template = attributes_json.get('mesd', None) + mesd['template'] = template + except Exception: + redirect = reverse(self.redirect_url) + exceptions.handle(self.request, + _('Unable to retrieve details for ' + 'MESD "%s".') % mesd_id, + redirect=redirect) + raise exceptions.Http302(redirect) + return mesd + + def get_tabs(self, request, *args, **kwargs): + mesd = self.get_data() + return self.tab_group_class(request, mesd=mesd, **kwargs) diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/__init__.py b/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/forms.py b/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/forms.py new file mode 100644 index 0000000..4a87a23 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/forms.py @@ -0,0 +1,176 @@ +# 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.forms import ValidationError +from django.utils.translation import ugettext_lazy as _ +from oslo_log import log as logging + +from horizon import exceptions +from horizon import forms +from horizon import messages + +from apmec_horizon.openstack_dashboard import api + +LOG = logging.getLogger(__name__) + + +class DeployMES(forms.SelfHandlingForm): + mes_name = forms.CharField(max_length=255, label=_("MES Name")) + description = forms.CharField(widget=forms.widgets.Textarea( + attrs={'rows': 4}), + label=_("Description"), + required=False) + mesd_id = forms.ChoiceField(label=_("MES Catalog Name")) + vim_id = forms.ChoiceField(label=_("VIM Name"), required=False) + source_type = forms.ChoiceField( + label=_('Parameter Value Source'), + required=False, + choices=[('file', _('File')), + ('raw', _('Direct Input'))], + widget=forms.Select( + attrs={'class': 'switchable', 'data-slug': 'source'})) + + param_file = forms.FileField( + label=_('Parameter Value File'), + help_text=_('A local Parameter Value file to upload.'), + widget=forms.FileInput( + attrs={'class': 'switched', 'data-switch-on': 'source', + 'data-source-file': _('Parameter Value File')}), + required=False) + + direct_input = forms.CharField( + label=_('Parameter Value YAML'), + help_text=_('The YAML formatted contents of Parameter Values.'), + widget=forms.widgets.Textarea( + attrs={'class': 'switched', 'data-switch-on': 'source', + 'data-source-raw': _('Parameter Values')}), + required=False) + + config_type = forms.ChoiceField( + label=_('Configuration Value Source'), + required=False, + choices=[('file', _('File')), + ('raw', _('Direct Input'))], + widget=forms.Select( + attrs={'class': 'switchable', 'data-slug': 'config'})) + + config_file = forms.FileField( + label=_('Configuration Value File'), + help_text=_('MES Configuration file with YAML ' + 'formatted contents to upload.'), + widget=forms.FileInput( + attrs={'class': 'switched', 'data-switch-on': 'config', + 'data-config-file': _('Configuration Value File')}), + required=False) + + config_input = forms.CharField( + label=_('Configuration Value YAML'), + help_text=_('YAML formatted MES configuration text.'), + widget=forms.widgets.Textarea( + attrs={'class': 'switched', 'data-switch-on': 'config', + 'data-config-raw': _('Configuration Values')}), + required=False) + + def __init__(self, request, *args, **kwargs): + super(DeployMES, self).__init__(request, *args, **kwargs) + + try: + mesd_list = api.apmec.mesd_list(request) + available_choices_mesd = [(mes['id'], mes['name']) for mes in + mesd_list] + except Exception as e: + available_choices_mesd = [] + msg = _('Failed to retrieve available MES Catalog names: %s') % e + LOG.error(msg) + + try: + vim_list = api.apmec.vim_list(request) + available_choices_vims = [(vim['id'], vim['name']) for vim in + vim_list] + + except Exception as e: + available_choices_vims = [] + msg = _('Failed to retrieve available VIM names: %s') % e + LOG.error(msg) + + self.fields['mesd_id'].choices = [('', _('Select a MES Catalog Name')) + ]+available_choices_mesd + self.fields['vim_id'].choices = [('', + _('Select a VIM Name')) + ]+available_choices_vims + + def clean(self): + data = super(DeployMES, self).clean() + + param_file = data.get('param_file', None) + param_raw = data.get('direct_input', None) + + if param_raw and param_file: + raise ValidationError( + _("Cannot specify both file and direct input.")) + + if param_file and not param_file.name.endswith('.yaml'): + raise ValidationError( + _("Please upload .yaml file only.")) + + if param_file: + data['param_values'] = self.files['param_file'].read() + elif param_raw: + data['param_values'] = data['direct_input'] + else: + data['param_values'] = None + + config_file = data.get('config_file', None) + config_raw = data.get('config_input', None) + + if config_file and config_raw: + raise ValidationError( + _("Cannot specify both file and direct input.")) + + if config_file and not config_file.name.endswith('.yaml'): + raise ValidationError(_("Only .yaml file uploads supported")) + + if config_file: + data['config_values'] = self.files['config_file'].read() + elif config_raw: + data['config_values'] = data['config_input'] + else: + data['config_values'] = None + + return data + + def handle(self, request, data): + try: + mes_name = data['mes_name'] + description = data['description'] + mesd_id = data['mesd_id'] + vim_id = data['vim_id'] + param_val = data['param_values'] + config_val = data['config_values'] + mes_arg = {'mes': {'mesd_id': mesd_id, 'name': mes_name, + 'description': description, + 'vim_id': vim_id}} + mes_attr = mes_arg['mes'].setdefault('attributes', {}) + if param_val: + mes_attr['param_values'] = param_val + if config_val: + mes_attr['config'] = config_val + + api.apmec.create_mes(request, mes_arg) + messages.success(request, + _('MES %s create operation initiated.') % + mes_name) + return True + except Exception as e: + exceptions.handle(request, + _('Failed to create MES: %s') % + e.message) diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/panel.py b/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/panel.py new file mode 100644 index 0000000..c443854 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/panel.py @@ -0,0 +1,25 @@ +# 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 +from apmec_horizon.openstack_dashboard.dashboards.mec import dashboard + + +class Mesmanager(horizon.Panel): + name = _("MES Manager") + slug = "mesmanager" + + +dashboard.Mec.register(Mesmanager) diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/tables.py b/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/tables.py new file mode 100644 index 0000000..ea55b15 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/tables.py @@ -0,0 +1,159 @@ +# 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.http import Http404 +from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ungettext_lazy + +from horizon import messages +from horizon import tables + +from openstack_dashboard import policy +from apmec_horizon.openstack_dashboard import api +from apmecclient.common.exceptions import NotFound + + +class MESManagerItem(object): + def __init__(self, name, description, vim, status, + mes_id, error_reason): + self.name = name + self.description = description + self.vim = vim + self.status = status + self.id = mes_id + self.error_reason = error_reason + + +class MESManagerItemList(object): + MESLIST_P = [] + + @classmethod + def get_obj_given_stack_ids(cls, mes_id): + for obj in cls.MESLIST_P: + if obj.id == mes_id: + return obj + + @classmethod + def add_item(cls, item): + cls.MESLIST_P.append(item) + + @classmethod + def clear_list(cls): + cls.MESLIST_P = [] + + +class MyFilterAction(tables.FilterAction): + name = "myfilter" + + +class MESUpdateRow(tables.Row): + ajax = True + + def can_be_selected(self, datum): + return datum.status != 'DELETE_COMPLETE' + + def get_data(self, request, mes_id): + try: + # stack = api.heat.stack_get(request, stack_id) + # if stack.stack_status == 'DELETE_COMPLETE': + # returning 404 to the ajax call removes the + # row from the table on the ui + # raise Http404 + item = MESManagerItemList.get_obj_given_stack_ids(mes_id) + mes_instance = api.apmec.get_mes(request, mes_id) + + if not mes_instance and not item: + # TODO(NAME) - bail with error + return None + + if not mes_instance and item: + # API failure, just keep the current state + return item + + mes = mes_instance['mes'] + try: + mes_desc_str = mes['description'] + except KeyError: + mes_desc_str = "" + + vim = mes['vim_id'] + if not item: + # Add an item entry + item = MESManagerItem(mes['name'], mes_desc_str, + str(vim), + mes['status'], mes['id'], + mes['error_reason']) + else: + item.description = mes_desc_str + item.status = mes['status'] + item.id = mes['id'] + return item + except (Http404, NotFound): + raise Http404 + except Exception as e: + messages.error(request, e) + raise + + +class DeleteMES(policy.PolicyTargetMixin, tables.DeleteAction): + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Terminate MES", + u"Terminate MESs", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Terminate MES", + u"Terminate MESs", + count + ) + + def action(self, request, obj_id): + api.apmec.delete_mes(request, obj_id) + + +class DeployMES(tables.LinkAction): + name = "deploymes" + verbose_name = _("Deploy MES") + classes = ("ajax-modal",) + icon = "plus" + url = "horizon:mec:mesmanager:deploymes" + + +class MESManagerTable(tables.DataTable): + STATUS_CHOICES = ( + ("ACTIVE", True), + ("ERROR", False), + ) + name = tables.Column("name", + link="horizon:mec:mesmanager:detail", + verbose_name=_("MES Name")) + description = tables.Column("description", + verbose_name=_("Description")) + vim = tables.Column("vim", verbose_name=_("VIM")) + status = tables.Column("status", + status=True, + status_choices=STATUS_CHOICES) + error_reason = tables.Column("error_reason", + verbose_name=_("Error Reason")) + + class Meta(object): + name = "mesmanager" + verbose_name = _("MESManager") + status_columns = ["status", ] + row_class = MESUpdateRow + table_actions = (DeployMES, DeleteMES, MyFilterAction,) diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/tabs.py b/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/tabs.py new file mode 100644 index 0000000..bc81eca --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/tabs.py @@ -0,0 +1,101 @@ +# 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 _ +from horizon import exceptions +from horizon import tabs + +from apmec_horizon.openstack_dashboard import api +from apmec_horizon.openstack_dashboard.dashboards.mec.mesmanager import tables +from apmec_horizon.openstack_dashboard.dashboards.mec import utils + + +class MESManagerTab(tabs.TableTab): + name = _("MESManager Tab") + slug = "mesmanager_tab" + table_classes = (tables.MESManagerTable,) + template_name = ("horizon/common/_detail_table.html") + preload = False + + def has_more_data(self, table): + return self._has_more + + def get_mesmanager_data(self): + try: + self._has_more = True + tables.MESManagerItemList.clear_list() + mess = api.apmec.mes_list(self.request) + for mes in mess: + try: + mes_desc_str = mes['description'] + except KeyError: + mes_desc_str = "" + + vim = mes['vim_id'] + obj = tables.MESManagerItem( + mes['name'], + mes_desc_str, + vim, + mes['status'], + mes['id'], + mes['error_reason']) + tables.MESManagerItemList.add_item(obj) + return tables.MESManagerItemList.MESLIST_P + except Exception: + self._has_more = False + error_message = _('Unable to get instances') + exceptions.handle(self.request, error_message) + + return [] + + +class MESManagerTabs(tabs.TabGroup): + slug = "mesmanager_tabs" + tabs = (MESManagerTab,) + sticky = True + + +class MESEventsTab(tabs.TableTab): + name = _("Events Tab") + slug = "events_tab" + table_classes = (utils.EventsTable,) + template_name = ("horizon/common/_detail_table.html") + preload = False + + def has_more_data(self, table): + return self._has_more + + def get_events_data(self): + try: + self._has_more = True + utils.EventItemList.clear_list() + events = api.apmec.events_list(self.request, + self.tab_group.kwargs['mes_id']) + for event in events: + evt_obj = utils.EventItem( + event['id'], event['resource_state'], + event['event_type'], + event['timestamp'], + event['event_details']) + utils.EventItemList.add_item(evt_obj) + return utils.EventItemList.EVTLIST_P + except Exception as e: + self._has_more = False + error_message = _('Unable to get events %s') % e + exceptions.handle(self.request, error_message) + return [] + + +class MESDetailsTabs(tabs.TabGroup): + slug = "MES_details" + tabs = (MESEventsTab,) + sticky = True diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/templates/nsmanager/_deploy_mes.html b/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/templates/nsmanager/_deploy_mes.html new file mode 100644 index 0000000..8de12a5 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/templates/nsmanager/_deploy_mes.html @@ -0,0 +1,15 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block form_attrs %}enctype="multipart/form-data"{% endblock %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% blocktrans %} Deploys a MES.
+ If the MESD template is parameterized, + upload a yaml file with values for those parameters.
+ If the MESD template is not parameterized, any + yaml file uploaded will be ignored.
+ If a configuration yaml file is uploaded, it will be + applied to the MES post its successful creation.{% endblocktrans %}

+{% endblock %} diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/templates/nsmanager/deploy_mes.html b/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/templates/nsmanager/deploy_mes.html new file mode 100644 index 0000000..424f44d --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/templates/nsmanager/deploy_mes.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Deploy MES" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Deploy a MES") %} +{% endblock page_header %} + +{% block main %} + {% include 'mec/mesmanager/_deploy_mes.html' %} +{% endblock %} diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/templates/nsmanager/detail.html b/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/templates/nsmanager/detail.html new file mode 100644 index 0000000..6101c86 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/templates/nsmanager/detail.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "MES Details" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=page_title %} +{% endblock page_header %} + +{% block main %} +
+
+ {{ tab_group.render }} +
+
+{% endblock %} + diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/templates/nsmanager/index.html b/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/templates/nsmanager/index.html new file mode 100644 index 0000000..35981ce --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/templates/nsmanager/index.html @@ -0,0 +1,17 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "MES Manager" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("MES Manager") %} +{% endblock page_header %} + +{% block main %} +
+
+ {{ tab_group.render }} +
+
+{% endblock %} + + diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/tests.py b/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/tests.py new file mode 100644 index 0000000..b30a7a4 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/tests.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 openstack_dashboard.test import helpers as test + + +class MesmanagerTests(test.TestCase): + def test_me(self): + self.assertTrue(1 + 1 == 2) diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/urls.py b/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/urls.py new file mode 100644 index 0000000..a9cdc4f --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/urls.py @@ -0,0 +1,22 @@ +# 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 apmec_horizon.openstack_dashboard.dashboards.mec.mesmanager import views + +urlpatterns = [ + url(r'^$', views.IndexView.as_view(), name='index'), + url(r'^deploymes$', views.DeployMESView.as_view(), name='deploymes'), + url(r'^(?P[^/]+)/$', views.DetailView.as_view(), name='detail'), +] diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/views.py b/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/views.py new file mode 100644 index 0000000..c064f78 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/mesmanager/views.py @@ -0,0 +1,99 @@ +# 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.core.urlresolvers import reverse +from django.core.urlresolvers import reverse_lazy +from django.utils.translation import ugettext_lazy as _ +from oslo_log import log as logging + +from horizon import exceptions +from horizon import forms +from horizon import tabs +from horizon.utils import memoized + +from apmec_horizon.openstack_dashboard import api as apmec_api +from apmec_horizon.openstack_dashboard.dashboards.mec.mesmanager \ + import forms as project_forms + +from apmec_horizon.openstack_dashboard.dashboards.mec.mesmanager \ + import tabs as mec_tabs + +LOG = logging.getLogger(__name__) + + +class IndexView(tabs.TabbedTableView): + # A very simple class-based view... + tab_group_class = mec_tabs.MESManagerTabs + template_name = 'mec/mesmanager/index.html' + + def get_data(self, request, context, *args, **kwargs): + # Add data to the context here... + return context + + +class DeployMESView(forms.ModalFormView): + form_class = project_forms.DeployMES + template_name = 'mec/mesmanager/deploy_mes.html' + success_url = reverse_lazy("horizon:mec:mesmanager:index") + modal_id = "deploy_mes_modal" + modal_header = _("Deploy MES") + submit_label = _("Deploy MES") + submit_url = "horizon:mec:mesmanager:deploymes" + + def get_initial(self): + # return {"instance_id": self.kwargs["instance_id"]} + return {} + + def get_context_data(self, **kwargs): + context = super(DeployMESView, self).get_context_data(**kwargs) + # instance_id = self.kwargs['instance_id'] + # context['instance_id'] = instance_id + # context['instance'] = self.get_object() + context['submit_url'] = reverse(self.submit_url) + return context + + +class DetailView(tabs.TabView): + tab_group_class = mec_tabs.MESDetailsTabs + template_name = 'mec/mesmanager/detail.html' + redirect_url = 'horizon:mec:mesmanager:index' + page_title = _("MES Details: {{ mes_id }}") + + def get_context_data(self, **kwargs): + context = super(DetailView, self).get_context_data(**kwargs) + mes = self.get_data() + context['mes'] = mes + context['mes_id'] = kwargs['mes_id'] + context['url'] = reverse(self.redirect_url) + return context + + @memoized.memoized_method + def get_data(self): + mes_id = self.kwargs['mes_id'] + + try: + mes = apmec_api.apmec.get_mes(self.request, mes_id) + return mes + except ValueError as e: + msg = _('Cannot decode json : %s') % e + LOG.error(msg) + except Exception: + redirect = reverse(self.redirect_url) + exceptions.handle(self.request, + _('Unable to retrieve details for ' + 'MES "%s".') % mes_id, + redirect=redirect) + raise exceptions.Http302(redirect) + + def get_tabs(self, request, *args, **kwargs): + mes = self.get_data() + return self.tab_group_class(request, mes=mes, **kwargs) diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/static/mec/css/mec.css b/apmec_horizon/openstack_dashboard/dashboards/mec/static/mec/css/mec.css new file mode 100644 index 0000000..ca80678 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/static/mec/css/mec.css @@ -0,0 +1 @@ +/* Additional CSS for mec. */ diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/static/mec/js/mec.js b/apmec_horizon/openstack_dashboard/dashboards/mec/static/mec/js/mec.js new file mode 100644 index 0000000..77a18bc --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/static/mec/js/mec.js @@ -0,0 +1 @@ +/* Additional JavaScript for mec. */ diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/templates/mec/base.html b/apmec_horizon/openstack_dashboard/dashboards/mec/templates/mec/base.html new file mode 100644 index 0000000..3aceb60 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/templates/mec/base.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} + +{% block sidebar %} + {% include 'horizon/common/_sidebar.html' %} +{% endblock %} + +{% block main %} + {% include "horizon/_messages.html" %} + {% block mec_main %}{% endblock %} +{% endblock %} + diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/utils.py b/apmec_horizon/openstack_dashboard/dashboards/mec/utils.py new file mode 100644 index 0000000..cd9497f --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/utils.py @@ -0,0 +1,54 @@ +# Copyright 2016 Brocade Communications System, Inc. +# +# 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 _ + +from horizon import tables + + +class EventItem(object): + def __init__(self, id, state, type, timestamp, details): + self.id = id + self.resource_state = state + self.event_type = type + self.timestamp = timestamp + self.event_details = details + + +class EventItemList(object): + EVTLIST_P = [] + + @classmethod + def add_item(cls, item): + cls.EVTLIST_P.append(item) + + @classmethod + def clear_list(cls): + cls.EVTLIST_P = [] + + +class EventsTable(tables.DataTable): + + id = tables.Column('id', verbose_name=_("Event ID")) + resource_state = tables.Column('resource_state', + verbose_name=_("Resource State")) + timestamp = tables.Column('timestamp', + verbose_name=_("Time Since Event")) + event_type = tables.Column("event_type", verbose_name=_("Event Type")) + event_details = tables.Column("event_details", + verbose_name=_("Event Details")) + + class Meta(object): + name = "events" diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/vim/__init__.py b/apmec_horizon/openstack_dashboard/dashboards/mec/vim/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/vim/forms.py b/apmec_horizon/openstack_dashboard/dashboards/mec/vim/forms.py new file mode 100644 index 0000000..fb6d5b6 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/vim/forms.py @@ -0,0 +1,89 @@ +# Copyright 2016 Brocade Communications System, Inc. +# +# 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 _ +from django.views.decorators.debug import sensitive_variables + +from horizon import exceptions +from horizon import forms +from horizon import messages + +from apmec_horizon.openstack_dashboard import api + + +class RegisterVim(forms.SelfHandlingForm): + vim_name = forms.CharField(max_length=255, label=_("Name")) + vim_description = forms.CharField(widget=forms.widgets.Textarea( + attrs={'rows': 4}), + label=_("Description"), + required=False) + auth_url = forms.URLField(label=_("Auth URL")) + username = forms.CharField(max_length=80, label=_("Username")) + password = forms.CharField(label=_("Password"), + widget=forms.PasswordInput(render_value=False)) + project_name = forms.CharField(max_length=80, label=_("Project Name")) + domain_name = forms.CharField(max_length=80, label=_("Domain Name"), + help_text=_('Applicable for OpenStack site ' + 'running keystone v3. Run ' + 'openstack domain list from ' + 'CLI to find domain name'), + required=False) + is_default = forms.BooleanField( + label=_("Default"), + initial=False, + required=False, + widget=forms.CheckboxInput( + attrs={ + 'class': 'switched', + } + ) + ) + + def __init__(self, request, *args, **kwargs): + super(RegisterVim, self).__init__(request, *args, **kwargs) + + def clean(self): + data = super(RegisterVim, self).clean() + return data + + @sensitive_variables('data', 'password') + def handle(self, request, data): + try: + vim_name = data['vim_name'] + description = data['vim_description'] + password = data['password'] + username = data['username'] + project_name = data['project_name'] + is_default = data['is_default'] + auth_url = data['auth_url'] + vim_type = 'openstack' + domain_name = data['domain_name'] + vim_arg = {'vim': {'name': vim_name, 'description': description, + 'type': vim_type, 'auth_url': auth_url, + 'auth_cred': {'username': username, + 'password': password, + 'user_domain_name': domain_name}, + 'vim_project': {'name': project_name, + 'project_domain_name': + domain_name}, + 'is_default': is_default}} + api.apmec.create_vim(request, vim_arg) + messages.success(request, + _('VIM %s create operation initiated.') % + vim_name) + return True + except Exception as e: + exceptions.handle(request, + _('Failed to register VIM: %s') % + e.message) diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/vim/panel.py b/apmec_horizon/openstack_dashboard/dashboards/mec/vim/panel.py new file mode 100644 index 0000000..4563235 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/vim/panel.py @@ -0,0 +1,27 @@ +# Copyright 2016 Brocade Communications System, Inc. +# +# 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 +from apmec_horizon.openstack_dashboard.dashboards.mec import dashboard + + +class Vimmanager(horizon.Panel): + name = _("VIM Management") + slug = "vim" + + +dashboard.Mec.register(Vimmanager) diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/vim/tables.py b/apmec_horizon/openstack_dashboard/dashboards/mec/vim/tables.py new file mode 100644 index 0000000..cc385ce --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/vim/tables.py @@ -0,0 +1,72 @@ +# Copyright 2016 Brocade Communications System, Inc. +# +# 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 _ +from django.utils.translation import ungettext_lazy + +from horizon import tables + +from openstack_dashboard import policy +from apmec_horizon.openstack_dashboard import api + + +class MyFilterAction(tables.FilterAction): + name = "myfilter" + + +class DeleteVIMLink(policy.PolicyTargetMixin, tables.DeleteAction): + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Delete VIM", + u"Delete VIMs", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Delete VIM", + u"Delete VIMs", + count + ) + + def action(self, request, obj_id): + api.apmec.delete_vim(request, obj_id) + + +class RegisterVIMLink(tables.LinkAction): + name = "registervim" + verbose_name = _("Register VIM") + classes = ("ajax-modal",) + icon = "plus" + url = "horizon:mec:vim:registervim" + + +class VIMTable(tables.DataTable): + name = tables.Column('name', verbose_name=_("Name"), + link="horizon:mec:vim:detail",) + description = tables.Column('description', verbose_name=_("Description")) + id = tables.Column('id', verbose_name=_("VIM Id")) + auth_url = tables.Column('auth_url', verbose_name=_("Auth URL")) + regions = tables.Column('regions', verbose_name=_("Regions")) + user = tables.Column('user', verbose_name=_("User")) + project = tables.Column('project', verbose_name=_("Project")) + status = tables.Column('status', verbose_name=_("Status")) + + class Meta(object): + name = "vim" + verbose_name = _("VIM") + table_actions = (RegisterVIMLink, DeleteVIMLink, MyFilterAction,) diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/vim/tabs.py b/apmec_horizon/openstack_dashboard/dashboards/mec/vim/tabs.py new file mode 100644 index 0000000..2034805 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/vim/tabs.py @@ -0,0 +1,120 @@ +# Copyright 2016 Brocade Communications System, Inc. +# +# 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 _ + +from horizon import exceptions +from horizon import tabs + +from apmec_horizon.openstack_dashboard import api +from apmec_horizon.openstack_dashboard.dashboards.mec import utils # noqa +from apmec_horizon.openstack_dashboard.dashboards.mec.vim import tables + + +class VIMItem(object): + def __init__(self, name, description, regions, vim_id, auth_url, + user, project, status): + self.id = vim_id + self.name = name + self.description = description + self.regions = regions + self.auth_url = auth_url + self.user = user + self.project = project + self.status = status + + +class VIMTab(tabs.TableTab): + name = _("VIM Tab") + slug = "vim_tab" + table_classes = (tables.VIMTable,) + template_name = ("horizon/common/_detail_table.html") + preload = False + + def has_more_data(self, table): + return self._has_more + + def get_vim_data(self): + try: + self._has_more = False + instances = [] + vims = api.apmec.vim_list(self.request) + for vim in vims: + auth_cred = vim['auth_cred'] + placement_attr = vim['placement_attr'] + vim_regions = ','.join(placement_attr['regions']) + user = auth_cred['username'] if auth_cred[ + 'username'] else auth_cred['user_id'] + project_info = vim['vim_project'] + project = project_info['name'] if project_info[ + 'name'] else project_info['id'] + status = vim["status"] + item = VIMItem(name=vim.get('name', ''), + description=vim.get('description', ''), + regions=vim_regions, + vim_id=vim.get('id', ''), + auth_url=vim.get('auth_url', ''), + user=user, project=project, status=status) + instances.append(item) + return instances + except Exception: + self._has_more = False + error_message = _('Unable to fetch vim list') + exceptions.handle(self.request, error_message) + + return [] + + +class VIMEventsTab(tabs.TableTab): + name = _("Events Tab") + slug = "events_tab" + table_classes = (utils.EventsTable,) + template_name = ("horizon/common/_detail_table.html") + preload = False + + def has_more_data(self, table): + return self._has_more + + def get_events_data(self): + try: + self._has_more = True + utils.EventItemList.clear_list() + events = api.apmec.events_list(self.request, + self.tab_group.kwargs['vim_id']) + for event in events: + evt_obj = utils.EventItem( + event['id'], event['resource_state'], + event['event_type'], + event['timestamp'], + event['event_details']) + utils.EventItemList.add_item(evt_obj) + return utils.EventItemList.EVTLIST_P + except Exception as e: + self._has_more = False + error_message = _('Unable to get events %s') % e + exceptions.handle(self.request, error_message) + return [] + + +class VIMTabs(tabs.TabGroup): + slug = "vim_tabs" + tabs = (VIMTab,) + sticky = True + + +class VIMDetailsTabs(tabs.TabGroup): + slug = "VIM_details" + tabs = (VIMEventsTab,) + sticky = True diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/vim/templates/vim/_registervim.html b/apmec_horizon/openstack_dashboard/dashboards/mec/vim/templates/vim/_registervim.html new file mode 100644 index 0000000..1b3d5e7 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/vim/templates/vim/_registervim.html @@ -0,0 +1,9 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block form_attrs %}enctype="form-data"{% endblock %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Registers a VIM." %}

+{% endblock %} diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/vim/templates/vim/detail.html b/apmec_horizon/openstack_dashboard/dashboards/mec/vim/templates/vim/detail.html new file mode 100644 index 0000000..0ad2884 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/vim/templates/vim/detail.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "VIM Event Details" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=page_title %} +{% endblock page_header %} + +{% block main %} +
+
+ {{ tab_group.render }} +
+
+{% endblock %} + diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/vim/templates/vim/index.html b/apmec_horizon/openstack_dashboard/dashboards/mec/vim/templates/vim/index.html new file mode 100644 index 0000000..66c9b25 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/vim/templates/vim/index.html @@ -0,0 +1,17 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "VIM" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("VIM Management") %} +{% endblock page_header %} + +{% block main %} +
+
+ {{ tab_group.render }} +
+
+{% endblock %} + + diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/vim/templates/vim/registervim.html b/apmec_horizon/openstack_dashboard/dashboards/mec/vim/templates/vim/registervim.html new file mode 100644 index 0000000..69542f4 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/vim/templates/vim/registervim.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Register VIM" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Register VIM") %} +{% endblock page_header %} + +{% block main %} + {% include 'mec/vim/_registervim.html' %} +{% endblock %} diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/vim/tests.py b/apmec_horizon/openstack_dashboard/dashboards/mec/vim/tests.py new file mode 100644 index 0000000..178c383 --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/vim/tests.py @@ -0,0 +1,22 @@ +# Copyright 2016 Brocade Communications System, Inc. +# +# 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 openstack_dashboard.test import helpers as test + + +class VimmanagerTests(test.TestCase): + # Unit tests for vimmanager. + def test_me(self): + self.assertTrue(1 + 1 == 2) diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/vim/urls.py b/apmec_horizon/openstack_dashboard/dashboards/mec/vim/urls.py new file mode 100644 index 0000000..0cc5afe --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/vim/urls.py @@ -0,0 +1,24 @@ +# Copyright 2016 Brocade Communications System, Inc. +# +# 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 apmec_horizon.openstack_dashboard.dashboards.mec.vim import views + +urlpatterns = [ + url(r'^$', views.IndexView.as_view(), name='index'), + url(r'^registervim$', views.RegisterVIMView.as_view(), name='registervim'), + url(r'^(?P[^/]+)/$', views.DetailView.as_view(), name='detail'), +] diff --git a/apmec_horizon/openstack_dashboard/dashboards/mec/vim/views.py b/apmec_horizon/openstack_dashboard/dashboards/mec/vim/views.py new file mode 100644 index 0000000..9698edc --- /dev/null +++ b/apmec_horizon/openstack_dashboard/dashboards/mec/vim/views.py @@ -0,0 +1,89 @@ +# Copyright 2016 Brocade Communications System, Inc. +# +# 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.core.urlresolvers import reverse +from django.core.urlresolvers import reverse_lazy +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon import tabs +from horizon.utils import memoized + +from apmec_horizon.openstack_dashboard import api as apmec_api +from apmec_horizon.openstack_dashboard.dashboards.mec.vim \ + import forms as project_forms + +from apmec_horizon.openstack_dashboard.dashboards.mec.vim \ + import tabs as vim_tabs + + +class IndexView(tabs.TabbedTableView): + # A very simple class-based view... + tab_group_class = vim_tabs.VIMTabs + template_name = 'mec/vim/index.html' + + def get_data(self, request, context, *args, **kwargs): + # Add data to the context here... + return context + + +class RegisterVIMView(forms.ModalFormView): + form_class = project_forms.RegisterVim + template_name = 'mec/vim/registervim.html' + success_url = reverse_lazy("horizon:mec:vim:index") + modal_id = "add_service_modal" + modal_header = _("Register VIM") + submit_label = _("Register VIM") + submit_url = "horizon:mec:vim:registervim" + + def get_context_data(self, **kwargs): + context = super(RegisterVIMView, self).get_context_data(**kwargs) + context['submit_url'] = reverse(self.submit_url) + return context + + +class DetailView(tabs.TabView): + tab_group_class = vim_tabs.VIMDetailsTabs + template_name = 'mec/vim/detail.html' + redirect_url = 'horizon:mec:vim:index' + page_title = _("VIM Event Details: {{ vim_id }}") + + def get_context_data(self, **kwargs): + context = super(DetailView, self).get_context_data(**kwargs) + vim = self.get_data() + context['vim'] = vim + context['vim_id'] = kwargs['vim_id'] + context['url'] = reverse(self.redirect_url) + return context + + @memoized.memoized_method + def get_data(self): + vim_id = self.kwargs['vim_id'] + + try: + vim = apmec_api.apmec.get_vim(self.request, vim_id) + return vim + except Exception: + redirect = reverse(self.redirect_url) + exceptions.handle(self.request, + _('Unable to retrieve details for ' + 'VIM "%s".') % vim_id, + redirect=redirect) + raise exceptions.Http302(redirect) + + def get_tabs(self, request, *args, **kwargs): + vim = self.get_data() + return self.tab_group_class(request, vim=vim, **kwargs) diff --git a/apmec_horizon/test/__init__.py b/apmec_horizon/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apmec_horizon/test/settings.py b/apmec_horizon/test/settings.py new file mode 100644 index 0000000..57db501 --- /dev/null +++ b/apmec_horizon/test/settings.py @@ -0,0 +1,58 @@ +# Copyright (C) 2015 Yahoo! Inc. All Rights Reserved. +# +# 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. + +import os + +from horizon.test.settings import * # noqa +from horizon.utils import secret_key as secret_key_utils +from openstack_dashboard.test.settings import * # noqa + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +TEST_DIR = os.path.dirname(os.path.abspath(__file__)) +ROOT_PATH = os.path.abspath(os.path.join(TEST_DIR, "..")) + +MEDIA_ROOT = os.path.abspath(os.path.join(ROOT_PATH, '..', 'media')) +MEDIA_URL = '/media/' +STATIC_ROOT = os.path.abspath(os.path.join(ROOT_PATH, '..', 'static')) +STATIC_URL = '/static/' + +SECRET_KEY = secret_key_utils.generate_or_read_from_file( + os.path.join(TEST_DIR, '.secret_key_store')) +ROOT_URLCONF = 'apmec_horizon.test.urls' +TEMPLATE_DIRS = ( + os.path.join(TEST_DIR, 'templates'), +) + +INSTALLED_APPS = ( + 'django.contrib.contenttypes', + 'django.contrib.auth', + 'django.contrib.sessions', + 'django.contrib.staticfiles', + 'django.contrib.messages', + 'django.contrib.humanize', + 'django_nose', + 'openstack_auth', + 'compressor', + 'horizon', + 'openstack_dashboard', + 'apmec_horizon', +) + +NOSE_ARGS = ['--nocapture', + '--nologcapture', + '--cover-package=apmec_horizon', + '--cover-inclusive', + '--all-modules'] diff --git a/apmec_horizon/test/urls.py b/apmec_horizon/test/urls.py new file mode 100644 index 0000000..70fc889 --- /dev/null +++ b/apmec_horizon/test/urls.py @@ -0,0 +1,21 @@ +# Copyright (C) 2015 Yahoo! Inc. All Rights Reserved. +# +# 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 urls + +import openstack_dashboard.urls + +urlpatterns = [ + urls.url(r'', urls.include(openstack_dashboard.urls)) +] diff --git a/babel-django.cfg b/babel-django.cfg new file mode 100644 index 0000000..e78d6c0 --- /dev/null +++ b/babel-django.cfg @@ -0,0 +1,5 @@ +[extractors] +django = django_babel.extract:extract_django + +[python: **.py] +[django: **/templates/**.html] diff --git a/babel-djangojs.cfg b/babel-djangojs.cfg new file mode 100644 index 0000000..a8273b6 --- /dev/null +++ b/babel-djangojs.cfg @@ -0,0 +1,14 @@ +[extractors] +# We use a custom extractor to find translatable strings in AngularJS +# templates. The extractor is included in horizon.utils for now. +# See http://babel.pocoo.org/docs/messages/#referencing-extraction-methods for +# details on how this works. +angular = horizon.utils.babel_extract_angular:extract_angular + +[javascript: **.js] + +# We need to look into all static folders for HTML files. +# The **/static ensures that we also search within +# /openstack_dashboard/dashboards/XYZ/static which will ensure +# that plugins are also translated. +[angular: **/static/**.html] diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..df691be --- /dev/null +++ b/manage.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python + +# 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. + +import os +import sys + +from django.core.management import execute_from_command_line + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", + "openstack_dashboard.settings") + execute_from_command_line(sys.argv) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3ee6d47 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,11 @@ +# The order of packages is significant, because pip processes them in the order +# of appearance. Changing the order has an impact on the overall integration +# process, which may cause wedges in the gate later. +# Order matters to the pip dependency resolver, so sorting this file +# changes how packages are installed. New dependencies should be +# added in alphabetical order, however, some dependencies may need to +# be installed in a specific order. +# +# PBR should always appear first +oslo.log>=3.30.0 # Apache-2.0 +pbr!=2.1.0,>=2.0.0 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..4728c64 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,40 @@ +[metadata] +name = apmec-horizon +summary = Apmec extension for Horizon +description-file = + README.rst +author = OpenStack +author-email = openstack-dev@lists.openstack.org +home-page = http://www.openstack.org/ +classifier = + Environment :: OpenStack + Intended Audience :: Developers + Intended Audience :: Information Technology + Intended Audience :: System Administrators + License :: OSI Approved :: Apache Software License + Operating System :: POSIX :: Linux + Programming Language :: Python + Programming Language :: Python :: 2 + Programming Language :: Python :: 2.7 + + +[files] +packages = + apmec_horizon +namespace_packages = + apmec_horizon + +[global] +setup-hooks = + pbr.hooks.setup_hook + +[build_sphinx] +all_files = 1 +build-dir = doc/build +source-dir = doc/source + +[nosetests] +where = test +verbosity = 2 +detailed-errors = 1 +cover-package = apmec_horizon diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..566d844 --- /dev/null +++ b/setup.py @@ -0,0 +1,29 @@ +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# +# 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. + +# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT +import setuptools + +# In python < 2.7.4, a lazy loading of package `pbr` will break +# setuptools if some other modules registered functions in `atexit`. +# solution from: http://bugs.python.org/issue15881#msg170215 +try: + import multiprocessing # noqa +except ImportError: + pass + +setuptools.setup( + setup_requires=['pbr>=2.0.0'], + pbr=True) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..805369a --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,28 @@ +# The order of packages is significant, because pip processes them in the order +# of appearance. Changing the order has an impact on the overall integration +# process, which may cause wedges in the gate later. +# Order matters to the pip dependency resolver, so sorting this file +# changes how packages are installed. New dependencies should be +# added in alphabetical order, however, some dependencies may need to +# be installed in a specific order. +# +# Hacking should appear first in case something else depends on pep8 +hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 +# +coverage!=4.4,>=4.0 # Apache-2.0 +django-nose>=1.4.4 # BSD +mock>=2.0.0 # BSD +mox3>=0.20.0 # Apache-2.0 +nodeenv>=0.9.4 # BSD +nose>=1.3.7 # LGPL +nose-exclude>=0.3.0 # LGPL +nosehtmloutput>=0.0.3 # Apache-2.0 +nosexcover>=1.0.10 # BSD +openstack.nose-plugin>=0.7 # Apache-2.0 +oslosphinx>=4.7.0 # Apache-2.0 +reno>=2.5.0 # Apache-2.0 +selenium>=2.50.1 # Apache-2.0 +sphinx>=1.6.2 # BSD +testtools>=1.4.0 # MIT +# This also needs xvfb library installed on your OS +xvfbwrapper>=0.1.3 #license: MIT diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..e69de29 diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..b32a6d0 --- /dev/null +++ b/tox.ini @@ -0,0 +1,56 @@ +[tox] +envlist = py27,pep8 +minversion = 1.6 +skipsdist = True + +[testenv] +basepython=python2.7 +usedevelop = True +install_command = pip install -U {opts} {packages} +setenv = VIRTUAL_ENV={envdir} +# Note the hash seed is set to 0 until horizon can be tested with a +# random hash seed successfully. +# PYTHONHASHSEED=0 +deps = + -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt + http://tarballs.openstack.org/horizon/horizon-master.tar.gz + http://tarballs.openstack.org/python-apmecclient/python-apmecclient-master.tar.gz + +commands = + {envpython} {toxinidir}/manage.py test apmec_horizon --settings=apmec_horizon.test.settings {posargs} + +[testenv:py27] +commands = + {envpython} {toxinidir}/manage.py test apmec_horizon --settings=apmec_horizon.test.settings {posargs} + +[tox:jenkins] + +[testenv:pep8] +usedevelop = True +whitelist_externals = + git +setenv = + {[testenv]setenv} + DJANGO_SETTINGS_MODULE=apmec_horizon.test.settings +commands = + flake8 + +[testenv:doc] +deps = Sphinx +commands = sphinx-build doc/source doc/build + +[testenv:cover] +setenv = NOSE_WITH_COVERAGE=1 + +[testenv:venv] +commands = {posargs} + +[flake8] +show-source = True +max-complexity = 20 + +[testenv:makemessages] +commands = + pybabel extract -F babel-django.cfg -o apmec_horizon/locale/django.pot -k gettext_noop -k gettext_lazy -k ngettext_lazy:1,2 -k ugettext_noop -k ugettext_lazy -k ungettext_lazy:1,2 -k npgettext:1c,2,3 -k pgettext_lazy:1c,2 -k npgettext_lazy:1c,2,3 apmec_horizon + pybabel extract -F babel-djangojs.cfg -o apmec_horizon/locale/djangojs.pot -k gettext_noop -k gettext_lazy -k ngettext_lazy:1,2 -k ugettext_noop -k ugettext_lazy -k ungettext_lazy:1,2 -k npgettext:1c,2,3 -k pgettext_lazy:1c,2 -k npgettext_lazy:1c,2,3 apmec_horizon