diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..b361ea3 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,14 @@ +If you would like to contribute to the development of OpenStack, you must +follow the steps in this page: +https://docs.openstack.org/infra/manual/developers.html + +If you already have a good understanding of how the system works and your +OpenStack accounts are set up, you can skip to the development workflow +section of this documentation to learn how changes to OpenStack should be +submitted for review via the Gerrit tool: +https://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/iotronic_ui diff --git a/HACKING.rst b/HACKING.rst new file mode 100644 index 0000000..3d43f6e --- /dev/null +++ b/HACKING.rst @@ -0,0 +1,4 @@ +iotronic_ui Style Commandments +=============================================== + +Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..68c771a --- /dev/null +++ b/LICENSE @@ -0,0 +1,176 @@ + + 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. + diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..7ef006d --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,9 @@ +include AUTHORS +include ChangeLog +exclude .gitignore +exclude .gitreview +include setup.py + +recursive-include iotronic_ui *.js *.html *.scss + +global-exclude *.pyc diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..b0de1eb --- /dev/null +++ b/README.rst @@ -0,0 +1,64 @@ +=============================== +IoTronic Panels +=============================== + +Iotronic plugin for the OpenStack Dashboard + +* Free software: Apache license +* Source: http://git.openstack.org/cgit/openstack/iotronic_ui +* Bugs: http://bugs.launchpad.net/None + +Features +-------- + +* TODO + +Enabling in DevStack +-------------------- + +Add this repo as an external repository into your ``local.conf`` file:: + + [[local|localrc]] + enable_plugin iotronic_ui https://github.com/openstack/iotronic_ui + +Manual Installation +------------------- + +Begin by cloning the Horizon and IoTronic Panels repositories:: + + git clone https://github.com/openstack/horizon + git clone https://github.com/openstack/iotronic_ui + +Create a virtual environment and install Horizon dependencies:: + + cd horizon + python tools/install_venv.py + +Set up your ``local_settings.py`` file:: + + cp openstack_dashboard/local/local_settings.py.example openstack_dashboard/local/local_settings.py + +Open up the copied ``local_settings.py`` file in your preferred text +editor. You will want to customize several settings: + +- ``OPENSTACK_HOST`` should be configured with the hostname of your + OpenStack server. Verify that the ``OPENSTACK_KEYSTONE_URL`` and + ``OPENSTACK_KEYSTONE_DEFAULT_ROLE`` settings are correct for your + environment. (They should be correct unless you modified your + OpenStack server to change them.) + +Install IoTronic Panels with all dependencies in your virtual environment:: + + tools/with_venv.sh pip install -e ../iotronic_ui/ + +And enable it in Horizon:: + + ln -s ../iotronic_ui/iotronic_ui/enabled/_90_project_iot_panelgroup.py openstack_dashboard/local/enabled + ln -s ../iotronic_ui/iotronic_ui/enabled/_91_project_iot_boardss_panel.py openstack_dashboard/local/enabled + +To run horizon with the newly enabled IoTronic Panels plugin run:: + + ./run_tests.sh --runserver 0.0.0.0:8080 + +to have the application start on port 8080 and the horizon dashboard will be +available in your browser at http://localhost:8080/ 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/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000..283be3b --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,152 @@ +# Makefile for Sphinx documentation + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/iotronic_ui.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/iotronic_ui.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/iotronic_ui" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/iotronic_ui" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 0000000..e672ab5 --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,441 @@ +# 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. +# +# Horizon documentation build configuration file, created by +# sphinx-quickstart on Thu Oct 27 11:38:59 2011. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +from __future__ import print_function + +import os +import sys + +import django + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.abspath(os.path.join(BASE_DIR, "..", "..")) + +sys.path.insert(0, ROOT) + +# This is required for ReadTheDocs.org, but isn't a bad idea anyway. +os.environ.setdefault('DJANGO_SETTINGS_MODULE', + 'iotronic_ui.test.settings') + +# Starting in Django 1.7, standalone scripts, such as a sphinx build +# require that django.setup() be called first. +# https://docs.djangoproject.com/en/1.8/releases/1.7/#standalone-scripts +django.setup() + + +def write_autodoc_index(): + + def find_autodoc_modules(module_name, sourcedir): + """returns a list of modules in the SOURCE directory.""" + modlist = [] + os.chdir(os.path.join(sourcedir, module_name)) + print("SEARCHING %s" % sourcedir) + for root, dirs, files in os.walk("."): + for filename in files: + if filename == 'tests.py': + continue + if filename.endswith(".py"): + # remove the pieces of the root + elements = root.split(os.path.sep) + # replace the leading "." with the module name + elements[0] = module_name + # and get the base module name + base, extension = os.path.splitext(filename) + if not (base == "__init__"): + elements.append(base) + result = ".".join(elements) + # print result + modlist.append(result) + return modlist + + RSTDIR = os.path.abspath(os.path.join(BASE_DIR, "contributor/api")) + SRCS = [('iotronic_ui', ROOT), ] + + EXCLUDED_MODULES = () + CURRENT_SOURCES = {} + + if not(os.path.exists(RSTDIR)): + os.mkdir(RSTDIR) + CURRENT_SOURCES[RSTDIR] = ['autoindex.rst'] + + INDEXOUT = open(os.path.join(RSTDIR, "autoindex.rst"), "w") + INDEXOUT.write(""" +================= +Source Code Index +================= + +.. contents:: + :depth: 1 + :local: + +""") + + for modulename, path in SRCS: + sys.stdout.write("Generating source documentation for %s\n" % + modulename) + INDEXOUT.write("\n%s\n" % modulename.capitalize()) + INDEXOUT.write("%s\n" % ("=" * len(modulename),)) + INDEXOUT.write(".. toctree::\n") + INDEXOUT.write(" :maxdepth: 1\n") + INDEXOUT.write("\n") + + MOD_DIR = os.path.join(RSTDIR, modulename) + CURRENT_SOURCES[MOD_DIR] = [] + if not(os.path.exists(MOD_DIR)): + os.mkdir(MOD_DIR) + for module in find_autodoc_modules(modulename, path): + if any([module.startswith(exclude) for exclude + in EXCLUDED_MODULES]): + print("Excluded module %s." % module) + continue + mod_path = os.path.join(path, *module.split(".")) + generated_file = os.path.join(MOD_DIR, "%s.rst" % module) + + INDEXOUT.write(" %s/%s\n" % (modulename, module)) + + # Find the __init__.py module if this is a directory + if os.path.isdir(mod_path): + source_file = ".".join((os.path.join(mod_path, "__init__"), + "py",)) + else: + source_file = ".".join((os.path.join(mod_path), "py")) + + CURRENT_SOURCES[MOD_DIR].append("%s.rst" % module) + # Only generate a new file if the source has changed or we don't + # have a doc file to begin with. + if not os.access(generated_file, os.F_OK) or ( + os.stat(generated_file).st_mtime < + os.stat(source_file).st_mtime): + print("Module %s updated, generating new documentation." + % module) + FILEOUT = open(generated_file, "w") + header = "The :mod:`%s` Module" % module + FILEOUT.write("%s\n" % ("=" * len(header),)) + FILEOUT.write("%s\n" % header) + FILEOUT.write("%s\n" % ("=" * len(header),)) + FILEOUT.write(".. automodule:: %s\n" % module) + FILEOUT.write(" :members:\n") + FILEOUT.write(" :undoc-members:\n") + FILEOUT.write(" :show-inheritance:\n") + FILEOUT.write(" :noindex:\n") + FILEOUT.close() + + INDEXOUT.close() + + # Delete auto-generated .rst files for sources which no longer exist + for directory, subdirs, files in list(os.walk(RSTDIR)): + for old_file in files: + if old_file not in CURRENT_SOURCES.get(directory, []): + print("Removing outdated file for %s" % old_file) + os.remove(os.path.join(directory, old_file)) + + +write_autodoc_index() + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ---------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. +# They can be extensions coming with Sphinx (named 'sphinx.ext.*') +# or your custom ones. +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.viewcode', + 'openstackdocstheme', + ] + +# Add any paths that contain templates here, relative to this directory. +# templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'IoTronic Panels' +copyright = u'2017, OpenStack Foundation' + +# Release notes are version independent. +# The full version, including alpha/beta/rc tags. +release = '' +# The short X.Y version. +version = '' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# today = '' +# Else, today_fmt is used as the format for a strftime call. +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['**/#*', '**~', '**/#*#'] + +# The reST default role (used for this markup: `text`) +# to use for all documents. +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +primary_domain = 'py' +nitpicky = False + + +# -- Options for HTML output -------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'openstackdocs' + +# openstackdocstheme options +repository_name = 'openstack/iotronic_ui' +bug_project = 'None' +bug_tag = '' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +# html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +# html_static_path = ['_static'] + +# Must set this variable to include year, month, day, hours, and minutes. +html_last_updated_fmt = '%Y-%m-%d %H:%M' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# html_additional_pages = {} + +# If false, no module index is generated. +# html_domain_indices = True + +# If false, no index is generated. +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'iotronic_uidoc' + + +# -- Options for LaTeX output ------------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # 'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass +# [howto/manual]). +latex_documents = [ + ('index', 'iotronic_ui.tex', + u'IoTronic Panels Documentation', + u'OpenStack Foundation', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# latex_use_parts = False + +# If true, show page references after internal links. +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# latex_appendices = [] + +# If false, no module index is generated. +# latex_domain_indices = True + + +# -- Options for manual page output ------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', u'IoTronic Panels Documentation', + 'Documentation for the IoTronic Panels plugin to the Openstack\ + Dashboard (Horizon)', + [u'OpenStack'], 1) +] + +# If true, show URL addresses after external links. +# man_show_urls = False + + +# -- Options for Texinfo output ----------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'IoTronic Panels', + u'IoTronic Panels Documentation', u'OpenStack', + 'IoTronic Panels', + 'Iotronic plugin for the OpenStack Dashboard', 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +# texinfo_appendices = [] + +# If false, no module index is generated. +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# texinfo_show_urls = 'footnote' + + +# -- Options for Epub output -------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = u'IoTronic Panels' +epub_author = u'OpenStack' +epub_publisher = u'OpenStack' +epub_copyright = u'2017, OpenStack' + +# The language of the text. It defaults to the language option +# or en if the language is not set. +# epub_language = '' + +# The scheme of the identifier. Typical schemes are ISBN or URL. +# epub_scheme = '' + +# The unique identifier of the text. This can be an ISBN number +# or the project homepage. +# epub_identifier = '' + +# A unique identification for the text. +# epub_uid = '' + +# A tuple containing the cover image and cover page html template filenames. +# epub_cover = () + +# HTML files that should be inserted before the pages created by sphinx. +# The format is a list of tuples containing the path and title. +# epub_pre_files = [] + +# HTML files shat should be inserted after the pages created by sphinx. +# The format is a list of tuples containing the path and title. +# epub_post_files = [] + +# A list of files that should not be packed into the epub file. +# epub_exclude_files = [] + +# The depth of the table of contents in toc.ncx. +# epub_tocdepth = 3 + +# Allow duplicate toc entries. +# epub_tocdup = True diff --git a/doc/source/configuration/index.rst b/doc/source/configuration/index.rst new file mode 100644 index 0000000..e9132e9 --- /dev/null +++ b/doc/source/configuration/index.rst @@ -0,0 +1,10 @@ +============= +Configuration +============= + +IoTronic Panels has no configuration option. + +For more configurations, see +`Deployment & Configuration +`__ +in the Horizon documentation. diff --git a/doc/source/contributor/api.rst b/doc/source/contributor/api.rst new file mode 100644 index 0000000..dd3be26 --- /dev/null +++ b/doc/source/contributor/api.rst @@ -0,0 +1,9 @@ +===================== +Source Code Reference +===================== + +.. toctree:: + :maxdepth: 1 + :glob: + + api/* diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst new file mode 100644 index 0000000..4c2b48b --- /dev/null +++ b/doc/source/contributor/index.rst @@ -0,0 +1,16 @@ +================= +Contributor Guide +================= + +There is no topic specific to IoTronic Panels now. + +See `Horizon Contributor Documentation +`__ +for general topic on developing a dashboard on horizon. + +---- + +.. toctree:: + :maxdepth: 1 + + api diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 0000000..a90a3df --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,33 @@ +=============================== +IoTronic Panels +=============================== + +Iotronic plugin for the OpenStack Dashboard + +* Free software: Apache license +* Source: http://git.openstack.org/cgit/openstack/iotronic_ui +* Bugs: http://bugs.launchpad.net/None + +Features +-------- + +* TODO + +User Documentation +------------------ + +.. toctree:: + :maxdepth: 2 + + install/index + configuration/index + Release Notes + +Contributor Guide +----------------- + +.. toctree:: + :glob: + :maxdepth: 2 + + contributor/index diff --git a/doc/source/install/index.rst b/doc/source/install/index.rst new file mode 100644 index 0000000..155f82c --- /dev/null +++ b/doc/source/install/index.rst @@ -0,0 +1,53 @@ +============ +Installation +============ + +Enabling in DevStack +-------------------- + +Add this repo as an external repository into your ``local.conf`` file:: + + [[local|localrc]] + enable_plugin iotronic_ui https://github.com/openstack/iotronic_ui + +Manual Installation +------------------- + +Begin by cloning the Horizon and IoTronic Panels repositories:: + + git clone https://github.com/openstack/horizon + git clone https://github.com/openstack/iotronic_ui + +Create a virtual environment and install Horizon dependencies:: + + cd horizon + python tools/install_venv.py + +Set up your ``local_settings.py`` file:: + + cp openstack_dashboard/local/local_settings.py.example openstack_dashboard/local/local_settings.py + +Open up the copied ``local_settings.py`` file in your preferred text +editor. You will want to customize several settings: + +- ``OPENSTACK_HOST`` should be configured with the hostname of your + OpenStack server. Verify that the ``OPENSTACK_KEYSTONE_URL`` and + ``OPENSTACK_KEYSTONE_DEFAULT_ROLE`` settings are correct for your + environment. (They should be correct unless you modified your + OpenStack server to change them.) + +Install IoTronic Panels with all dependencies in your virtual environment:: + + tools/with_venv.sh pip install -e ../iotronic_ui/ + +And enable it in Horizon:: + + ln -s ../iotronic_ui/iotronic_ui/enabled/_90_project_iot_panelgroup.py openstack_dashboard/local/enabled + ln -s ../iotronic_ui/iotronic_ui/enabled/_91_project_iot_boardss_panel.py openstack_dashboard/local/enabled + +To run horizon with the newly enabled IoTronic Panels plugin run:: + + ./run_tests.sh --runserver 0.0.0.0:8080 + +to have the application start on port 8080 and the horizon dashboard will be +available in your browser at http://localhost:8080/ diff --git a/iotronic_ui.egg-info/PKG-INFO b/iotronic_ui.egg-info/PKG-INFO new file mode 100644 index 0000000..df40467 --- /dev/null +++ b/iotronic_ui.egg-info/PKG-INFO @@ -0,0 +1,85 @@ +Metadata-Version: 1.1 +Name: iotronic-ui +Version: 0.0.0 +Summary: Iotronic plugin for the OpenStack Dashboard +Home-page: http://www.openstack.org/ +Author: OpenStack +Author-email: openstack-dev@lists.openstack.org +License: UNKNOWN +Description: =============================== + IoTronic Panels + =============================== + + Iotronic plugin for the OpenStack Dashboard + + * Free software: Apache license + * Source: http://git.openstack.org/cgit/openstack/iotronic_ui + * Bugs: http://bugs.launchpad.net/None + + Features + -------- + + * TODO + + Enabling in DevStack + -------------------- + + Add this repo as an external repository into your ``local.conf`` file:: + + [[local|localrc]] + enable_plugin iotronic_ui https://github.com/openstack/iotronic_ui + + Manual Installation + ------------------- + + Begin by cloning the Horizon and IoTronic Panels repositories:: + + git clone https://github.com/openstack/horizon + git clone https://github.com/openstack/iotronic_ui + + Create a virtual environment and install Horizon dependencies:: + + cd horizon + python tools/install_venv.py + + Set up your ``local_settings.py`` file:: + + cp openstack_dashboard/local/local_settings.py.example openstack_dashboard/local/local_settings.py + + Open up the copied ``local_settings.py`` file in your preferred text + editor. You will want to customize several settings: + + - ``OPENSTACK_HOST`` should be configured with the hostname of your + OpenStack server. Verify that the ``OPENSTACK_KEYSTONE_URL`` and + ``OPENSTACK_KEYSTONE_DEFAULT_ROLE`` settings are correct for your + environment. (They should be correct unless you modified your + OpenStack server to change them.) + + Install IoTronic Panels with all dependencies in your virtual environment:: + + tools/with_venv.sh pip install -e ../iotronic_ui/ + + And enable it in Horizon:: + + ln -s ../iotronic_ui/iotronic_ui/enabled/_90_project_iot_panelgroup.py openstack_dashboard/local/enabled + ln -s ../iotronic_ui/iotronic_ui/enabled/_91_project_iot_boardss_panel.py openstack_dashboard/local/enabled + + To run horizon with the newly enabled IoTronic Panels plugin run:: + + ./run_tests.sh --runserver 0.0.0.0:8080 + + to have the application start on port 8080 and the horizon dashboard will be + available in your browser at http://localhost:8080/ + + +Platform: UNKNOWN +Classifier: Environment :: OpenStack +Classifier: Intended Audience :: Information Technology +Classifier: Intended Audience :: System Administrators +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Operating System :: POSIX :: Linux +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.5 diff --git a/iotronic_ui.egg-info/SOURCES.txt b/iotronic_ui.egg-info/SOURCES.txt new file mode 100644 index 0000000..1e94740 --- /dev/null +++ b/iotronic_ui.egg-info/SOURCES.txt @@ -0,0 +1,88 @@ +CONTRIBUTING.rst +HACKING.rst +LICENSE +MANIFEST.in +README.rst +babel-django.cfg +babel-djangojs.cfg +manage.py +package.json +requirements.txt +setup.cfg +setup.py +test-requirements.txt +test-shim.js +tox.ini +doc/Makefile +doc/source/conf.py +doc/source/index.rst +doc/source/configuration/index.rst +doc/source/contributor/api.rst +doc/source/contributor/index.rst +doc/source/install/index.rst +iotronic_ui/__init__.py +iotronic_ui.egg-info/PKG-INFO +iotronic_ui.egg-info/SOURCES.txt +iotronic_ui.egg-info/dependency_links.txt +iotronic_ui.egg-info/not-zip-safe +iotronic_ui.egg-info/pbr.json +iotronic_ui.egg-info/requires.txt +iotronic_ui.egg-info/top_level.txt +iotronic_ui/api/iotronic.py +iotronic_ui/enabled/_6000_iot.py +iotronic_ui/enabled/_6010_iot_boards_panel.py +iotronic_ui/enabled/_6020_iot_plugins_panel.py +iotronic_ui/iot/__init__.py +iotronic_ui/iot/dashboard.py +iotronic_ui/iot/boards/__init__.py +iotronic_ui/iot/boards/forms.py +iotronic_ui/iot/boards/panel.py +iotronic_ui/iot/boards/tables.py +iotronic_ui/iot/boards/tabs.py +iotronic_ui/iot/boards/tests.py +iotronic_ui/iot/boards/urls.py +iotronic_ui/iot/boards/views.py +iotronic_ui/iot/boards/templates/boards/_create.html +iotronic_ui/iot/boards/templates/boards/_detail_overview.html +iotronic_ui/iot/boards/templates/boards/_removeplugins.html +iotronic_ui/iot/boards/templates/boards/_update.html +iotronic_ui/iot/boards/templates/boards/create.html +iotronic_ui/iot/boards/templates/boards/index.html +iotronic_ui/iot/boards/templates/boards/removeplugins.html +iotronic_ui/iot/boards/templates/boards/update.html +iotronic_ui/iot/plugins/__init__.py +iotronic_ui/iot/plugins/forms.py +iotronic_ui/iot/plugins/panel.py +iotronic_ui/iot/plugins/tables.py +iotronic_ui/iot/plugins/tabs.py +iotronic_ui/iot/plugins/tests.py +iotronic_ui/iot/plugins/urls.py +iotronic_ui/iot/plugins/views.py +iotronic_ui/iot/plugins/templates/plugins/_call.html +iotronic_ui/iot/plugins/templates/plugins/_create.html +iotronic_ui/iot/plugins/templates/plugins/_detail_overview.html +iotronic_ui/iot/plugins/templates/plugins/_inject.html +iotronic_ui/iot/plugins/templates/plugins/_remove.html +iotronic_ui/iot/plugins/templates/plugins/_start.html +iotronic_ui/iot/plugins/templates/plugins/_stop.html +iotronic_ui/iot/plugins/templates/plugins/_update.html +iotronic_ui/iot/plugins/templates/plugins/call.html +iotronic_ui/iot/plugins/templates/plugins/create.html +iotronic_ui/iot/plugins/templates/plugins/index.html +iotronic_ui/iot/plugins/templates/plugins/inject.html +iotronic_ui/iot/plugins/templates/plugins/remove.html +iotronic_ui/iot/plugins/templates/plugins/start.html +iotronic_ui/iot/plugins/templates/plugins/stop.html +iotronic_ui/iot/plugins/templates/plugins/update.html +iotronic_ui/iot/static/iot/images/blue-circle.png +iotronic_ui/iot/static/iot/images/green-circle.png +iotronic_ui/iot/static/iot/images/marker-icon-green.png +iotronic_ui/iot/static/iot/images/marker-icon-red.png +iotronic_ui/iot/static/iot/images/marker-icon.png +iotronic_ui/iot/static/iot/images/marker-shadow.png +iotronic_ui/iot/static/iot/images/red-circle.png +iotronic_ui/iot/static/iot/js/iot.js +iotronic_ui/iot/static/iot/scss/iot.scss +iotronic_ui/iot/templates/iot/base.html +tools/tox_install.sh +tools/tox_install.sh_ORIG \ No newline at end of file diff --git a/iotronic_ui.egg-info/dependency_links.txt b/iotronic_ui.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/iotronic_ui.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/iotronic_ui.egg-info/not-zip-safe b/iotronic_ui.egg-info/not-zip-safe new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/iotronic_ui.egg-info/not-zip-safe @@ -0,0 +1 @@ + diff --git a/iotronic_ui.egg-info/pbr.json b/iotronic_ui.egg-info/pbr.json new file mode 100644 index 0000000..34d2e6e --- /dev/null +++ b/iotronic_ui.egg-info/pbr.json @@ -0,0 +1 @@ +{"git_version": "4902c1d", "is_release": false} \ No newline at end of file diff --git a/iotronic_ui.egg-info/requires.txt b/iotronic_ui.egg-info/requires.txt new file mode 100644 index 0000000..1725ef9 --- /dev/null +++ b/iotronic_ui.egg-info/requires.txt @@ -0,0 +1,6 @@ +pbr!=2.1.0,>=2.0.0 +Babel!=2.4.0,>=2.3.4 +Django<2.0,>=1.8 +django-babel>=0.5.1 +django-compressor>=2.0 +django-pyscss>=2.0.2 diff --git a/iotronic_ui.egg-info/top_level.txt b/iotronic_ui.egg-info/top_level.txt new file mode 100644 index 0000000..7d4fe56 --- /dev/null +++ b/iotronic_ui.egg-info/top_level.txt @@ -0,0 +1 @@ +iotronic_ui diff --git a/iotronic_ui/__init__.py b/iotronic_ui/__init__.py new file mode 100644 index 0000000..b8023d8 --- /dev/null +++ b/iotronic_ui/__init__.py @@ -0,0 +1 @@ +__version__ = '0.0.1' diff --git a/iotronic_ui/api/iotronic.py b/iotronic_ui/api/iotronic.py new file mode 100644 index 0000000..9f69941 --- /dev/null +++ b/iotronic_ui/api/iotronic.py @@ -0,0 +1,148 @@ +# 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 collections import OrderedDict +# import threading + +from iotronicclient import client as iotronic_client +# from django.conf import settings +# from django.utils.translation import ugettext_lazy as _ + +# from horizon import exceptions +from horizon.utils.memoized import memoized # noqa + +from openstack_dashboard.api import base +# from openstack_dashboard.api import keystone + + +# TESTING +import logging +LOG = logging.getLogger(__name__) + + +@memoized +def iotronicclient(request): + """Initialization of Iotronic client.""" + + endpoint = base.url_for(request, 'iot') + # insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False) + # cacert = getattr(settings, 'OPENSTACK_SSL_CACERT', None) + + return iotronic_client.Client('1', endpoint, token=request.user.token.id) + + +# BOARD MANAGEMENT +def board_list(request, status=None, detail=None, project=None): + """List boards.""" + boards = iotronicclient(request).board.list(status, detail, project) + return boards + + +def board_get(request, board_id, fields): + """Get board info.""" + board = iotronicclient(request).board.get(board_id, fields) + return board + + +def board_create(request, code, mobile, location, type, name): + """Create board.""" + params = {"code": code, + "mobile": mobile, + "location": location, + "type": type, + "name": name} + board = iotronicclient(request).board.create(**params) + return board + + +def board_update(request, board_id, patch): + """Update board.""" + board = iotronicclient(request).board.update(board_id, patch) + return board + + +def board_delete(request, board_id): + """Delete board.""" + board = iotronicclient(request).board.delete(board_id) + return board + + +# PLUGIN MANAGEMENT (Cloud Side) +def plugin_list(request, detail=None, project=None, with_public=False, + all_plugins=False): + """List plugins.""" + plugin = iotronicclient(request).plugin() + plugins = plugin.list(detail, project, + with_public=with_public, + all_plugins=all_plugins) + return plugins + + +def plugin_get(request, plugin_id, fields): + """Get plugin info.""" + plugin = iotronicclient(request).plugin.get(plugin_id, fields) + return plugin + + +def plugin_create(request, name, public, callable, code, parameters): + """Create plugin.""" + params = {"name": name, + "public": public, + "callable": callable, + "code": code, + "parameters": parameters} + plugin = iotronicclient(request).plugin.create(**params) + return plugin + + +def plugin_update(request, plugin_id, patch): + """Update plugin.""" + plugin = iotronicclient(request).plugin.update(plugin_id, patch) + return plugin + + +def plugin_delete(request, plugin_id): + """Delete plugin.""" + plugin = iotronicclient(request).plugin.delete(plugin_id) + return plugin + + +# PLUGIN MANAGEMENT (Board Side) +def plugin_inject(request, board_id, plugin_id, onboot): + """Inject plugin on board(s).""" + plugin_injection = iotronicclient(request).plugin_injection() + plugin = plugin_injection.plugin_inject(board_id, plugin_id, onboot) + return plugin + + +def plugin_action(request, board_id, plugin_id, action, params={}): + """Start/Stop/Call actions on board(s).""" + plugin_injection = iotronicclient(request).plugin_injection() + plugin = plugin_injection.plugin_action(board_id, + plugin_id, + action, + params) + return plugin + + +def plugin_remove(request, board_id, plugin_id): + """Inject plugin on board(s).""" + plugin_injection = iotronicclient(request).plugin_injection() + plugin = plugin_injection.plugin_remove(board_id, plugin_id) + return plugin + + +def plugins_on_board(request, board_id): + """Plugins on board.""" + plugin_injection = iotronicclient(request).plugin_injection() + plugins = plugin_injection.plugins_on_board(board_id) + return plugins diff --git a/iotronic_ui/enabled/_6000_iot.py b/iotronic_ui/enabled/_6000_iot.py new file mode 100644 index 0000000..ccbc62e --- /dev/null +++ b/iotronic_ui/enabled/_6000_iot.py @@ -0,0 +1,29 @@ +# Copyright 2015, Rackspace, US, 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. + +# The slug of the dashboard to be added to HORIZON['dashboards']. Required. +DASHBOARD = 'iot' + +# A list of applications to be added to INSTALLED_APPS. +ADD_INSTALLED_APPS = [ + 'iotronic_ui.iot', +] + +""" +ADD_ANGULAR_MODULES = [ + 'horizon.dashboard.iot', +] +""" + +AUTO_DISCOVER_STATIC_FILES = True diff --git a/iotronic_ui/enabled/_6010_iot_boards_panel.py b/iotronic_ui/enabled/_6010_iot_boards_panel.py new file mode 100644 index 0000000..cf377c6 --- /dev/null +++ b/iotronic_ui/enabled/_6010_iot_boards_panel.py @@ -0,0 +1,23 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# The slug of the panel to be added to HORIZON_CONFIG. Required. +PANEL = 'boards' +# The slug of the dashboard the PANEL associated with. Required. +PANEL_DASHBOARD = 'iot' +# The slug of the panel group the PANEL is associated with. +PANEL_GROUP = 'iot' +# If set, it will update the default panel of the PANEL_DASHBOARD. +DEFAULT_PANEL = '' + +# Python panel class of the PANEL to be added. +ADD_PANEL = 'iotronic_ui.iot.boards.panel.Boards' diff --git a/iotronic_ui/enabled/_6020_iot_plugins_panel.py b/iotronic_ui/enabled/_6020_iot_plugins_panel.py new file mode 100644 index 0000000..57b5198 --- /dev/null +++ b/iotronic_ui/enabled/_6020_iot_plugins_panel.py @@ -0,0 +1,23 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# The slug of the panel to be added to HORIZON_CONFIG. Required. +PANEL = 'plugins' +# The slug of the dashboard the PANEL associated with. Required. +PANEL_DASHBOARD = 'iot' +# The slug of the panel group the PANEL is associated with. +PANEL_GROUP = 'iot' +# If set, it will update the default panel of the PANEL_DASHBOARD. +DEFAULT_PANEL = '' + +# Python panel class of the PANEL to be added. +ADD_PANEL = 'iotronic_ui.iot.plugins.panel.Plugins' diff --git a/iotronic_ui/iot/__init__.py b/iotronic_ui/iot/__init__.py new file mode 100644 index 0000000..b8023d8 --- /dev/null +++ b/iotronic_ui/iot/__init__.py @@ -0,0 +1 @@ +__version__ = '0.0.1' diff --git a/iotronic_ui/iot/boards/__init__.py b/iotronic_ui/iot/boards/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iotronic_ui/iot/boards/forms.py b/iotronic_ui/iot/boards/forms.py new file mode 100644 index 0000000..8e4ba45 --- /dev/null +++ b/iotronic_ui/iot/boards/forms.py @@ -0,0 +1,197 @@ +# 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 logging + +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon import messages + +from openstack_dashboard.api import iotronic +from openstack_dashboard import policy + +LOG = logging.getLogger(__name__) + + +class CreateBoardForm(forms.SelfHandlingForm): + name = forms.CharField(label=_("Board Name")) + code = forms.CharField( + label=_("Registration Code"), + help_text=_("Registration code") + ) + + # MODIFY ---> options: yun, server + type = forms.ChoiceField( + label=_("Type"), + choices=[('yun', _('YUN')), ('server', _('Server'))], + widget=forms.Select( + attrs={'class': 'switchable', 'data-slug': 'slug-type'}, + ) + ) + + """ + mobile = forms.ChoiceField( + label=_("Mobile"), + choices =[('false', _('False')), ('true', _('True'))], + widget=forms.Select( + attrs={'class': 'switchable', 'data-slug': 'slug-mobile'}, + ) + ) + """ + mobile = forms.BooleanField(label=_("Mobile"), required=False) + + latitude = forms.FloatField(label=_("Latitude")) + longitude = forms.FloatField(label=_("Longitude")) + altitude = forms.FloatField(label=_("Altitude")) + + def handle(self, request, data): + try: + + # Float + # data["location"] = [{"latitude": data["latitude"], + # "longitude": data["longitude"], + # "altitude": data["altitude"]}] + # String + data["location"] = [{"latitude": str(data["latitude"]), + "longitude": str(data["longitude"]), + "altitude": str(data["altitude"])}] + + board = iotronic.board_create(request, data["code"], + data["mobile"], data["location"], + data["type"], data["name"]) + messages.success(request, _("Board created successfully.")) + + return board + except Exception: + exceptions.handle(request, _('Unable to create board.')) + + +class UpdateBoardForm(forms.SelfHandlingForm): + uuid = forms.CharField(label=_("Board ID"), widget=forms.HiddenInput) + name = forms.CharField(label=_("Board Name")) + mobile = forms.BooleanField(label=_("Mobile"), required=False) + + latitude = forms.FloatField(label=_("Latitude")) + longitude = forms.FloatField(label=_("Longitude")) + altitude = forms.FloatField(label=_("Altitude")) + + def __init__(self, *args, **kwargs): + + super(UpdateBoardForm, self).__init__(*args, **kwargs) + + # LOG.debug("MELO INITIAL: %s", kwargs["initial"]) + + LOG.debug("MELO Manager: %s", policy.check((("iot", "iot_manager"),), + self.request)) + LOG.debug("MELO Admin: %s", policy.check((("iot", "iot_admin"),), + self.request)) + + # Admin + if policy.check((("iot", "iot:update_boards"),), self.request): + # LOG.debug("MELO ADMIN") + pass + + # Manager or Admin of the iot project + elif (policy.check((("iot", "iot_manager"),), self.request) or + policy.check((("iot", "iot_admin"),), self.request)): + # LOG.debug("MELO NO-edit IOT ADMIN") + pass + + # Other users + else: + if self.request.user.id != kwargs["initial"]["owner"]: + # LOG.debug("MELO IMMUTABLE FIELDS") + self.fields["name"].widget.attrs = {'readonly': 'readonly'} + self.fields["mobile"].widget.attrs = {'disabled': 'disabled'} + + self.fields["latitude"].widget.attrs = {'readonly': + 'readonly'} + self.fields["longitude"].widget.attrs = {'readonly': + 'readonly'} + self.fields["altitude"].widget.attrs = {'readonly': + 'readonly'} + + def handle(self, request, data): + try: + + data["location"] = [{"latitude": str(data["latitude"]), + "longitude": str(data["longitude"]), + "altitude": str(data["altitude"])}] + iotronic.board_update(request, data["uuid"], + {"name": data["name"], + "mobile": data["mobile"], + "location": data["location"]}) + + messages.success(request, _("Board updated successfully.")) + return True + except Exception: + exceptions.handle(request, _('Unable to update board.')) + + +class RemovePluginsForm(forms.SelfHandlingForm): + + uuid = forms.CharField(label=_("Board ID"), widget=forms.HiddenInput) + + name = forms.CharField( + label=_('Board Name'), + widget=forms.TextInput(attrs={'readonly': 'readonly'}) + ) + + plugin_list = forms.MultipleChoiceField( + label=_("Plugins List"), + widget=forms.SelectMultiple( + attrs={'class': 'switchable', 'data-slug': 'slug-remove-plugins'}), + help_text=_("Select plugins in this pool ") + ) + + def __init__(self, *args, **kwargs): + + super(RemovePluginsForm, self).__init__(*args, **kwargs) + # input=kwargs.get('initial',{}) + + boardslist_length = len(kwargs["initial"]["board_list"]) + + self.fields["plugin_list"].choices = kwargs["initial"]["plugin_list"] + self.fields["plugin_list"].max_length = boardslist_length + + def handle(self, request, data): + + counter = 0 + + for plugin in data["plugin_list"]: + for key, value in self.fields["plugin_list"].choices: + if key == plugin: + + try: + board = None + + # LOG.debug('INJECT: %s %s', plugin, value) + # board = iotronic.plugin_create(request, data["name"], + # data["public"], + # data["callable"], + # data["code"]) + message_text = "Plugin " + str(value) + \ + " removed successfully." + messages.success(request, _(message_text)) + + if counter != len(data["plugin_list"]) - 1: + counter += 1 + else: + return board + except Exception: + message_text = "Unable to remove plugin " \ + + str(value) + "." + exceptions.handle(request, _(message_text)) + + break diff --git a/iotronic_ui/iot/boards/panel.py b/iotronic_ui/iot/boards/panel.py new file mode 100644 index 0000000..beb3ef9 --- /dev/null +++ b/iotronic_ui/iot/boards/panel.py @@ -0,0 +1,33 @@ +# 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 openstack_dashboard.api import keystone + + +class Boards(horizon.Panel): + name = _("Boards") + slug = "boards" + permissions = ('openstack.services.iot', ) + # policy_rules = (("iot", "iot:list_all_boards"),) + + # TO BE REMOVED + """ + def can_access(self, context): + if keystone.is_multi_domain_enabled() \ + and not keystone.is_domain_admin(context['request']): + return False + return super(Roles, self).can_access(context) + """ diff --git a/iotronic_ui/iot/boards/tables.py b/iotronic_ui/iot/boards/tables.py new file mode 100644 index 0000000..c2a4bf9 --- /dev/null +++ b/iotronic_ui/iot/boards/tables.py @@ -0,0 +1,123 @@ +# 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 logging + +from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ungettext_lazy + +from horizon import tables + +from openstack_dashboard import api + +LOG = logging.getLogger(__name__) + + +class CreateBoardLink(tables.LinkAction): + name = "create" + verbose_name = _("Create Board") + url = "horizon:iot:boards:create" + classes = ("ajax-modal",) + icon = "plus" + # policy_rules = (("iot", "iot:create_board"),) + + +class EditBoardLink(tables.LinkAction): + name = "edit" + verbose_name = _("Edit") + url = "horizon:iot:boards:update" + classes = ("ajax-modal",) + icon = "pencil" + # policy_rules = (("iot", "iot:update_board"),) + + """ + def allowed(self, request, role): + return api.keystone.keystone_can_edit_role() + """ + + +class RemovePluginsLink(tables.LinkAction): + name = "removeplugins" + verbose_name = _("Remove Plugin(s)") + url = "horizon:iot:boards:removeplugins" + classes = ("ajax-modal",) + icon = "plus" + # policy_rules = (("iot", "iot:create_board"),) + + +class DeleteBoardsAction(tables.DeleteAction): + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Delete Board", + u"Delete Boards", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Deleted Board", + u"Deleted Boards", + count + ) + # policy_rules = (("iot", "iot:delete_board"),) + + """ + def allowed(self, request, role): + return api.keystone.keystone_can_edit_role() + """ + + def delete(self, request, board_id): + api.iotronic.board_delete(request, board_id) + + +class BoardFilterAction(tables.FilterAction): + + # If uncommented it will appear the select menu list of fields + # and filter button + """ + filter_type = "server" + filter_choices = (("name", _("Board Name ="), True), + ("type", _("Type ="), True), + ("status", _("Status ="), True)) + """ + + def filter(self, table, boards, filter_string): + """Naive case-insensitive search.""" + q = filter_string.lower() + return [board for board in boards + if q in board.name.lower()] + + +class BoardsTable(tables.DataTable): + name = tables.WrappingColumn('name', link="horizon:iot:boards:detail", + verbose_name=_('Board Name')) + type = tables.Column('type', verbose_name=_('Type')) + # mobile = tables.Column('mobile', verbose_name=_('Mobile')) + uuid = tables.Column('uuid', verbose_name=_('Board ID')) + # code = tables.Column('code', verbose_name=_('Code')) + status = tables.Column('status', verbose_name=_('Status')) + location = tables.Column('location', verbose_name=_('Geo')) + # extra = tables.Column('extra', verbose_name=_('Extra')) + + # Overriding get_object_id method because in IoT service the "id" is + # identified by the field UUID + def get_object_id(self, datum): + return datum.uuid + + class Meta(object): + name = "boards" + verbose_name = _("boards") + row_actions = (EditBoardLink, RemovePluginsLink, DeleteBoardsAction) + table_actions = (BoardFilterAction, CreateBoardLink, + DeleteBoardsAction) diff --git a/iotronic_ui/iot/boards/tabs.py b/iotronic_ui/iot/boards/tabs.py new file mode 100644 index 0000000..f99d752 --- /dev/null +++ b/iotronic_ui/iot/boards/tabs.py @@ -0,0 +1,47 @@ +# 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 logging + +# from django.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import tabs + +LOG = logging.getLogger(__name__) + +""" +import inspect +LOG.debug('MELO CLASSES: %s', + inspect.getmembers(tabs, predicate=inspect.isclass)) +""" + + +class OverviewTab(tabs.Tab): + name = _("Overview") + slug = "overview" + template_name = ("iot/boards/_detail_overview.html") + + def get_context_data(self, request): + coordinates = self.tab_group.kwargs['board'].__dict__["location"][0] + # LOG.debug('IOT INFO: %s', coordinates) + + return {"board": self.tab_group.kwargs['board'], + "coordinates": coordinates, + "is_superuser": request.user.is_superuser} + + +class BoardDetailTabs(tabs.TabGroup): + slug = "board_details" + # tabs = (OverviewTab, LogTab, ConsoleTab, AuditTab) + tabs = (OverviewTab,) + sticky = True diff --git a/iotronic_ui/iot/boards/templates/boards/_create.html b/iotronic_ui/iot/boards/templates/boards/_create.html new file mode 100644 index 0000000..2d20be4 --- /dev/null +++ b/iotronic_ui/iot/boards/templates/boards/_create.html @@ -0,0 +1,8 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Add a new board." %}

+{% endblock %} + diff --git a/iotronic_ui/iot/boards/templates/boards/_detail_overview.html b/iotronic_ui/iot/boards/templates/boards/_detail_overview.html new file mode 100644 index 0000000..d396f88 --- /dev/null +++ b/iotronic_ui/iot/boards/templates/boards/_detail_overview.html @@ -0,0 +1,51 @@ +{% load i18n sizeformat %} + +
+
+
{% trans "Name" %}
+
{{ board.name }}
+
{% trans "Status" %}
+
{{ board.status }}
+
{% trans "Type" %}
+
{{ board.type }}
+
{% trans "ID" %}
+
{{ board.uuid }}
+
{% trans "Code" %}
+
{{ board.code }}
+
{% trans "Creation data" %}
+
{{ board.created_at }}
+
{% trans "Location" %}
+
Latitude: {{ coordinates.latitude }}
+
Longitude: {{ coordinates.longitude }}
+
Altitude: {{ coordinates.altitude }}
+
{% trans "Mobile" %}
+
{{ board.mobile }}
+
{% trans "Extra" %}
+
{{ board.extra }}
+
+
+ + +
+ +
+ diff --git a/iotronic_ui/iot/boards/templates/boards/_removeplugins.html b/iotronic_ui/iot/boards/templates/boards/_removeplugins.html new file mode 100644 index 0000000..2f91899 --- /dev/null +++ b/iotronic_ui/iot/boards/templates/boards/_removeplugins.html @@ -0,0 +1,8 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Remove plugin(s) from this board." %}

+{% endblock %} + diff --git a/iotronic_ui/iot/boards/templates/boards/_update.html b/iotronic_ui/iot/boards/templates/boards/_update.html new file mode 100644 index 0000000..f59eae5 --- /dev/null +++ b/iotronic_ui/iot/boards/templates/boards/_update.html @@ -0,0 +1,7 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Edit the board's details." %}

+{% endblock %} diff --git a/iotronic_ui/iot/boards/templates/boards/create.html b/iotronic_ui/iot/boards/templates/boards/create.html new file mode 100644 index 0000000..df06bf8 --- /dev/null +++ b/iotronic_ui/iot/boards/templates/boards/create.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Insert Board" %}{% endblock %} + +{% block main %} + {% include 'iot/boards/_create.html' %} +{% endblock %} diff --git a/iotronic_ui/iot/boards/templates/boards/index.html b/iotronic_ui/iot/boards/templates/boards/index.html new file mode 100644 index 0000000..d62836b --- /dev/null +++ b/iotronic_ui/iot/boards/templates/boards/index.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Boards" %}{% endblock %} + +{% block main %} + {{ table.render }} +{% endblock %} diff --git a/iotronic_ui/iot/boards/templates/boards/removeplugins.html b/iotronic_ui/iot/boards/templates/boards/removeplugins.html new file mode 100644 index 0000000..0c133d8 --- /dev/null +++ b/iotronic_ui/iot/boards/templates/boards/removeplugins.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Remove plugin(s) from this board." %}{% endblock %} + +{% block main %} + {% include 'iot/boards/_removeplugins.html' %} +{% endblock %} diff --git a/iotronic_ui/iot/boards/templates/boards/update.html b/iotronic_ui/iot/boards/templates/boards/update.html new file mode 100644 index 0000000..f7ec785 --- /dev/null +++ b/iotronic_ui/iot/boards/templates/boards/update.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Update Board" %}{% endblock %} + +{% block main %} + {% include 'iot/boards/_update.html' %} +{% endblock %} diff --git a/iotronic_ui/iot/boards/tests.py b/iotronic_ui/iot/boards/tests.py new file mode 100644 index 0000000..2dcc689 --- /dev/null +++ b/iotronic_ui/iot/boards/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 horizon.test import helpers as test + + +class BoardsTests(test.TestCase): + # Unit tests for boards. + def test_me(self): + self.assertTrue(1 + 1 == 2) diff --git a/iotronic_ui/iot/boards/urls.py b/iotronic_ui/iot/boards/urls.py new file mode 100644 index 0000000..7349a04 --- /dev/null +++ b/iotronic_ui/iot/boards/urls.py @@ -0,0 +1,27 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.conf.urls import url + +from iotronic_ui.iot.boards import views + + +urlpatterns = [ + url(r'^$', views.IndexView.as_view(), name='index'), + url(r'^create/$', views.CreateView.as_view(), name='create'), + url(r'^(?P[^/]+)/update/$', views.UpdateView.as_view(), + name='update'), + url(r'^(?P[^/]+)/removeplugins/$', + views.RemovePluginsView.as_view(), name='removeplugins'), + url(r'^(?P[^/]+)/detail/$', views.BoardDetailView.as_view(), + name='detail'), +] diff --git a/iotronic_ui/iot/boards/views.py b/iotronic_ui/iot/boards/views.py new file mode 100644 index 0000000..961f222 --- /dev/null +++ b/iotronic_ui/iot/boards/views.py @@ -0,0 +1,329 @@ +# 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 logging + +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 messages +from horizon import tables +from horizon import tabs +from horizon.utils import memoized + +from openstack_dashboard.api import iotronic +from openstack_dashboard import policy + +from iotronic_ui.iot.boards import forms as project_forms +from iotronic_ui.iot.boards import tables as project_tables +from iotronic_ui.iot.boards import tabs as project_tabs + + +LOG = logging.getLogger(__name__) + + +class IndexView(tables.DataTableView): + table_class = project_tables.BoardsTable + template_name = 'iot/boards/index.html' + page_title = _("Boards") + + def get_data(self): + boards = [] + + # FROM + """ + if policy.check((("identity", "identity:list_roles"),), self.request): + try: + boards = iotronic.board_list(self.request, None, None) + # LOG.debug('IOT BOARDS: %s', boards) + except Exception: + exceptions.handle(self.request, + _('Unable to retrieve boards list.')) + else: + msg = _("Insufficient privilege level to view boards information.") + messages.info(self.request, msg) + """ + + # TO + # Admin + if policy.check((("iot", "iot:list_all_boards"),), self.request): + try: + boards = iotronic.board_list(self.request, None, None) + + except Exception: + exceptions.handle(self.request, + _('Unable to retrieve boards list.')) + + # Admin_iot_project + elif policy.check((("iot", "iot:list_project_boards"),), self.request): + try: + boards = iotronic.board_list(self.request, None, None) + + except Exception: + exceptions.handle(self.request, + _('Unable to retrieve user boards list.')) + + # Other users + else: + try: + boards = iotronic.board_list(self.request, None, None) + + except Exception: + exceptions.handle(self.request, + _('Unable to retrieve user boards list.')) + + return boards + + +class CreateView(forms.ModalFormView): + template_name = 'iot/boards/create.html' + modal_header = _("Create Board") + form_id = "create_board_form" + form_class = project_forms.CreateBoardForm + submit_label = _("Create Board") + submit_url = reverse_lazy("horizon:iot:boards:create") + success_url = reverse_lazy('horizon:iot:boards:index') + page_title = _("Create Board") + + +class UpdateView(forms.ModalFormView): + template_name = 'iot/boards/update.html' + modal_header = _("Update Board") + form_id = "update_board_form" + form_class = project_forms.UpdateBoardForm + submit_label = _("Update Board") + submit_url = "horizon:iot:boards:update" + success_url = reverse_lazy('horizon:iot:boards:index') + page_title = _("Update Board") + + @memoized.memoized_method + def get_object(self): + try: + return iotronic.board_get(self.request, self.kwargs['board_id'], + None) + except Exception: + redirect = reverse("horizon:iot:boards:index") + exceptions.handle(self.request, + _('Unable to update board.'), + redirect=redirect) + + def get_context_data(self, **kwargs): + context = super(UpdateView, self).get_context_data(**kwargs) + args = (self.get_object().uuid,) + context['submit_url'] = reverse(self.submit_url, args=args) + return context + + def get_initial(self): + board = self.get_object() + + # LOG.debug("MELO BOARD INFO: %s", board) + location = board.location[0] + + return {'uuid': board.uuid, + 'name': board.name, + 'mobile': board.mobile, + 'owner': board.owner, + 'latitude': location["latitude"], + 'longitude': location["longitude"], + 'altitude': location["altitude"]} + + +class RemovePluginsView(forms.ModalFormView): + template_name = 'iot/boards/removeplugins.html' + modal_header = _("Remove Plugins from board") + form_id = "remove_boardplugins_form" + form_class = project_forms.RemovePluginsForm + submit_label = _("Remove Plugins from board") + # submit_url = reverse_lazy("horizon:iot:boards:removeplugins") + submit_url = "horizon:iot:boards:removeplugins" + success_url = reverse_lazy('horizon:iot:boards:index') + page_title = _("Remove Plugins from board") + + @memoized.memoized_method + def get_object(self): + try: + return iotronic.board_get(self.request, self.kwargs['board_id'], + None) + except Exception: + redirect = reverse("horizon:iot:boards:index") + exceptions.handle(self.request, + _('Unable to remove plugin.'), + redirect=redirect) + + def get_context_data(self, **kwargs): + context = super(RemovePluginsView, self).get_context_data(**kwargs) + args = (self.get_object().uuid,) + context['submit_url'] = reverse(self.submit_url, args=args) + return context + + def get_initial(self): + board = self.get_object() + + # Populate plugins + # TO BE DONE.....filter by available on this board!!! + # plugins = iotronic.plugin_list(self.request, None, None) + plugins = iotronic.plugins_on_board(self.request, board.uuid) + + plugins.sort(key=lambda b: b.name) + + plugin_list = [] + for plugin in plugins: + plugin_list.append((plugin.uuid, _(plugin.name))) + + return {'uuid': board.uuid, + 'name': board.name, + 'plugin_list': plugin_list} + + +class DetailView(tabs.TabView): + # FROM + """ + tab_group_class = project_tabs.InstanceDetailTabs + template_name = 'horizon/common/_detail.html' + redirect_url = 'horizon:project:instances:index' + page_title = "{{ instance.name|default:instance.id }}" + image_url = 'horizon:project:images:images:detail' + volume_url = 'horizon:project:volumes:volumes:detail' + """ + # TO + tab_group_class = project_tabs.BoardDetailTabs + template_name = 'horizon/common/_detail.html' + page_title = "{{ board.name|default:board.uuid }}" + + def get_context_data(self, **kwargs): + context = super(DetailView, self).get_context_data(**kwargs) + # FROM + """ + instance = self.get_data() + if instance.image: + instance.image_url = reverse(self.image_url, + args=[instance.image['id']]) + instance.volume_url = self.volume_url + context["instance"] = instance + context["url"] = reverse(self.redirect_url) + context["actions"] = self._get_actions(instance) + """ + # TO + board = self.get_data() + context["board"] = board + context["url"] = reverse(self.redirect_url) + context["actions"] = self._get_actions(board) + + return context + + # FROM + """ + def _get_actions(self, instance): + table = project_tables.InstancesTable(self.request) + return table.render_row_actions(instance) + """ + # TO + def _get_actions(self, board): + table = project_tables.BoardsTable(self.request) + return table.render_row_actions(board) + + # FROM + """ + @memoized.memoized_method + def get_data(self): + instance_id = self.kwargs['instance_id'] + + try: + instance = api.nova.server_get(self.request, instance_id) + except Exception: + redirect = reverse(self.redirect_url) + exceptions.handle(self.request, + _('Unable to retrieve details for ' + 'instance "%s".') % instance_id, + redirect=redirect) + # Not all exception types handled above will result in a redirect. + # Need to raise here just in case. + raise exceptions.Http302(redirect) + + choices = project_tables.STATUS_DISPLAY_CHOICES + instance.status_label = ( + filters.get_display_label(choices, instance.status)) + + try: + instance.volumes = api.nova.instance_volumes_list(self.request, + instance_id) + # Sort by device name + instance.volumes.sort(key=lambda vol: vol.device) + except Exception: + msg = _('Unable to retrieve volume list for instance ' + '"%(name)s" (%(id)s).') % {'name': instance.name, + 'id': instance_id} + exceptions.handle(self.request, msg, ignore=True) + + try: + instance.full_flavor = api.nova.flavor_get( + self.request, instance.flavor["id"]) + except Exception: + msg = _('Unable to retrieve flavor information for instance ' + '"%(name)s" (%(id)s).') % {'name': instance.name, + 'id': instance_id} + exceptions.handle(self.request, msg, ignore=True) + + try: + instance.security_groups = api.network.server_security_groups( + self.request, instance_id) + except Exception: + msg = _('Unable to retrieve security groups for instance ' + '"%(name)s" (%(id)s).') % {'name': instance.name, + 'id': instance_id} + exceptions.handle(self.request, msg, ignore=True) + + try: + api.network.servers_update_addresses(self.request, [instance]) + except Exception: + msg = _('Unable to retrieve IP addresses from Neutron for ' + 'instance "%(name)s" (%(id)s).') % {'name': instance.name, + 'id': instance_id} + exceptions.handle(self.request, msg, ignore=True) + + return instance + """ + + # TO + @memoized.memoized_method + def get_data(self): + board_id = self.kwargs['board_id'] + try: + board = iotronic.board_get(self.request, board_id, None) + except Exception: + msg = ('Unable to retrieve board %s information') % {'name': + board.name} + exceptions.handle(self.request, msg, ignore=True) + return board + + # FROM + """ + def get_tabs(self, request, *args, **kwargs): + instance = self.get_data() + return self.tab_group_class(request, instance=instance, **kwargs) + """ + + # TO + def get_tabs(self, request, *args, **kwargs): + board = self.get_data() + return self.tab_group_class(request, board=board, **kwargs) + + +class BoardDetailView(DetailView): + redirect_url = 'horizon:iot:boards:index' + + def _get_actions(self, board): + table = project_tables.BoardsTable(self.request) + return table.render_row_actions(board) diff --git a/iotronic_ui/iot/dashboard.py b/iotronic_ui/iot/dashboard.py new file mode 100644 index 0000000..a84e97e --- /dev/null +++ b/iotronic_ui/iot/dashboard.py @@ -0,0 +1,26 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.utils.translation import ugettext_lazy as _ + +import horizon + + +class Iot(horizon.Dashboard): + name = _("Iot") + slug = "iot" + panels = ('boards', 'plugins') # Add your panels here. + + # Specify the slug of the dashboard's default panel. + default_panel = 'boards' + +horizon.register(Iot) diff --git a/iotronic_ui/iot/plugins/__init__.py b/iotronic_ui/iot/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iotronic_ui/iot/plugins/forms.py b/iotronic_ui/iot/plugins/forms.py new file mode 100644 index 0000000..8ef14ba --- /dev/null +++ b/iotronic_ui/iot/plugins/forms.py @@ -0,0 +1,495 @@ +# 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 cPickle +import json +import logging + +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +# START FROM HERE !!!!!!! openstack_dashboard/api/nova.py +# from iotronicclient.common.apiclient import exceptions as iot_exceptions + +from horizon import forms +from horizon import messages + +from openstack_dashboard.api import iotronic +from openstack_dashboard import policy + +LOG = logging.getLogger(__name__) + + +class CreatePluginForm(forms.SelfHandlingForm): + name = forms.CharField(label=_("Plugin Name")) + + """ + public = forms.ChoiceField( + label=_("Public"), + choices =[('false', _('False')), ('true', _('True'))], + widget=forms.Select( + attrs={'class': 'switchable', 'data-slug': 'slug-public'}, + ) + ) + """ + + public = forms.BooleanField(label=_("Public"), required=False) + callable = forms.BooleanField(label=_("Callable"), required=False) + + code = forms.CharField( + label=_("Code"), + widget=forms.Textarea( + attrs={'class': 'switchable', 'data-slug': 'slug-code'}) + ) + + parameters = forms.CharField( + label=_("Parameters"), + required=False, + widget=forms.Textarea( + attrs={'class': 'switchable', + 'data-slug': 'slug-parameters-create'}), + help_text=_("Plugin parameters") + ) + + def handle(self, request, data): + + if not data["parameters"]: + data["parameters"] = {} + else: + data["parameters"] = json.loads(data["parameters"]) + + try: + plugin = iotronic.plugin_create(request, data["name"], + data["public"], data["callable"], + data["code"], data["parameters"]) + LOG.debug("MELO API REQ: %s", request) + + messages.success(request, _("Plugin created successfully.")) + + return plugin + # except iot_exceptions.ClientException: + except Exception: + # LOG.debug("MELO API REQ EXC: %s", request) + # LOG.debug("MELO API REQ (DICT): %s", exceptions.__dict__) + exceptions.handle(request, _('Unable to create plugin.')) + + +class InjectPluginForm(forms.SelfHandlingForm): + + uuid = forms.CharField(label=_("Plugin ID"), widget=forms.HiddenInput) + + name = forms.CharField( + label=_('Plugin Name'), + widget=forms.TextInput(attrs={'readonly': 'readonly'}) + ) + + onboot = forms.BooleanField(label=_("On Boot"), required=False) + + board_list = forms.MultipleChoiceField( + label=_("Boards List"), + widget=forms.SelectMultiple( + attrs={'class': 'switchable', 'data-slug': 'slug-inject-boards'}), + help_text=_("Select boards in this pool ") + ) + + def __init__(self, *args, **kwargs): + + super(InjectPluginForm, self).__init__(*args, **kwargs) + # input=kwargs.get('initial',{}) + + boardslist_length = len(kwargs["initial"]["board_list"]) + + self.fields["board_list"].choices = kwargs["initial"]["board_list"] + self.fields["board_list"].max_length = boardslist_length + + def handle(self, request, data): + + counter = 0 + + for board in data["board_list"]: + for key, value in self.fields["board_list"].choices: + if key == board: + + try: + plugin = None + plugin = iotronic.plugin_inject(request, key, + data["uuid"], + data["onboot"]) + # LOG.debug("MELO API: %s %s", plugin, request) + message_text = "Plugin injected successfully on " \ + "board " + str(value) + "." + messages.success(request, _(message_text)) + + if counter != len(data["board_list"]) - 1: + counter += 1 + else: + return plugin + except Exception: + message_text = "Unable to inject plugin on board " \ + + str(value) + "." + exceptions.handle(request, _(message_text)) + + break + + +class StartPluginForm(forms.SelfHandlingForm): + + uuid = forms.CharField(label=_("Plugin ID"), widget=forms.HiddenInput) + + name = forms.CharField( + label=_('Plugin Name'), + widget=forms.TextInput(attrs={'readonly': 'readonly'}) + ) + + board_list = forms.MultipleChoiceField( + label=_("Boards List"), + widget=forms.SelectMultiple( + attrs={'class': 'switchable', + 'data-slug': 'slug-start-boards'}), + help_text=_("Select boards in this pool ") + ) + + parameters = forms.CharField( + label=_("Parameters"), + required=False, + widget=forms.Textarea( + attrs={'class': 'switchable', + 'data-slug': 'slug-startplugin-json'}), + help_text=_("Plugin parameters") + ) + + def __init__(self, *args, **kwargs): + + super(StartPluginForm, self).__init__(*args, **kwargs) + # input=kwargs.get('initial',{}) + + boardslist_length = len(kwargs["initial"]["board_list"]) + + self.fields["board_list"].choices = kwargs["initial"]["board_list"] + self.fields["board_list"].max_length = boardslist_length + + def handle(self, request, data): + + counter = 0 + + if not data["parameters"]: + data["parameters"] = {} + else: + data["parameters"] = json.loads(data["parameters"]) + + for board in data["board_list"]: + for key, value in self.fields["board_list"].choices: + if key == board: + + try: + plugin = None + plugin = iotronic.plugin_action(request, key, + data["uuid"], + "PluginStart", + data["parameters"]) + # LOG.debug("MELO API: %s %s", plugin, request) + message_text = "Plugin started successfully on board "\ + + str(value) + "." + messages.success(request, _(message_text)) + + if counter != len(data["board_list"]) - 1: + counter += 1 + else: + return plugin + except Exception: + message_text = "Unable to start plugin on board " \ + + str(value) + "." + exceptions.handle(request, _(message_text)) + + break + + +class StopPluginForm(forms.SelfHandlingForm): + + uuid = forms.CharField(label=_("Plugin ID"), widget=forms.HiddenInput) + + name = forms.CharField( + label=_('Plugin Name'), + widget=forms.TextInput(attrs={'readonly': 'readonly'}) + ) + + delay = forms.IntegerField( + label=_("Delay in secs"), + required=False, + help_text=_("OPTIONAL: seconds to wait before stopping the plugin") + ) + + board_list = forms.MultipleChoiceField( + label=_("Boards List"), + widget=forms.SelectMultiple( + attrs={'class': 'switchable', 'data-slug': 'slug-stop-boards'}), + help_text=_("Select boards in this pool ") + ) + + def __init__(self, *args, **kwargs): + + super(StopPluginForm, self).__init__(*args, **kwargs) + # input=kwargs.get('initial',{}) + + boardslist_length = len(kwargs["initial"]["board_list"]) + + self.fields["board_list"].choices = kwargs["initial"]["board_list"] + self.fields["board_list"].max_length = boardslist_length + + def handle(self, request, data): + + counter = 0 + + if not data["delay"]: + data["delay"] = {} + else: + data["delay"] = {"delay": data["delay"]} + + for board in data["board_list"]: + for key, value in self.fields["board_list"].choices: + if key == board: + + try: + plugin = None + plugin = iotronic.plugin_action(request, key, + data["uuid"], + "PluginStop", + data["delay"]) + # LOG.debug("MELO API: %s %s", plugin, request) + message_text = "Plugin stopped successfully on board "\ + + str(value) + "." + messages.success(request, _(message_text)) + + if counter != len(data["board_list"]) - 1: + counter += 1 + else: + return plugin + except Exception: + message_text = "Unable to stop plugin on board " \ + + str(value) + "." + exceptions.handle(request, _(message_text)) + + break + + +class CallPluginForm(forms.SelfHandlingForm): + + uuid = forms.CharField(label=_("Plugin ID"), widget=forms.HiddenInput) + + name = forms.CharField( + label=_('Plugin Name'), + widget=forms.TextInput(attrs={'readonly': 'readonly'}) + ) + + board_list = forms.MultipleChoiceField( + label=_("Boards List"), + widget=forms.SelectMultiple( + attrs={'class': 'switchable', 'data-slug': 'slug-call-boards'}), + help_text=_("Select boards in this pool ") + ) + + parameters = forms.CharField( + label=_("Parameters"), + required=False, + widget=forms.Textarea( + attrs={'class': 'switchable', + 'data-slug': 'slug-callplugin-json'}), + help_text=_("Plugin parameters") + ) + + def __init__(self, *args, **kwargs): + + super(CallPluginForm, self).__init__(*args, **kwargs) + # input=kwargs.get('initial',{}) + + boardslist_length = len(kwargs["initial"]["board_list"]) + + self.fields["board_list"].choices = kwargs["initial"]["board_list"] + self.fields["board_list"].max_length = boardslist_length + + def handle(self, request, data): + + counter = 0 + + if not data["parameters"]: + data["parameters"] = {} + else: + data["parameters"] = json.loads(data["parameters"]) + + for board in data["board_list"]: + for key, value in self.fields["board_list"].choices: + if key == board: + + try: + plugin = None + plugin = iotronic.plugin_action(request, key, + data["uuid"], + "PluginCall", + data["parameters"]) + # LOG.debug("MELO API: %s %s", plugin, request) + message_text = "Plugin called successfully on board " \ + + str(value) + "." + messages.success(request, _(message_text)) + + if counter != len(data["board_list"]) - 1: + counter += 2 + else: + return plugin + except Exception: + message_text = "Unable to call plugin on board " \ + + str(value) + "." + exceptions.handle(request, _(message_text)) + + break + + +class RemovePluginForm(forms.SelfHandlingForm): + + uuid = forms.CharField(label=_("Plugin ID"), widget=forms.HiddenInput) + + name = forms.CharField( + label=_('Plugin Name'), + widget=forms.TextInput(attrs={'readonly': 'readonly'}) + ) + + board_list = forms.MultipleChoiceField( + label=_("Boards List"), + widget=forms.SelectMultiple( + attrs={'class': 'switchable', 'data-slug': 'slug-remove-boards'}), + help_text=_("Select boards in this pool ") + ) + + def __init__(self, *args, **kwargs): + + super(RemovePluginForm, self).__init__(*args, **kwargs) + # input=kwargs.get('initial',{}) + + boardslist_length = len(kwargs["initial"]["board_list"]) + + self.fields["board_list"].choices = kwargs["initial"]["board_list"] + self.fields["board_list"].max_length = boardslist_length + + def handle(self, request, data): + + counter = 0 + + for board in data["board_list"]: + for key, value in self.fields["board_list"].choices: + if key == board: + + try: + plugin = None + plugin = iotronic.plugin_remove(request, key, + data["uuid"]) + # LOG.debug("MELO API: %s %s", plugin, request) + message_text = "Plugin removed successfully from" \ + + " board " + str(value) + "." + messages.success(request, _(message_text)) + + if counter != len(data["board_list"]) - 1: + counter += 1 + else: + return plugin + except Exception: + message_text = "Unable to remove plugin from board " \ + + str(value) + "." + exceptions.handle(request, _(message_text)) + + break + + +class UpdatePluginForm(forms.SelfHandlingForm): + + uuid = forms.CharField(label=_("Plugin ID"), widget=forms.HiddenInput) + owner = forms.CharField(label=_("Owner"), widget=forms.HiddenInput) + + """ + name = forms.CharField(label=_("Plugin Name")) + public = forms.ChoiceField(label=_("Public")) + callable = forms.ChoiceField(label=_("Callable")) + code = forms.CharField(label=_("Code")) + """ + + name = forms.CharField(label=_("Plugin Name")) + + """ + public = forms.ChoiceField( + label=_("Public"), + choices =[('false', _('False')), ('true', _('True'))], + widget=forms.Select( + attrs={'class': 'switchable', 'data-slug': 'slug-public'}, + ) + ) + + + callable = forms.ChoiceField( + label=_("Callable"), + choices =[('false', _('False')), ('true', _('True'))], + widget=forms.Select( + attrs={'class': 'switchable', 'data-slug': 'slug-callable'}, + ) + ) + """ + public = forms.BooleanField(label=_("Public"), required=False) + callable = forms.BooleanField(label=_("Callable"), required=False) + + code = forms.CharField( + label=_("Code"), + widget=forms.Textarea( + attrs={'class': 'switchable', 'data-slug': 'slug-code'}) + ) + + def __init__(self, *args, **kwargs): + + super(UpdatePluginForm, self).__init__(*args, **kwargs) + + # Admin + if policy.check((("iot", "iot:update_plugins"),), self.request): + # LOG.debug("MELO ADMIN") + pass + + # Admin_iot_project + elif policy.check((("iot", "iot:update_project_plugins"),), + self.request): + # LOG.debug("MELO IOT ADMIN") + + if self.request.user.id != kwargs["initial"]["owner"]: + # LOG.debug("MELO NO-edit IOT ADMIN") + self.fields["name"].widget.attrs = {'readonly': 'readonly'} + self.fields["public"].widget.attrs = {'disabled': 'disabled'} + self.fields["callable"].widget.attrs = {'disabled': 'disabled'} + self.fields["code"].widget.attrs = {'readonly': 'readonly'} + + # Other users + else: + if self.request.user.id != kwargs["initial"]["owner"]: + # LOG.debug("MELO IMMUTABLE FIELDS") + self.fields["name"].widget.attrs = {'readonly': 'readonly'} + self.fields["public"].widget.attrs = {'disabled': 'disabled'} + self.fields["callable"].widget.attrs = {'disabled': 'disabled'} + self.fields["code"].widget.attrs = {'readonly': 'readonly'} + + def handle(self, request, data): + try: + + # LOG.debug("MELO DATA: %s", data) + data["code"] = cPickle.dumps(str(data["code"])) + + iotronic.plugin_update(request, data["uuid"], + {"name": data["name"], + "public": data["public"], + "callable": data["callable"], + "code": data["code"]}) + messages.success(request, _("Plugin updated successfully.")) + return True + except Exception: + exceptions.handle(request, _('Unable to update plugin.')) diff --git a/iotronic_ui/iot/plugins/panel.py b/iotronic_ui/iot/plugins/panel.py new file mode 100644 index 0000000..a099a3d --- /dev/null +++ b/iotronic_ui/iot/plugins/panel.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.utils.translation import ugettext_lazy as _ + +import horizon + + +class Plugins(horizon.Panel): + name = _("Plugins") + slug = "plugins" + # policy_rules = (("iot", "iot:list_all_plugins"), + # ("iot", "iot:list_project_plugins")) diff --git a/iotronic_ui/iot/plugins/tables.py b/iotronic_ui/iot/plugins/tables.py new file mode 100644 index 0000000..464ec46 --- /dev/null +++ b/iotronic_ui/iot/plugins/tables.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. + +import logging + +from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ungettext_lazy + +from horizon import tables + +from openstack_dashboard import api + +LOG = logging.getLogger(__name__) + + +class CreatePluginLink(tables.LinkAction): + name = "create" + verbose_name = _("Create Plugin") + url = "horizon:iot:plugins:create" + classes = ("ajax-modal",) + icon = "plus" + # policy_rules = (("iot", "iot:create_board"),) + + +class EditPluginLink(tables.LinkAction): + name = "edit" + verbose_name = _("Edit") + url = "horizon:iot:plugins:update" + classes = ("ajax-modal",) + icon = "pencil" + # policy_rules = (("iot", "iot:update_board"),) + + """ + def allowed(self, request, plugin): + # LOG.debug("MELO ALLOWED: %s %s %s", self, request, plugin) + # LOG.debug("MELO user: %s", request.user.id) + + return True + """ + + +class InjectPluginLink(tables.LinkAction): + name = "inject" + verbose_name = _("Inject Plugin") + url = "horizon:iot:plugins:inject" + classes = ("ajax-modal",) + icon = "plus" + # policy_rules = (("iot", "iot:create_board"),) + + +class StartPluginLink(tables.LinkAction): + name = "start" + verbose_name = _("Start Plugin") + url = "horizon:iot:plugins:start" + classes = ("ajax-modal",) + icon = "plus" + # policy_rules = (("iot", "iot:create_board"),) + + +class StopPluginLink(tables.LinkAction): + name = "stop" + verbose_name = _("Stop Plugin") + url = "horizon:iot:plugins:stop" + classes = ("ajax-modal",) + icon = "plus" + # policy_rules = (("iot", "iot:create_board"),) + + +class CallPluginLink(tables.LinkAction): + name = "call" + verbose_name = _("Call Plugin") + url = "horizon:iot:plugins:call" + classes = ("ajax-modal",) + icon = "plus" + # policy_rules = (("iot", "iot:create_board"),) + + +class RemovePluginLink(tables.LinkAction): + name = "remove" + verbose_name = _("Remove Plugin from board(s)") + url = "horizon:iot:plugins:remove" + classes = ("ajax-modal",) + icon = "plus" + # policy_rules = (("iot", "iot:create_board"),) + + +class DeletePluginsAction(tables.DeleteAction): + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Delete Plugin", + u"Delete Plugins", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Deleted Plugin", + u"Deleted Plugins", + count + ) + # policy_rules = (("iot", "iot:delete_board"),) + + """ + def allowed(self, request, role): + return api.keystone.keystone_can_edit_role() + """ + + def delete(self, request, plugin_id): + api.iotronic.plugin_delete(request, plugin_id) + + +class PluginFilterAction(tables.FilterAction): + + def filter(self, table, plugins, filter_string): + """Naive case-insensitive search.""" + q = filter_string.lower() + return [plugin for plugin in plugins + if q in plugin.name.lower()] + + +class PluginsTable(tables.DataTable): + name = tables.WrappingColumn('name', link="horizon:iot:plugins:detail", + verbose_name=_('Plugin Name')) + uuid = tables.Column('uuid', verbose_name=_('Plugin ID')) + owner = tables.Column('owner', verbose_name=_('Owner')) + public = tables.Column('public', verbose_name=_('Public')) + callable = tables.Column('callable', verbose_name=_('Callable')) + + # Overriding get_object_id method because in IoT service the "id" is + # identified by the field UUID + def get_object_id(self, datum): + # LOG.debug("MELO datum %s", datum) + return datum.uuid + + # Overriding get_row_actions method because we need to discriminate + # between Sync and Async plugins + def get_row_actions(self, datum): + actions = super(PluginsTable, self).get_row_actions(datum) + # LOG.debug("MELO ACTIONS: %s %s", actions[0].name, datum.name) + + selected_row_actions = [] + + common_actions = ["edit", "inject", "remove", "delete"] + + for action in actions: + if action.name in common_actions: + selected_row_actions.append(action) + + elif datum.callable == True and action.name == "call": + selected_row_actions.append(action) + + elif datum.callable == False and action.name != "call": + selected_row_actions.append(action) + + return selected_row_actions + + class Meta(object): + name = "plugins" + verbose_name = _("plugins") + + row_actions = (EditPluginLink, InjectPluginLink, StartPluginLink, + StopPluginLink, CallPluginLink, RemovePluginLink, + DeletePluginsAction,) + table_actions = (PluginFilterAction, CreatePluginLink, + DeletePluginsAction) diff --git a/iotronic_ui/iot/plugins/tabs.py b/iotronic_ui/iot/plugins/tabs.py new file mode 100644 index 0000000..de5fec0 --- /dev/null +++ b/iotronic_ui/iot/plugins/tabs.py @@ -0,0 +1,36 @@ +# 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 logging + +from django.utils.translation import ugettext_lazy as _ + +from horizon import tabs + +LOG = logging.getLogger(__name__) + + +class OverviewTab(tabs.Tab): + name = _("Overview") + slug = "overview" + template_name = ("iot/plugins/_detail_overview.html") + + def get_context_data(self, request): + return {"plugin": self.tab_group.kwargs['plugin'], + "is_superuser": request.user.is_superuser} + + +class PluginDetailTabs(tabs.TabGroup): + slug = "plugin_details" + # tabs = (OverviewTab, LogTab, ConsoleTab, AuditTab) + tabs = (OverviewTab,) + sticky = True diff --git a/iotronic_ui/iot/plugins/templates/plugins/_call.html b/iotronic_ui/iot/plugins/templates/plugins/_call.html new file mode 100644 index 0000000..ad15c71 --- /dev/null +++ b/iotronic_ui/iot/plugins/templates/plugins/_call.html @@ -0,0 +1,7 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Call a plugin on board(s)." %}

+{% endblock %} diff --git a/iotronic_ui/iot/plugins/templates/plugins/_create.html b/iotronic_ui/iot/plugins/templates/plugins/_create.html new file mode 100644 index 0000000..c1cdcd2 --- /dev/null +++ b/iotronic_ui/iot/plugins/templates/plugins/_create.html @@ -0,0 +1,7 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Add a new plugin into the Cloud." %}

+{% endblock %} diff --git a/iotronic_ui/iot/plugins/templates/plugins/_detail_overview.html b/iotronic_ui/iot/plugins/templates/plugins/_detail_overview.html new file mode 100644 index 0000000..8906b44 --- /dev/null +++ b/iotronic_ui/iot/plugins/templates/plugins/_detail_overview.html @@ -0,0 +1,16 @@ +{% load i18n sizeformat %} + +
+
+
{% trans "Name" %}
+
{{ plugin.name }}
+
{% trans "ID" %}
+
{{ plugin.uuid }}
+
{% trans "Public" %}
+
{{ plugin.public }}
+
{% trans "Callable" %}
+
{{ plugin.callable }}
+
{% trans "Code" %}
+
{{ plugin.code }}
+
+
diff --git a/iotronic_ui/iot/plugins/templates/plugins/_inject.html b/iotronic_ui/iot/plugins/templates/plugins/_inject.html new file mode 100644 index 0000000..f4ec1e0 --- /dev/null +++ b/iotronic_ui/iot/plugins/templates/plugins/_inject.html @@ -0,0 +1,7 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Inject a plugin into board(s)." %}

+{% endblock %} diff --git a/iotronic_ui/iot/plugins/templates/plugins/_remove.html b/iotronic_ui/iot/plugins/templates/plugins/_remove.html new file mode 100644 index 0000000..79aa0be --- /dev/null +++ b/iotronic_ui/iot/plugins/templates/plugins/_remove.html @@ -0,0 +1,7 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Remove a plugin from board(s)." %}

+{% endblock %} diff --git a/iotronic_ui/iot/plugins/templates/plugins/_start.html b/iotronic_ui/iot/plugins/templates/plugins/_start.html new file mode 100644 index 0000000..07df81b --- /dev/null +++ b/iotronic_ui/iot/plugins/templates/plugins/_start.html @@ -0,0 +1,7 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Start a plugin on board(s)." %}

+{% endblock %} diff --git a/iotronic_ui/iot/plugins/templates/plugins/_stop.html b/iotronic_ui/iot/plugins/templates/plugins/_stop.html new file mode 100644 index 0000000..d72d9f8 --- /dev/null +++ b/iotronic_ui/iot/plugins/templates/plugins/_stop.html @@ -0,0 +1,7 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Stop a plugin on board(s)." %}

+{% endblock %} diff --git a/iotronic_ui/iot/plugins/templates/plugins/_update.html b/iotronic_ui/iot/plugins/templates/plugins/_update.html new file mode 100644 index 0000000..2233fcb --- /dev/null +++ b/iotronic_ui/iot/plugins/templates/plugins/_update.html @@ -0,0 +1,7 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Edit the plugin's details." %}

+{% endblock %} diff --git a/iotronic_ui/iot/plugins/templates/plugins/call.html b/iotronic_ui/iot/plugins/templates/plugins/call.html new file mode 100644 index 0000000..531077a --- /dev/null +++ b/iotronic_ui/iot/plugins/templates/plugins/call.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Call Plugin" %}{% endblock %} + +{% block main %} + {% include 'iot/plugins/_call.html' %} +{% endblock %} diff --git a/iotronic_ui/iot/plugins/templates/plugins/create.html b/iotronic_ui/iot/plugins/templates/plugins/create.html new file mode 100644 index 0000000..f056540 --- /dev/null +++ b/iotronic_ui/iot/plugins/templates/plugins/create.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Insert Plugin" %}{% endblock %} + +{% block main %} + {% include 'iot/plugins/_create.html' %} +{% endblock %} diff --git a/iotronic_ui/iot/plugins/templates/plugins/index.html b/iotronic_ui/iot/plugins/templates/plugins/index.html new file mode 100644 index 0000000..c4c0458 --- /dev/null +++ b/iotronic_ui/iot/plugins/templates/plugins/index.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Plugins" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Plugins") %} +{% endblock page_header %} + +{% block main %} + {{ table.render }} +{% endblock %} + diff --git a/iotronic_ui/iot/plugins/templates/plugins/inject.html b/iotronic_ui/iot/plugins/templates/plugins/inject.html new file mode 100644 index 0000000..247d683 --- /dev/null +++ b/iotronic_ui/iot/plugins/templates/plugins/inject.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Inject Plugin" %}{% endblock %} + +{% block main %} + {% include 'iot/plugins/_inject.html' %} +{% endblock %} diff --git a/iotronic_ui/iot/plugins/templates/plugins/remove.html b/iotronic_ui/iot/plugins/templates/plugins/remove.html new file mode 100644 index 0000000..d684294 --- /dev/null +++ b/iotronic_ui/iot/plugins/templates/plugins/remove.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Remove Plugin" %}{% endblock %} + +{% block main %} + {% include 'iot/plugins/_remove.html' %} +{% endblock %} diff --git a/iotronic_ui/iot/plugins/templates/plugins/start.html b/iotronic_ui/iot/plugins/templates/plugins/start.html new file mode 100644 index 0000000..0d6b488 --- /dev/null +++ b/iotronic_ui/iot/plugins/templates/plugins/start.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Start Plugin" %}{% endblock %} + +{% block main %} + {% include 'iot/plugins/_start.html' %} +{% endblock %} diff --git a/iotronic_ui/iot/plugins/templates/plugins/stop.html b/iotronic_ui/iot/plugins/templates/plugins/stop.html new file mode 100644 index 0000000..bdf8f3b --- /dev/null +++ b/iotronic_ui/iot/plugins/templates/plugins/stop.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Stop Plugin" %}{% endblock %} + +{% block main %} + {% include 'iot/plugins/_stop.html' %} +{% endblock %} diff --git a/iotronic_ui/iot/plugins/templates/plugins/update.html b/iotronic_ui/iot/plugins/templates/plugins/update.html new file mode 100644 index 0000000..f0919eb --- /dev/null +++ b/iotronic_ui/iot/plugins/templates/plugins/update.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Update Plugin" %}{% endblock %} + +{% block main %} + {% include 'iot/plugins/_update.html' %} +{% endblock %} diff --git a/iotronic_ui/iot/plugins/tests.py b/iotronic_ui/iot/plugins/tests.py new file mode 100644 index 0000000..a1e1acd --- /dev/null +++ b/iotronic_ui/iot/plugins/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 horizon.test import helpers as test + + +class PluginsTests(test.TestCase): + # Unit tests for plugins. + def test_me(self): + self.assertTrue(1 + 1 == 2) diff --git a/iotronic_ui/iot/plugins/urls.py b/iotronic_ui/iot/plugins/urls.py new file mode 100644 index 0000000..1894504 --- /dev/null +++ b/iotronic_ui/iot/plugins/urls.py @@ -0,0 +1,35 @@ +# 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 iotronic_ui.iot.plugins import views + + +urlpatterns = [ + url(r'^$', views.IndexView.as_view(), name='index'), + url(r'^create/$', views.CreateView.as_view(), name='create'), + url(r'^(?P[^/]+)/inject/$', views.InjectView.as_view(), + name='inject'), + url(r'^(?P[^/]+)/start/$', views.StartView.as_view(), + name='start'), + url(r'^(?P[^/]+)/stop/$', views.StopView.as_view(), + name='stop'), + url(r'^(?P[^/]+)/call/$', views.CallView.as_view(), + name='call'), + url(r'^(?P[^/]+)/remove/$', views.RemoveView.as_view(), + name='remove'), + url(r'^(?P[^/]+)/update/$', views.UpdateView.as_view(), + name='update'), + url(r'^(?P[^/]+)/detail/$', views.PluginDetailView.as_view(), + name='detail'), +] diff --git a/iotronic_ui/iot/plugins/views.py b/iotronic_ui/iot/plugins/views.py new file mode 100644 index 0000000..0a1dc6c --- /dev/null +++ b/iotronic_ui/iot/plugins/views.py @@ -0,0 +1,413 @@ +# 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 cPickle +import logging + +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 messages +from horizon import tables +from horizon import tabs +from horizon.utils import memoized + +from openstack_dashboard.api import iotronic, keystone +from openstack_dashboard import policy + +from iotronic_ui.iot.plugins import forms as project_forms +from iotronic_ui.iot.plugins import tables as project_tables +from iotronic_ui.iot.plugins import tabs as project_tabs + +LOG = logging.getLogger(__name__) + + +class IndexView(tables.DataTableView): + table_class = project_tables.PluginsTable + template_name = 'iot/plugins/index.html' + page_title = _("Plugins") + + def get_data(self): + plugins = [] + users = [] + + # Admin + if policy.check((("iot", "iot:list_all_plugins"),), self.request): + try: + plugins = iotronic.plugin_list(self.request, None, None, + all_plugins=True) + users = keystone.user_list(self.request) + + except Exception: + exceptions.handle(self.request, _('Unable to retrieve plugins \ + list.')) + + # Admin_iot_project + elif policy.check((("iot", "iot:list_project_plugins"),), + self.request): + try: + plugins = iotronic.plugin_list(self.request, None, None, + with_public=True) + + except Exception: + exceptions.handle(self.request, + _('Unable to retrieve user plugins list.')) + + # Other users + else: + # FROM + """ + msg = _("Insufficient privilege level to view + plugins information.") + messages.info(self.request, msg) + """ + # TO + try: + plugins = iotronic.plugin_list(self.request, None, None, + with_public=True) + + except Exception: + exceptions.handle(self.request, + _('Unable to retrieve user plugins list.')) + + # Replace owner column values (user.id) with user.name (only Admin + # can see human-readable names) + for plugin in plugins: + for user in users: + if plugin.owner == user.id: + plugin.owner = user.name + break + + return plugins + + +class CreateView(forms.ModalFormView): + template_name = 'iot/plugins/create.html' + modal_header = _("Create Plugin") + form_id = "create_plugin_form" + form_class = project_forms.CreatePluginForm + submit_label = _("Create Plugin") + submit_url = reverse_lazy("horizon:iot:plugins:create") + success_url = reverse_lazy('horizon:iot:plugins:index') + page_title = _("Create Plugin") + + +class InjectView(forms.ModalFormView): + template_name = 'iot/plugins/inject.html' + modal_header = _("Inject Plugin") + form_id = "inject_plugin_form" + form_class = project_forms.InjectPluginForm + submit_label = _("Inject Plugin") + # submit_url = reverse_lazy("horizon:iot:plugins:inject") + submit_url = "horizon:iot:plugins:inject" + success_url = reverse_lazy('horizon:iot:plugins:index') + page_title = _("Inject Plugin") + + @memoized.memoized_method + def get_object(self): + try: + return iotronic.plugin_get(self.request, self.kwargs['plugin_id'], + None) + except Exception: + redirect = reverse("horizon:iot:plugins:index") + exceptions.handle(self.request, + _('Unable to inject plugin.'), + redirect=redirect) + + def get_context_data(self, **kwargs): + context = super(InjectView, self).get_context_data(**kwargs) + args = (self.get_object().uuid,) + context['submit_url'] = reverse(self.submit_url, args=args) + return context + + def get_initial(self): + plugin = self.get_object() + + # Populate boards + boards = iotronic.board_list(self.request, "online", None, None) + boards.sort(key=lambda b: b.name) + + board_list = [] + for board in boards: + board_list.append((board.uuid, _(board.name))) + + return {'uuid': plugin.uuid, + 'name': plugin.name, + 'board_list': board_list} + + +class StartView(forms.ModalFormView): + template_name = 'iot/plugins/start.html' + modal_header = _("Start Plugin") + form_id = "start_plugin_form" + form_class = project_forms.StartPluginForm + submit_label = _("Start Plugin") + # submit_url = reverse_lazy("horizon:iot:plugins:start") + submit_url = "horizon:iot:plugins:start" + success_url = reverse_lazy('horizon:iot:plugins:index') + page_title = _("Start Plugin") + + @memoized.memoized_method + def get_object(self): + try: + return iotronic.plugin_get(self.request, self.kwargs['plugin_id'], + None) + except Exception: + redirect = reverse("horizon:iot:plugins:index") + exceptions.handle(self.request, + _('Unable to start plugin.'), + redirect=redirect) + + def get_context_data(self, **kwargs): + context = super(StartView, self).get_context_data(**kwargs) + args = (self.get_object().uuid,) + context['submit_url'] = reverse(self.submit_url, args=args) + return context + + def get_initial(self): + plugin = self.get_object() + + # Populate boards + boards = iotronic.board_list(self.request, "online", None, None) + boards.sort(key=lambda b: b.name) + + board_list = [] + for board in boards: + board_list.append((board.uuid, _(board.name))) + + return {'uuid': plugin.uuid, + 'name': plugin.name, + 'board_list': board_list} + + +class StopView(forms.ModalFormView): + template_name = 'iot/plugins/stop.html' + modal_header = _("Stop Plugin") + form_id = "stop_plugin_form" + form_class = project_forms.StopPluginForm + submit_label = _("Stop Plugin") + # submit_url = reverse_lazy("horizon:iot:plugins:stop") + submit_url = "horizon:iot:plugins:stop" + success_url = reverse_lazy('horizon:iot:plugins:index') + page_title = _("Stop Plugin") + + @memoized.memoized_method + def get_object(self): + try: + return iotronic.plugin_get(self.request, self.kwargs['plugin_id'], + None) + except Exception: + redirect = reverse("horizon:iot:plugins:index") + exceptions.handle(self.request, + _('Unable to stop plugin.'), + redirect=redirect) + + def get_context_data(self, **kwargs): + context = super(StopView, self).get_context_data(**kwargs) + args = (self.get_object().uuid,) + context['submit_url'] = reverse(self.submit_url, args=args) + return context + + def get_initial(self): + plugin = self.get_object() + + # Populate boards + boards = iotronic.board_list(self.request, "online", None, None) + boards.sort(key=lambda b: b.name) + + board_list = [] + for board in boards: + board_list.append((board.uuid, _(board.name))) + + return {'uuid': plugin.uuid, + 'name': plugin.name, + 'board_list': board_list} + + +class CallView(forms.ModalFormView): + template_name = 'iot/plugins/call.html' + modal_header = _("Call Plugin") + form_id = "call_plugin_form" + form_class = project_forms.CallPluginForm + submit_label = _("Call Plugin") + # submit_url = reverse_lazy("horizon:iot:plugins:call") + submit_url = "horizon:iot:plugins:call" + success_url = reverse_lazy('horizon:iot:plugins:index') + page_title = _("Call Plugin") + + @memoized.memoized_method + def get_object(self): + try: + return iotronic.plugin_get(self.request, self.kwargs['plugin_id'], + None) + except Exception: + redirect = reverse("horizon:iot:plugins:index") + exceptions.handle(self.request, + _('Unable to call plugin.'), + redirect=redirect) + + def get_context_data(self, **kwargs): + context = super(CallView, self).get_context_data(**kwargs) + args = (self.get_object().uuid,) + context['submit_url'] = reverse(self.submit_url, args=args) + return context + + def get_initial(self): + plugin = self.get_object() + + # Populate boards + boards = iotronic.board_list(self.request, "online", None, None) + boards.sort(key=lambda b: b.name) + + board_list = [] + for board in boards: + board_list.append((board.uuid, _(board.name))) + + return {'uuid': plugin.uuid, + 'name': plugin.name, + 'board_list': board_list} + + +class RemoveView(forms.ModalFormView): + template_name = 'iot/plugins/remove.html' + modal_header = _("Remove Plugin") + form_id = "remove_plugin_form" + form_class = project_forms.RemovePluginForm + submit_label = _("Remove Plugin") + # submit_url = reverse_lazy("horizon:iot:plugins:remove") + submit_url = "horizon:iot:plugins:remove" + success_url = reverse_lazy('horizon:iot:plugins:index') + page_title = _("Remove Plugin") + + @memoized.memoized_method + def get_object(self): + try: + return iotronic.plugin_get(self.request, self.kwargs['plugin_id'], + None) + except Exception: + redirect = reverse("horizon:iot:plugins:index") + exceptions.handle(self.request, + _('Unable to remove plugin.'), + redirect=redirect) + + def get_context_data(self, **kwargs): + context = super(RemoveView, self).get_context_data(**kwargs) + args = (self.get_object().uuid,) + context['submit_url'] = reverse(self.submit_url, args=args) + return context + + def get_initial(self): + plugin = self.get_object() + + # Populate boards + boards = iotronic.board_list(self.request, "online", None, None) + boards.sort(key=lambda b: b.name) + + board_list = [] + for board in boards: + board_list.append((board.uuid, _(board.name))) + + return {'uuid': plugin.uuid, + 'name': plugin.name, + 'board_list': board_list} + + +class UpdateView(forms.ModalFormView): + template_name = 'iot/plugins/update.html' + modal_header = _("Update Plugin") + form_id = "update_plugin_form" + form_class = project_forms.UpdatePluginForm + submit_label = _("Update Plugin") + submit_url = "horizon:iot:plugins:update" + success_url = reverse_lazy('horizon:iot:plugins:index') + page_title = _("Update Plugin") + + @memoized.memoized_method + def get_object(self): + try: + return iotronic.plugin_get(self.request, self.kwargs['plugin_id'], + None) + except Exception: + redirect = reverse("horizon:iot:plugins:index") + exceptions.handle(self.request, + _('Unable to update plugin.'), + redirect=redirect) + + def get_context_data(self, **kwargs): + context = super(UpdateView, self).get_context_data(**kwargs) + args = (self.get_object().uuid,) + context['submit_url'] = reverse(self.submit_url, args=args) + return context + + def get_initial(self): + plugin = self.get_object() + + # Deserialize the code of the plugin + plugin.code = cPickle.loads(str(plugin.code)) + + return {'uuid': plugin.uuid, + 'owner': plugin.owner, + 'name': plugin.name, + 'public': plugin.public, + 'callable': plugin.callable, + 'code': plugin.code} + + +class DetailView(tabs.TabView): + + tab_group_class = project_tabs.PluginDetailTabs + template_name = 'horizon/common/_detail.html' + page_title = "{{ plugin.name|default:plugin.uuid }}" + + def get_context_data(self, **kwargs): + context = super(DetailView, self).get_context_data(**kwargs) + + plugin = self.get_data() + context["plugin"] = plugin + context["url"] = reverse(self.redirect_url) + context["actions"] = self._get_actions(plugin) + + return context + + def _get_actions(self, plugin): + table = project_tables.PluginsTable(self.request) + return table.render_row_actions(plugin) + + @memoized.memoized_method + def get_data(self): + plugin_id = self.kwargs['plugin_id'] + try: + plugin = iotronic.plugin_get(self.request, plugin_id, None) + except Exception: + msg = ('Unable to retrieve plugin %s information') % {'name': + plugin.name} + exceptions.handle(self.request, msg, ignore=True) + return plugin + + def get_tabs(self, request, *args, **kwargs): + plugin = self.get_data() + return self.tab_group_class(request, plugin=plugin, **kwargs) + + +class PluginDetailView(DetailView): + redirect_url = 'horizon:iot:plugins:index' + + def _get_actions(self, plugin): + + # Deserialize the code of the plugin + plugin.code = cPickle.loads(str(plugin.code)) + + table = project_tables.PluginsTable(self.request) + return table.render_row_actions(plugin) diff --git a/iotronic_ui/iot/static/iot/images/blue-circle.png b/iotronic_ui/iot/static/iot/images/blue-circle.png new file mode 100644 index 0000000..3e1f389 Binary files /dev/null and b/iotronic_ui/iot/static/iot/images/blue-circle.png differ diff --git a/iotronic_ui/iot/static/iot/images/green-circle.png b/iotronic_ui/iot/static/iot/images/green-circle.png new file mode 100644 index 0000000..f639e78 Binary files /dev/null and b/iotronic_ui/iot/static/iot/images/green-circle.png differ diff --git a/iotronic_ui/iot/static/iot/images/marker-icon-green.png b/iotronic_ui/iot/static/iot/images/marker-icon-green.png new file mode 100644 index 0000000..56db5ea Binary files /dev/null and b/iotronic_ui/iot/static/iot/images/marker-icon-green.png differ diff --git a/iotronic_ui/iot/static/iot/images/marker-icon-red.png b/iotronic_ui/iot/static/iot/images/marker-icon-red.png new file mode 100644 index 0000000..3165a31 Binary files /dev/null and b/iotronic_ui/iot/static/iot/images/marker-icon-red.png differ diff --git a/iotronic_ui/iot/static/iot/images/marker-icon.png b/iotronic_ui/iot/static/iot/images/marker-icon.png new file mode 100644 index 0000000..e2e9f75 Binary files /dev/null and b/iotronic_ui/iot/static/iot/images/marker-icon.png differ diff --git a/iotronic_ui/iot/static/iot/images/marker-shadow.png b/iotronic_ui/iot/static/iot/images/marker-shadow.png new file mode 100644 index 0000000..d1e773c Binary files /dev/null and b/iotronic_ui/iot/static/iot/images/marker-shadow.png differ diff --git a/iotronic_ui/iot/static/iot/images/red-circle.png b/iotronic_ui/iot/static/iot/images/red-circle.png new file mode 100644 index 0000000..bb3bbdf Binary files /dev/null and b/iotronic_ui/iot/static/iot/images/red-circle.png differ diff --git a/iotronic_ui/iot/static/iot/js/iot.js b/iotronic_ui/iot/static/iot/js/iot.js new file mode 100644 index 0000000..97e3b8d --- /dev/null +++ b/iotronic_ui/iot/static/iot/js/iot.js @@ -0,0 +1,122 @@ +/* Additional JavaScript for iot. */ + +//alert('MELO'); + +//var image_url = 'https://ing-res-17.me.trigrid.it/iotronic/'; +var images_url = 'http://'+location.host+'/dashboard/static/iot/images/'; + +var markers = []; +markers = L.markerClusterGroup({ + spiderfyOnMaxZoom: false, + disableClusteringAtZoom: 17 +}); + + +var marker_red = L.icon({ + iconUrl: images_url+'marker-icon-red.png', + iconAnchor:[12.5, 41], + shadowUrl: images_url+'marker-shadow.png' +}); + + +var marker_green = L.icon({ + iconUrl: images_url+'marker-icon-green.png', + iconAnchor:[12.5, 41], + shadowUrl: images_url+'marker-shadow.png' +}); + + +var marker_blue = L.icon({ + iconUrl: images_url+'marker-icon.png', + iconAnchor:[12.5, 41], + shadowUrl: images_url+'marker-shadow.png' +}); + + +var labels = []; +var latitude = []; +var longitude = []; +var altitude = []; +var statuses = []; +var last_update = []; + + +function render_map(map_id, coordinates){ + var osmUrl='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; + var osm = new L.TileLayer(osmUrl, {}); + + var lat = 38.20523; + var lon = 15.55972; + if(coordinates["coordinates"].length ==1){ + lat = coordinates["coordinates"][0].lat; + lon = coordinates["coordinates"][0].lon; + } + var map = L.map(map_id, {scrollWheelZoom:false, worldCopyJump: true}).setView([lat, lon], 12); + map.addLayer(osm); + + //Copyright + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap - by MDSLab' + }).addTo(map); + + //var marker = L.marker([lat, lon]); + //marker.setIcon(marker_red); + coord = coordinates["coordinates"]; + for(var i=0;i'; + if(statuses[sel] == "online") + img = ''; + if(statuses[sel] == "offline") + img = ''; + + var open_popup = '
'; + + var default_popup = '
'+img +' '+labels[sel]+'

' + + '
'+last_update[sel]+'

'+ + 'Latitude: '+latitude[sel]+ '
' + + 'Longitude: '+longitude[sel]+'
' + + 'Altitude: '+altitude[sel]+'

'; + + global_popup = open_popup + default_popup +"
"; + var popup = L.popup().setLatLng(e.latlng).setContent(global_popup).openOn(map); + }); + markers.addLayer(marker); + } + map.addLayer(markers); + //return map; +} + + + +function choose_marker(status){ + if(status=="online") return marker_green; + else if(status =="offline") return marker_red; + else return marker_blue; +} diff --git a/iotronic_ui/iot/static/iot/scss/iot.scss b/iotronic_ui/iot/static/iot/scss/iot.scss new file mode 100644 index 0000000..e1bd25d --- /dev/null +++ b/iotronic_ui/iot/static/iot/scss/iot.scss @@ -0,0 +1,7 @@ +/* Additional SCSS for {{ dash_name }}. */ + +/* +#mapdiv { + min-height: 300px; +} +*/ diff --git a/iotronic_ui/iot/templates/iot/base.html b/iotronic_ui/iot/templates/iot/base.html new file mode 100644 index 0000000..41c73e4 --- /dev/null +++ b/iotronic_ui/iot/templates/iot/base.html @@ -0,0 +1,10 @@ +{% extends 'base.html' %} + +{% block sidebar %} + {% include 'horizon/common/_sidebar.html' %} +{% endblock %} + +{% block main %} + {% include "horizon/_messages.html" %} + {% block iot_main %}{% endblock %} +{% endblock %} diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..6cac6ba --- /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", + "iotronic_ui.test.settings") + execute_from_command_line(sys.argv) diff --git a/package.json b/package.json new file mode 100644 index 0000000..1962e2c --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "iotronic_ui", + "description": "IoTronic Panels JavaScript tests", + "repository": { + "type": "git", + "url": "git://git.openstack.org/openstack/iotronic_ui" + }, + "version": "0.0.0", + "private": true, + "license": "Apache 2.0", + "author": "Openstack ", + "devDependencies": { + "eslint": "^1.10.3", + "eslint-config-openstack": "^1.2.4", + "eslint-plugin-angular": "1.0.1", + "jasmine-core": "2.4.1", + "karma": "1.1.2", + "karma-chrome-launcher": "1.0.1", + "karma-cli": "1.0.1", + "karma-coverage": "1.1.1", + "karma-jasmine": "1.0.2", + "karma-ng-html2js-preprocessor": "1.0.0", + "karma-threshold-reporter": "0.1.15" + }, + "dependencies": {}, + "scripts": { + "postinstall": "if [ ! -d .tox ] || [ ! -d .tox/py27 ]; then tox -epy27 --notest; fi", + "lint": "eslint --no-color iotronic_ui/static", + "lintq": "eslint --quiet iotronic_ui/static", + "test": "karma start iotronic_ui/karma.conf.js --single-run" + } +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5f85fca --- /dev/null +++ b/requirements.txt @@ -0,0 +1,15 @@ +# 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 +pbr>=2.0.0,!=2.1.0 # Apache-2.0 +Babel>=2.3.4,!=2.4.0 # BSD +Django>=1.8,<2.0 # BSD +django-babel>=0.5.1 # BSD +django-compressor>=2.0 # MIT +django-pyscss>=2.0.2 # BSD License (2 clause) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..e8cfe56 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,29 @@ +[metadata] +name = iotronic_ui +summary = Iotronic plugin for the OpenStack Dashboard +description-file = + README.rst +author = OpenStack +author-email = openstack-dev@lists.openstack.org +home-page = http://www.openstack.org/ +classifier = + Environment :: OpenStack + 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 + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.5 + +[files] +packages = + iotronic_ui + +[build_sphinx] +all_files = 1 +build-dir = doc/build +source-dir = doc/source +warning-is-error = 1 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..d74ff58 --- /dev/null +++ b/setup.py @@ -0,0 +1,27 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# 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..5f85fca --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,15 @@ +# 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 +pbr>=2.0.0,!=2.1.0 # Apache-2.0 +Babel>=2.3.4,!=2.4.0 # BSD +Django>=1.8,<2.0 # BSD +django-babel>=0.5.1 # BSD +django-compressor>=2.0 # MIT +django-pyscss>=2.0.2 # BSD License (2 clause) diff --git a/test-shim.js b/test-shim.js new file mode 100644 index 0000000..5b364ba --- /dev/null +++ b/test-shim.js @@ -0,0 +1,96 @@ +/* + * Shim for Javascript unit tests; supplying expected global features. + * This should be removed from the codebase once i18n services are provided. + * Taken from default i18n file provided by Django. + */ + +var horizonPlugInModules = []; + + +(function (globals) { + + var django = globals.django || (globals.django = {}); + + + django.pluralidx = function (count) { return (count == 1) ? 0 : 1; }; + + /* gettext identity library */ + + django.gettext = function (msgid) { return msgid; }; + django.ngettext = function (singular, plural, count) { return (count == 1) ? singular : plural; }; + django.gettext_noop = function (msgid) { return msgid; }; + django.pgettext = function (context, msgid) { return msgid; }; + django.npgettext = function (context, singular, plural, count) { return (count == 1) ? singular : plural; }; + + + django.interpolate = function (fmt, obj, named) { + if (named) { + return fmt.replace(/%\(\w+\)s/g, function(match){return String(obj[match.slice(2,-2)])}); + } else { + return fmt.replace(/%s/g, function(match){return String(obj.shift())}); + } + }; + + + /* formatting library */ + + django.formats = { + "DATETIME_FORMAT": "N j, Y, P", + "DATETIME_INPUT_FORMATS": [ + "%Y-%m-%d %H:%M:%S", + "%Y-%m-%d %H:%M:%S.%f", + "%Y-%m-%d %H:%M", + "%Y-%m-%d", + "%m/%d/%Y %H:%M:%S", + "%m/%d/%Y %H:%M:%S.%f", + "%m/%d/%Y %H:%M", + "%m/%d/%Y", + "%m/%d/%y %H:%M:%S", + "%m/%d/%y %H:%M:%S.%f", + "%m/%d/%y %H:%M", + "%m/%d/%y" + ], + "DATE_FORMAT": "N j, Y", + "DATE_INPUT_FORMATS": [ + "%Y-%m-%d", + "%m/%d/%Y", + "%m/%d/%y" + ], + "DECIMAL_SEPARATOR": ".", + "FIRST_DAY_OF_WEEK": "0", + "MONTH_DAY_FORMAT": "F j", + "NUMBER_GROUPING": "3", + "SHORT_DATETIME_FORMAT": "m/d/Y P", + "SHORT_DATE_FORMAT": "m/d/Y", + "THOUSAND_SEPARATOR": ",", + "TIME_FORMAT": "P", + "TIME_INPUT_FORMATS": [ + "%H:%M:%S", + "%H:%M:%S.%f", + "%H:%M" + ], + "YEAR_MONTH_FORMAT": "F Y" + }; + + django.get_format = function (format_type) { + var value = django.formats[format_type]; + if (typeof(value) == 'undefined') { + return format_type; + } else { + return value; + } + }; + + /* add to global namespace */ + globals.pluralidx = django.pluralidx; + globals.gettext = django.gettext; + globals.ngettext = django.ngettext; + globals.gettext_noop = django.gettext_noop; + globals.pgettext = django.pgettext; + globals.npgettext = django.npgettext; + globals.interpolate = django.interpolate; + globals.get_format = django.get_format; + globals.STATIC_URL = '/static/'; + globals.WEBROOT = '/'; + +}(this)); diff --git a/tools/tox_install.sh b/tools/tox_install.sh new file mode 100755 index 0000000..3a38ee4 --- /dev/null +++ b/tools/tox_install.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash + +# Many of horizon's repos suffer from the problem of depending on horizon, +# but it not existing on pypi. + +# This wrapper for tox's package installer will use the existing package +# if it exists, else use zuul-cloner if that program exists, else grab it +# from horizon master via a hard-coded URL. That last case should only +# happen with devs running unit tests locally. + +# From the tox.ini config page: +# install_command=ARGV +# default: +# pip install {opts} {packages} + +ZUUL_CLONER=/usr/zuul-env/bin/zuul-cloner +GIT_BASE=${GIT_BASE:-https://git.openstack.org/} + +install_project() { + local project=$1 + local branch=${2:-$BRANCH_NAME} + local module_name=${project//-/_} + + set +e + project_installed=$(echo "import $module_name" | python 2>/dev/null ; echo $?) + set -e + + if [ $project_installed -eq 0 ]; then + echo "ALREADY INSTALLED" > /tmp/tox_install.txt + echo "$project already installed; using existing package" + elif [ -x "$ZUUL_CLONER" ]; then + echo "ZUUL CLONER" > /tmp/tox_install.txt + # Make this relative to current working directory so that + # git clean can remove it. We cannot remove the directory directly + # since it is referenced after $install_cmd -e + mkdir -p .tmp + PROJECT_DIR=$(/bin/mktemp -d -p $(pwd)/.tmp) + pushd $PROJECT_DIR + $ZUUL_CLONER --cache-dir \ + /opt/git \ + --branch $branch \ + http://git.openstack.org \ + openstack/$project + cd openstack/$project + $install_cmd -e . + popd + else + echo "PIP HARDCODE" > /tmp/tox_install.txt + local GIT_REPO="$GIT_BASE/openstack/$project" + SRC_DIR="$VIRTUAL_ENV/src/$project" + git clone --depth 1 --branch $branch $GIT_REPO $SRC_DIR + $install_cmd -U -e $SRC_DIR + fi +} + +set -e + +install_cmd="pip install -c$1" +shift + +install_project horizon + +$install_cmd -U $* +exit $? diff --git a/tools/tox_install.sh_ORIG b/tools/tox_install.sh_ORIG new file mode 100755 index 0000000..7fd2a59 --- /dev/null +++ b/tools/tox_install.sh_ORIG @@ -0,0 +1,88 @@ +#!/usr/bin/env bash + +# Client constraint file contains this client version pin that is in conflict +# with installing the client from source. We should remove the version pin in +# the constraints file before applying it for from-source installation. +# The script also has a secondary purpose to install certain special +# dependencies directly from git. + +# Wrapper for pip install that always uses constraints. +function pip_install() { + pip install -c"$localfile" -U "$@" +} + +# Grab the library from git using either zuul-cloner or pip. The former is +# there to a take advantage of the setup done by the gate infrastructure +# and honour any/all Depends-On headers in the commit message +function install_from_git() { + ZUUL_CLONER=/usr/zuul-env/bin/zuul-cloner + # GIT_HOST=git.openstack.org + GIT_HOST=github.com + PROJ=$1 + EGG=$2 + + edit-constraints "$localfile" -- "$EGG" + if [ -x "$ZUUL_CLONER" ]; then + SRC_DIR="$VIRTUAL_ENV/src" + mkdir -p "$SRC_DIR" + cd "$SRC_DIR" >/dev/null + ZUUL_CACHE_DIR=${ZUUL_CACHE_DIR:-/opt/git} $ZUUL_CLONER \ + --branch "$BRANCH_NAME" \ + "git://$GIT_HOST" "$PROJ" + pip_install -e "$PROJ/." + cd - >/dev/null + else + SRC_DIR="$VIRTUAL_ENV/src/$PROJ" + git clone --depth 1 --branch $BRANCH_NAME https://$GIT_HOST/$PROJ $SRC_DIR + pip_install -e $SRC_DIR + fi +} + + + +CONSTRAINTS_FILE="$1" +shift 1 + +# This script will either complete with a return code of 0 or the return code +# of whatever failed. +set -e + +# NOTE(tonyb): Place this in the tox environment's log dir so it will get +# published to logs.openstack.org for easy debugging. +mkdir -p "$VIRTUAL_ENV/log/" +localfile="$VIRTUAL_ENV/log/upper-constraints.txt" + +if [[ "$CONSTRAINTS_FILE" != http* ]]; then + CONSTRAINTS_FILE="file://$CONSTRAINTS_FILE" +fi +# NOTE(tonyb): need to add curl to bindep.txt if the project supports bindep +curl "$CONSTRAINTS_FILE" --insecure --progress-bar --output "$localfile" + +pip_install openstack-requirements + +# This is the main purpose of the script: Allow local installation of +# the current repo. It is listed in constraints file and thus any +# install will be constrained and we need to unconstrain it. +edit-constraints "$localfile" -- "$CLIENT_NAME" + +declare -a passthrough_args +while [ $# -gt 0 ] ; do + case "$1" in + # If we have any special os: deps then process them + os:*) + declare -a pkg_spec + IFS=: pkg_spec=($1) + install_from_git "${pkg_spec[1]}" "${pkg_spec[2]}" + ;; + # Otherwise just pass the other deps through to the constrained pip install + *) + passthrough_args+=("$1") + ;; + esac + shift 1 +done + +# If *only* had special args then then isn't any need to run pip. +if [ -n "$passthrough_args" ] ; then + pip_install "${passthrough_args[@]}" +fi diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..34cedf7 --- /dev/null +++ b/tox.ini @@ -0,0 +1,39 @@ +[tox] +envlist = py27,py27dj18,pep8 +minversion = 2.3.1 +skipsdist = True + +[testenv] +usedevelop = True +setenv = VIRTUAL_ENV={envdir} + BRANCH_NAME=master + CLIENT_NAME=iotronic-ui +install_command = {toxinidir}/tools/tox_install.sh {env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} +deps = -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt +commands = {} +whitelist_externals = flake8 + +[testenv:pep8] +basepython = python2.7 +# commands = flake8 {posargs} +commands = + +[testenv:venv] +commands = {posargs} + +[testenv:py27] +basepython = python2.7 +commands = + +[testenv:py27dj18] +basepython = python2.7 +commands = + pip install django>=1.8,<2.0 + +[flake8] +show-source = True +builtins = _ +ignore = E711,E712,H404,H405,E123,E125,E901,H301,H701,E226 +exclude = .venv,.git,.tox,dist,*lib/python*,*egg,build,node_modules +max-complexity = 20