Retire the Tuskar UI codebase

Change-Id: I469fdc1339d4991586bf2e1d62d99fd5b68289eb
Depends-On: I904b2f27591333e104bf9080bb8c3876fcb3596c
This commit is contained in:
Dougal Matthews 2016-01-21 15:12:37 +00:00
parent 0a5b41bad6
commit 31e0bb84f6
219 changed files with 10 additions and 17306 deletions

View File

@ -1,12 +0,0 @@
# Format is:
# <preferred e-mail> <other e-mail 1>
# <preferred e-mail> <other e-mail 2>
<ghe@debian.org> <ghe.rivero@stackops.com>
<jake@ansolabs.com> <admin@jakedahn.com>
<launchpad@markgius.com> <mgius7096@gmail.com>
<yorik.sar@gmail.com> <yorik@ytaraday>
<jeblair@hp.com> <james.blair@rackspace.com>
<ke.wu@ibeca.me> <ke.wu@nebula.com>
Zhongyue Luo <zhongyue.nah@intel.com> <lzyeval@gmail.com>
Joe Gordon <joe.gordon0@gmail.com> <jogo@cloudscaling.com>
Kun Huang <gareth@unitedstack.com> <academicgareth@gmail.com>

View File

@ -1,42 +0,0 @@
# The format of this file isn't really documented; just use --generate-rcfile
[MASTER]
# Add <file or directory> to the black list. It should be a base name, not a
# path. You may set this option multiple times.
ignore=test
[Messages Control]
# NOTE(justinsb): We might want to have a 2nd strict pylintrc in future
# C0111: Don't require docstrings on every method
# W0511: TODOs in code comments are fine.
# W0142: *args and **kwargs are fine.
# W0622: Redefining id is fine.
disable=C0111,W0511,W0142,W0622
[Basic]
# Variable names can be 1 to 31 characters long, with lowercase and underscores
variable-rgx=[a-z_][a-z0-9_]{0,30}$
# Argument names can be 2 to 31 characters long, with lowercase and underscores
argument-rgx=[a-z_][a-z0-9_]{1,30}$
# Method names should be at least 3 characters long
# and be lowecased with underscores
method-rgx=([a-z_][a-z0-9_]{2,50}|setUp|tearDown)$
# Module names matching keystone-* are ok (files in bin/)
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+)|(keystone-[a-z0-9_-]+))$
# Don't require docstrings on tests.
no-docstring-rgx=((__.*__)|([tT]est.*)|setUp|tearDown)$
[Design]
max-public-methods=100
min-public-methods=0
max-args=6
[Variables]
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
# _ is used by our localization
additional-builtins=_

View File

@ -1,60 +0,0 @@
Contributing
============
The code repository is located at `OpenStack <https://github.com/openstack>`__.
Please go there if you want to check it out:
git clone https://github.com/openstack/tuskar-ui.git
The list of bugs and blueprints is on Launchpad:
`<https://launchpad.net/tuskar-ui>`__
We use OpenStack's Gerrit for the code contributions:
`<https://review.openstack.org/#/q/status:open+project:openstack/tuskar-ui,n,z>`__
and we follow the `OpenStack Gerrit Workflow <http://docs.openstack.org/infra/manual/developers.html#development-workflow>`__.
If you're interested in the code, here are some key places to start:
* `tuskar_ui/api.py <https://github.com/openstack/tuskar-ui/blob/master/tuskar_ui/api.py>`_
- This file contains all the API calls made to the Tuskar API
(through python-tuskarclient).
* `tuskar_ui/infrastructure <https://github.com/openstack/tuskar-ui/tree/master/tuskar_ui/infrastructure>`_
- The Tuskar UI code is contained within this directory.
Running tests
=============
There are several ways to run tests for tuskar-ui.
Using ``tox``:
This is the easiest way to run tests. When run, tox installs dependencies,
prepares the virtual python environment, then runs test commands. The gate
tests in gerrit usually also use tox to run tests. For avaliable tox
environments, see ``tox.ini``.
By running ``run_tests.sh``:
Tests can also be run using the ``run_tests.sh`` script, to see available
options, run it with the ``--help`` option. It handles preparing the
virtual environment and executing tests, but in contrast with tox, it does
not install all dependencies, e.g. ``jshint`` must be installed before
running the jshint testcase.
Manual tests:
To manually check tuskar-ui, it is possible to run a development server
for tuskar-ui by running ``run_tests.sh --runserver``.
To run the server with the settings used by the test environment:
``run_tests.sh --runserver 0.0.0.0:8000 --settings=tuskar_ui.test.settings``
OpenStack Style Commandments
============================
- Step 1: Read http://www.python.org/dev/peps/pep-0008/
- Step 2: Read http://www.python.org/dev/peps/pep-0008/ again
- Step 3: Read https://github.com/openstack-dev/hacking/blob/master/HACKING.rst

176
LICENSE
View File

@ -1,176 +0,0 @@
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.

View File

@ -1,19 +0,0 @@
recursive-include bin *.js
recursive-include doc *.py *.rst *.css *.js *.html *.conf *.jpg *.gif *.png *.css_t
recursive-include tools *.py *.sh
recursive-include tuskar_ui *.py *.html *.js *.scss *.mo *.po *.example *.eot *.svg *.ttf *.woff *.png *.ico *.wsgi *.gif *.csv *.template
include AUTHORS
include ChangeLog
include LICENSE
include Makefile
include manage.py
include README.rst
include run_tests.sh
include tox.ini
include doc/Makefile
include doc/source/_templates/.placeholder
include requirements.txt
include test-requirements.txt
exclude openstack_dashboard/local/local_settings.py

View File

@ -1,24 +0,0 @@
PYTHON=`which python`
DESTDIR=/
PROJECT=horizon
all:
@echo "make test - Run tests"
@echo "make source - Create source package"
@echo "make install - Install on local system"
@echo "make buildrpm - Generate a rpm package"
@echo "make clean - Get rid of scratch and byte files"
source:
$(PYTHON) setup.py sdist $(COMPILE)
install:
$(PYTHON) setup.py install --root $(DESTDIR) $(COMPILE)
buildrpm:
$(PYTHON) setup.py bdist_rpm --post-install=rpm/postinstall --pre-uninstall=rpm/preuninstall
clean:
$(PYTHON) setup.py clean
rm -rf build/ MANIFEST
find . -name '*.pyc' -delete

10
README Normal file
View File

@ -0,0 +1,10 @@
This project is no longer maintained.
The contents of this repository are still available in the Git
source code management system. To see the contents of this
repository before it reached its end of life, please check out the
previous commit with "git checkout HEAD^1".
For any further questions, please email
openstack-dev@lists.openstack.org or join #openstack-dev or #tripleo
on Freenode.

View File

@ -1,41 +0,0 @@
=========
Tuskar UI
=========
**Tuskar UI** is a user interface for
`Tuskar <https://github.com/openstack/tuskar>`__, a management API for
OpenStack deployments. It is a plugin for `OpenStack
Horizon <https://wiki.openstack.org/wiki/Horizon>`__.
High-Level Overview
-------------------
Tuskar UI endeavours to be a stateless UI, relying on Tuskar API calls
as much as possible. We use existing Horizon libraries and components
where possible. If added libraries and components are needed, we will
work with the OpenStack community to push those changes back into Horizon.
Interested in seeing Tuskar and Tuskar UI in action?
`Watch a demo! <https://www.youtube.com/watch?v=-6whFIqCqLU>`_
Installation Guide
------------------
Use the `Installation Guide <http://tuskar-ui.readthedocs.org/en/latest/install.html>`_ to install Tuskar UI.
License
-------
This project is licensed under the Apache License, version 2. More
information can be found in the LICENSE file.
Contact Us
----------
Join us on IRC (Internet Relay Chat)::
Network: Freenode (irc.freenode.net/tuskar)
Channel: #tuskar
Or send an email to openstack-dev@lists.openstack.org.

View File

@ -1,2 +0,0 @@
DASHBOARD = 'admin'
DISABLED = True

View File

@ -1,2 +0,0 @@
DASHBOARD = 'project'
DISABLED = True

View File

@ -1,2 +0,0 @@
DASHBOARD = 'identity'
DISABLED = True

View File

@ -1,12 +0,0 @@
from tuskar_ui import exceptions
DASHBOARD = 'infrastructure'
ADD_INSTALLED_APPS = [
'tuskar_ui.infrastructure',
]
ADD_EXCEPTIONS = {
'recoverable': exceptions.RECOVERABLE,
'not_found': exceptions.NOT_FOUND,
'unauthorized': exceptions.UNAUTHORIZED,
}
DEFAULT = True

View File

@ -1,56 +0,0 @@
#!/bin/bash
set -ex
USAGE="Usage: `basename $0` <undercloud_ip> <undercloud_admin_password>"
if [ "$#" -ne 2 ]; then
echo $USAGE
exit 1
fi
UNDERCLOUD_IP=$1
UNDERCLOUD_ADMIN_PASSWORD=$2
echo "Copying SSH key..."
cp /home/stack/.ssh/id_rsa /root/.ssh/
echo "Installing system requirements..."
yum install -y git python-devel swig openssl-devel mysql-devel libxml2-devel libxslt-devel gcc gcc-c++
easy_install pip nose
echo "Cloning repos..."
mkdir /opt/stack
cd /opt/stack
git clone git://github.com/openstack/horizon.git
git clone git://github.com/openstack/python-tuskarclient.git
git clone git://github.com/openstack/tuskar-ui.git
git clone git://github.com/rdo-management/tuskar-ui-extras.git
echo "Setting up repos..."
cd horizon
python tools/install_venv.py
./run_tests.sh -V
cp openstack_dashboard/local/local_settings.py.example openstack_dashboard/local/local_settings.py
tools/with_venv.sh pip install -e ../python-tuskarclient/
tools/with_venv.sh pip install -e ../tuskar-ui/
tools/with_venv.sh pip install -e ../tuskar-ui-extras/
cp ../tuskar-ui/_50_tuskar.py.example openstack_dashboard/local/enabled/_50_tuskar.py
cp ../tuskar-ui-extras/_60_tuskar_boxes.py.example openstack_dashboard/local/enabled/_60_tuskar_boxes.py
cp ../tuskar-ui/_10_admin.py.example openstack_dashboard/local/enabled/_10_admin.py
cp ../tuskar-ui/_20_project.py.example openstack_dashboard/local/enabled/_20_project.py
cp ../tuskar-ui/_30_identity.py.example openstack_dashboard/local/enabled/_30_identity.py
sed -i s/'OPENSTACK_HOST = "127.0.0.1"'/'OPENSTACK_HOST = "192.0.2.1"'/ openstack_dashboard/local/local_settings.py
echo 'IRONIC_DISCOVERD_URL = "http://%s:5050" % OPENSTACK_HOST' >> openstack_dashboard/local/local_settings.py
echo 'UNDERCLOUD_ADMIN_PASSWORD = "'$UNDERCLOUD_ADMIN_PASSWORD'"' >> openstack_dashboard/local/local_settings.py
echo 'DEPLOYMENT_MODE = "scale"' >> openstack_dashboard/local/local_settings.py
echo "Setting up networking..."
sudo ip route replace 192.0.2.0/24 dev virbr0 via $UNDERCLOUD_IP
echo "Setting up iptables on the undercloud..."
RULE_1="-A INPUT -p tcp -m tcp --dport 8585 -j ACCEPT"
RULE_2="-A INPUT -p tcp -m tcp --dport 9696 -j ACCEPT"
RULE_3="-A INPUT -p tcp -m tcp --dport 8777 -j ACCEPT"
ssh $UNDERCLOUD_IP "sed -i '/$RULE_1/a $RULE_2' /etc/sysconfig/iptables"
ssh $UNDERCLOUD_IP "sed -i '/$RULE_2/a $RULE_3' /etc/sysconfig/iptables"
ssh $UNDERCLOUD_IP "service iptables restart"

View File

@ -1,153 +0,0 @@
# 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 <target>' where <target> 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/TuskarUI.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/TuskarUI.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/TuskarUI"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/TuskarUI"
@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."

View File

@ -1,190 +0,0 @@
@ECHO OFF
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set BUILDDIR=build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source
set I18NSPHINXOPTS=%SPHINXOPTS% source
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
)
if "%1" == "" goto help
if "%1" == "help" (
:help
echo.Please use `make ^<target^>` where ^<target^> 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. text to make text files
echo. man to make manual pages
echo. texinfo to make Texinfo files
echo. gettext to make PO message catalogs
echo. changes to make an overview over 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
goto end
)
if "%1" == "clean" (
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
del /q /s %BUILDDIR%\*
goto end
)
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
if "%1" == "singlehtml" (
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\TuskarUI.qhcp
echo.To view the help file:
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\TuskarUI.ghc
goto end
)
if "%1" == "devhelp" (
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished.
goto end
)
if "%1" == "epub" (
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub file is in %BUILDDIR%/epub.
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
if errorlevel 1 exit /b 1
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "text" (
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The text files are in %BUILDDIR%/text.
goto end
)
if "%1" == "man" (
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The manual pages are in %BUILDDIR%/man.
goto end
)
if "%1" == "texinfo" (
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
goto end
)
if "%1" == "gettext" (
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
if errorlevel 1 exit /b 1
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
if errorlevel 1 exit /b 1
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
:end

View File

@ -1 +0,0 @@
../../HACKING.rst

View File

@ -1 +0,0 @@
../../README.rst

View File

@ -1,242 +0,0 @@
# -*- coding: utf-8 -*-
#
# Tuskar UI documentation build configuration file, created by
# sphinx-quickstart on Thu Apr 24 09:19:32 2014.
#
# 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.
import sys, os
# 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.viewcode', 'oslosphinx']
# 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'Tuskar UI'
copyright = u'2014, Tuskar Team'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = 'Juno'
# The full version, including alpha/beta/rc tags.
release = 'Juno'
# 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 = []
# -- 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 = 'default'
# 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
# "<project> v<release> 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']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# 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 <link> 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 = 'TuskarUIdoc'
# -- 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', 'TuskarUI.tex', u'Tuskar UI Documentation',
u'Tuskar Team', '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', 'tuskarui', u'Tuskar UI Documentation',
[u'Tuskar Team'], 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', 'TuskarUI', u'Tuskar UI Documentation',
u'Tuskar Team', 'TuskarUI', 'One line description of project.',
'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'

View File

@ -1,20 +0,0 @@
Tuskar UI
=========
Contents:
.. toctree::
:maxdepth: 2
README
install
user_guide
HACKING
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@ -1,133 +0,0 @@
Installation instructions
=========================
Note
----
If you want to install and configure the entire TripleO + Tuskar + Tuskar UI
stack, you can use
`the devtest installation guide <https://wiki.openstack.org/wiki/Tuskar/Devtest>`_.
Otherwise, you can use the installation instructions for Tuskar UI below.
Prerequisites
-------------
Installation prerequisites are:
1. A functional OpenStack installation. Horizon and Tuskar UI will
connect to the Keystone service here. Keystone does *not* need to be
on the same machine as your Tuskar UI interface, but its HTTP API
must be accessible.
2. A functional Tuskar installation. Tuskar UI talks to Tuskar via an
HTTP interface. It may, but does not have to, reside on the same
machine as Tuskar UI, but it must be network accessible.
You may find
`the Tuskar install guide <https://github.com/openstack/tuskar/blob/master/doc/source/install.rst>`_
helpful.
Installing the packages
-----------------------
Tuskar UI is a Django app written in Python and has a few installation
dependencies:
On a RHEL 6 system, you should install the following:
::
yum install git python-devel swig openssl-devel mysql-devel libxml2-devel libxslt-devel gcc gcc-c++
The above should work well for similar RPM-based distributions. For
other distros or platforms, you will obviously need to convert as
appropriate.
Then, you'll want to use the ``easy_install`` utility to set up a few
other tools:
::
easy_install pip
easy_install nose
Install the management UI
-------------------------
Begin by cloning the Horizon and Tuskar UI repositories:
::
git clone git://github.com/openstack/horizon.git
git clone git://github.com/openstack/python-tuskarclient.git
git clone git://github.com/openstack/tuskar-ui.git
Go into ``horizon`` and install a virtual environment for your setup::
cd horizon
python tools/install_venv.py
Next, run ``run_tests.sh`` to have pip install Horizon dependencies:
::
./run_tests.sh
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 Tuskar UI with all dependencies in your virtual environment::
tools/with_venv.sh pip install -e ../python-tuskarclient/
tools/with_venv.sh pip install -e ../tuskar-ui/
And enable it in Horizon::
cp ../tuskar-ui/_50_tuskar.py.example openstack_dashboard/local/enabled/_50_tuskar.py
Then disable the other dashboards::
cp ../tuskar-ui/_10_admin.py.example openstack_dashboard/local/enabled/_10_admin.py
cp ../tuskar-ui/_20_project.py.example openstack_dashboard/local/enabled/_20_project.py
cp ../tuskar-ui/_30_identity.py.example openstack_dashboard/local/enabled/_30_identity.py
Starting the app
----------------
If everything has gone according to plan, you should be able to run:
::
tools/with_venv.sh ./manage.py runserver
and have the application start on port 8080. The Tuskar UI dashboard will
be located at http://localhost:8080/infrastructure
If you wish to access it remotely (i.e., not just from localhost), you
need to open port 8080 in iptables:
::
iptables -I INPUT -p tcp --dport 8080 -j ACCEPT
and launch the server with ``0.0.0.0:8080`` on the end:
::
tools/with_venv.sh ./manage.py runserver 0.0.0.0:8080

View File

@ -1,16 +0,0 @@
==========
User Guide
==========
Nodes List File
---------------
To allow users to load a bunch of nodes at once, there is possibility to
upload CSV file with given list of nodes. This file should be formatted as
::
driver,address,username,password/ssh key,mac addresses,cpu architecture,number of CPUs,available memory,available storage
Even if there is no all data available, we assume empty values for missing
keys and try to parse everything, what is possible.

View File

@ -1,11 +0,0 @@
#!/usr/bin/env python
import os
import sys
from django.core.management import execute_from_command_line
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE",
"openstack_dashboard.settings")
execute_from_command_line(sys.argv)

View File

@ -1,20 +0,0 @@
#!/bin/bash
set -eux
OUTPUT_FILE=${OUTPUT_FILE:-"nodes.csv"}
NODES_JSON_FILE=${NODES_JSON_FILE:-"/home/stack/instackenv.json"}
NUM_NODES=$(jq '.nodes | length' $NODES_JSON_FILE)
if [ -e $OUTPUT_FILE ]; then
rm $OUTPUT_FILE
fi
for i in $(seq 0 $(expr $NUM_NODES - 1)); do
DRIVER=$(jq -r ".nodes[${i}] | .[\"pm_type\"]" $NODES_JSON_FILE)
SSH_ADDRESS=$(jq -r ".nodes[${i}] | .[\"pm_addr\"]" $NODES_JSON_FILE)
SSH_USERNAME=$(jq -r ".nodes[${i}] | .[\"pm_user\"]" $NODES_JSON_FILE)
SSH_KEY_CONTENTS=$(jq -r ".nodes[${i}] | .[\"pm_password\"]" $NODES_JSON_FILE)
MAC=$(jq -r ".nodes[${i}] | .mac[0]" $NODES_JSON_FILE)
echo "${DRIVER},${SSH_ADDRESS},${SSH_USERNAME},\"${SSH_KEY_CONTENTS}\",${MAC}" >> $OUTPUT_FILE
done

View File

@ -1,6 +0,0 @@
# 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.
os-cloud-config
python-ironic-inspector-client>=1.0.1
python-ironicclient>=0.8.0

View File

@ -1,552 +0,0 @@
#!/bin/bash
set -o errexit
function usage {
echo "Usage: $0 [OPTION]..."
echo "Run Horizon's test suite(s)"
echo ""
echo " -V, --virtual-env Always use virtualenv. Install automatically"
echo " if not present"
echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local"
echo " environment"
echo " -c, --coverage Generate reports using Coverage"
echo " -f, --force Force a clean re-build of the virtual"
echo " environment. Useful when dependencies have"
echo " been added."
echo " -m, --manage Run a Django management command."
echo " --makemessages Create/Update English translation files."
echo " --compilemessages Compile all translation files."
echo " --check-only Do not update translation files (--makemessages only)."
echo " --pseudo Pseudo translate a language."
echo " -p, --pep8 Just run pep8"
echo " -8, --pep8-changed [<basecommit>]"
echo " Just run PEP8 and HACKING compliance check"
echo " on files changed since HEAD~1 (or <basecommit>)"
echo " -P, --no-pep8 Don't run pep8 by default"
echo " -t, --tabs Check for tab characters in files."
echo " -y, --pylint Just run pylint"
echo " -j, --jshint Just run jshint"
echo " -s, --jscs Just run jscs"
echo " -q, --quiet Run non-interactively. (Relatively) quiet."
echo " Implies -V if -N is not set."
echo " --only-selenium Run only the Selenium unit tests"
echo " --with-selenium Run unit tests including Selenium tests"
echo " --selenium-headless Run Selenium tests headless"
echo " --integration Run the integration tests (requires a running "
echo " OpenStack environment)"
echo " --runserver Run the Django development server for"
echo " openstack_dashboard in the virtual"
echo " environment."
echo " --docs Just build the documentation"
echo " --backup-environment Make a backup of the environment on exit"
echo " --restore-environment Restore the environment before running"
echo " --destroy-environment Destroy the environment and exit"
echo " -h, --help Print this usage message"
echo ""
echo "Note: with no options specified, the script will try to run the tests in"
echo " a virtual environment, If no virtualenv is found, the script will ask"
echo " if you would like to create one. If you prefer to run tests NOT in a"
echo " virtual environment, simply pass the -N option."
exit
}
# DEFAULTS FOR RUN_TESTS.SH
#
root=`pwd -P`
venv=$root/.venv
venv_env_version=$venv/environments
with_venv=tools/with_venv.sh
included_dirs="tuskar_ui"
always_venv=0
backup_env=0
command_wrapper=""
destroy=0
force=0
just_pep8=0
just_pep8_changed=0
no_pep8=0
just_pylint=0
just_docs=0
just_tabs=0
just_jscs=0
just_jshint=0
never_venv=0
quiet=0
restore_env=0
runserver=0
only_selenium=0
with_selenium=0
selenium_headless=0
integration=0
testopts=""
testargs=""
with_coverage=0
makemessages=0
compilemessages=0
check_only=0
pseudo=0
manage=0
# Jenkins sets a "JOB_NAME" variable, if it's not set, we'll make it "default"
[ "$JOB_NAME" ] || JOB_NAME="default"
function process_option {
# If running manage command, treat the rest of options as arguments.
if [ $manage -eq 1 ]; then
testargs="$testargs $1"
return 0
fi
case "$1" in
-h|--help) usage;;
-V|--virtual-env) always_venv=1; never_venv=0;;
-N|--no-virtual-env) always_venv=0; never_venv=1;;
-p|--pep8) just_pep8=1;;
-8|--pep8-changed) just_pep8_changed=1;;
-P|--no-pep8) no_pep8=1;;
-y|--pylint) just_pylint=1;;
-j|--jshint) just_jshint=1;;
-s|--jscs) just_jscs=1;;
-f|--force) force=1;;
-t|--tabs) just_tabs=1;;
-q|--quiet) quiet=1;;
-c|--coverage) with_coverage=1;;
-m|--manage) manage=1;;
--makemessages) makemessages=1;;
--compilemessages) compilemessages=1;;
--check-only) check_only=1;;
--pseudo) pseudo=1;;
--only-selenium) only_selenium=1;;
--with-selenium) with_selenium=1;;
--selenium-headless) selenium_headless=1;;
--integration) integration=1;;
--docs) just_docs=1;;
--runserver) runserver=1;;
--backup-environment) backup_env=1;;
--restore-environment) restore_env=1;;
--destroy-environment) destroy=1;;
-*) testopts="$testopts $1";;
*) testargs="$testargs $1"
esac
}
function run_management_command {
${command_wrapper} python $root/manage.py $testopts $testargs
}
function run_server {
echo "Starting Django development server..."
${command_wrapper} python $root/manage.py runserver $testopts $testargs
echo "Server stopped."
}
function run_pylint {
echo "Running pylint ..."
PYTHONPATH=$root ${command_wrapper} pylint --rcfile=.pylintrc -f parseable $included_dirs > pylint.txt || true
CODE=$?
grep Global -A2 pylint.txt
if [ $CODE -lt 32 ]; then
echo "Completed successfully."
exit 0
else
echo "Completed with problems."
exit $CODE
fi
}
function run_jshint {
echo "Running jshint ..."
jshint tuskar_ui/infrastructure/static/infrastructure
}
function run_jscs {
echo "Running jscs ..."
if [ "`which jscs`" == '' ] ; then
echo "jscs is not present; please install, e.g. sudo npm install jscs -g"
else
jscs tuskar_ui/infrastructure/static/infrastructure/js \
tuskar_ui/infrastructure/static/infrastructure/tests
fi
}
function warn_on_flake8_without_venv {
set +o errexit
${command_wrapper} python -c "import hacking" 2>/dev/null
no_hacking=$?
set -o errexit
if [ $never_venv -eq 1 -a $no_hacking -eq 1 ]; then
echo "**WARNING**:" >&2
echo "OpenStack hacking is not installed on your host. Its detection will be missed." >&2
echo "Please install or use virtual env if you need OpenStack hacking detection." >&2
fi
}
function run_pep8 {
echo "Running flake8 ..."
warn_on_flake8_without_venv
DJANGO_SETTINGS_MODULE=tuskar_ui.test.settings ${command_wrapper} flake8 $included_dirs
}
function run_pep8_changed {
# NOTE(gilliard) We want use flake8 to check the entirety of every file that has
# a change in it. Unfortunately the --filenames argument to flake8 only accepts
# file *names* and there are no files named (eg) "nova/compute/manager.py". The
# --diff argument behaves surprisingly as well, because although you feed it a
# diff, it actually checks the file on disk anyway.
local base_commit=${testargs:-HEAD~1}
files=$(git diff --name-only $base_commit | tr '\n' ' ')
echo "Running flake8 on ${files}"
warn_on_flake8_without_venv
diff -u --from-file /dev/null ${files} | DJANGO_SETTINGS_MODULE=openstack_dashboard.test.settings ${command_wrapper} flake8 --diff
exit
}
function run_sphinx {
echo "Building sphinx..."
export DJANGO_SETTINGS_MODULE=openstack_dashboard.settings
${command_wrapper} sphinx-build -b html doc/source doc/build/html
echo "Build complete."
}
function tab_check {
TAB_VIOLATIONS=`find $included_dirs -type f -regex ".*\.\(css\|js\|py\|html\)" -print0 | xargs -0 awk '/\t/' | wc -l`
if [ $TAB_VIOLATIONS -gt 0 ]; then
echo "TABS! $TAB_VIOLATIONS of them! Oh no!"
HORIZON_FILES=`find $included_dirs -type f -regex ".*\.\(css\|js\|py|\html\)"`
for TABBED_FILE in $HORIZON_FILES
do
TAB_COUNT=`awk '/\t/' $TABBED_FILE | wc -l`
if [ $TAB_COUNT -gt 0 ]; then
echo "$TABBED_FILE: $TAB_COUNT"
fi
done
fi
return $TAB_VIOLATIONS;
}
function destroy_venv {
echo "Cleaning environment..."
echo "Removing virtualenv..."
rm -rf $venv
echo "Virtualenv removed."
}
function environment_check {
echo "Checking environment."
if [ -f $venv_env_version ]; then
set +o errexit
cat requirements.txt test-requirements.txt | cmp $venv_env_version - > /dev/null
local env_check_result=$?
set -o errexit
if [ $env_check_result -eq 0 ]; then
# If the environment exists and is up-to-date then set our variables
command_wrapper="${root}/${with_venv}"
echo "Environment is up to date."
return 0
fi
fi
if [ $always_venv -eq 1 ]; then
install_venv
else
if [ ! -e ${venv} ]; then
echo -e "Environment not found. Install? (Y/n) \c"
else
echo -e "Your environment appears to be out of date. Update? (Y/n) \c"
fi
read update_env
if [ "x$update_env" = "xY" -o "x$update_env" = "x" -o "x$update_env" = "xy" ]; then
install_venv
else
# Set our command wrapper anyway.
command_wrapper="${root}/${with_venv}"
fi
fi
}
function sanity_check {
# Anything that should be determined prior to running the tests, server, etc.
# Don't sanity-check anything environment-related in -N flag is set
if [ $never_venv -eq 0 ]; then
if [ ! -e ${venv} ]; then
echo "Virtualenv not found at $venv. Did install_venv.py succeed?"
exit 1
fi
fi
# Remove .pyc files. This is sanity checking because they can linger
# after old files are deleted.
find . -name "*.pyc" -exec rm -rf {} \;
}
function backup_environment {
if [ $backup_env -eq 1 ]; then
echo "Backing up environment \"$JOB_NAME\"..."
if [ ! -e ${venv} ]; then
echo "Environment not installed. Cannot back up."
return 0
fi
if [ -d /tmp/.horizon_environment/$JOB_NAME ]; then
mv /tmp/.horizon_environment/$JOB_NAME /tmp/.horizon_environment/$JOB_NAME.old
rm -rf /tmp/.horizon_environment/$JOB_NAME
fi
mkdir -p /tmp/.horizon_environment/$JOB_NAME
cp -r $venv /tmp/.horizon_environment/$JOB_NAME/
cp .environment_version /tmp/.horizon_environment/$JOB_NAME/
# Remove the backup now that we've completed successfully
rm -rf /tmp/.horizon_environment/$JOB_NAME.old
echo "Backup completed"
fi
}
function restore_environment {
if [ $restore_env -eq 1 ]; then
echo "Restoring environment from backup..."
if [ ! -d /tmp/.horizon_environment/$JOB_NAME ]; then
echo "No backup to restore from."
return 0
fi
cp -r /tmp/.horizon_environment/$JOB_NAME/.venv ./ || true
echo "Environment restored successfully."
fi
}
function install_venv {
# Install with install_venv.py
export PIP_DOWNLOAD_CACHE=${PIP_DOWNLOAD_CACHE-/tmp/.pip_download_cache}
export PIP_USE_MIRRORS=true
if [ $quiet -eq 1 ]; then
export PIP_NO_INPUT=true
fi
echo "Fetching new src packages..."
rm -rf $venv/src
python tools/install_venv.py
command_wrapper="$root/${with_venv}"
# Make sure it worked and record the environment version
sanity_check
chmod -R 754 $venv
cat requirements.txt test-requirements.txt > $venv_env_version
}
function run_tests {
sanity_check
if [ $with_selenium -eq 1 ]; then
export WITH_SELENIUM=1
elif [ $only_selenium -eq 1 ]; then
export WITH_SELENIUM=1
export SKIP_UNITTESTS=1
fi
if [ $with_selenium -eq 0 -a $integration -eq 0 ]; then
testopts="$testopts --exclude-dir=tuskar_ui/test/integration_tests"
fi
if [ $selenium_headless -eq 1 ]; then
export SELENIUM_HEADLESS=1
fi
if [ -z "$testargs" ]; then
run_tests_all
else
run_tests_subset
fi
}
function run_tests_subset {
project=`echo $testargs | awk -F. '{print $1}'`
${command_wrapper} python $root/manage.py test --settings=$project.test.settings $testopts $testargs
}
function run_tests_all {
echo "Running Tuskar-UI application tests"
export NOSE_XUNIT_FILE=tuskar_ui/nosetests.xml
if [ "$NOSE_WITH_HTML_OUTPUT" = '1' ]; then
export NOSE_HTML_OUT_FILE='tuskar_ui_nose_results.html'
fi
if [ $with_coverage -eq 1 ]; then
${command_wrapper} python -m coverage.__main__ erase
coverage_run="python -m coverage.__main__ run -p"
fi
${command_wrapper} ${coverage_run} $root/manage.py test tuskar_ui --settings=tuskar_ui.test.settings $testopts
# get results of the Horizon tests
TUSKAR_UI_RESULT=$?
if [ $with_coverage -eq 1 ]; then
echo "Generating coverage reports"
${command_wrapper} python -m coverage.__main__ combine
${command_wrapper} python -m coverage.__main__ xml -i --include="tuskar_ui/*" --omit='/usr*,setup.py,*egg*,.venv/*'
${command_wrapper} python -m coverage.__main__ html -i --include="tuskar_ui/*" --omit='/usr*,setup.py,*egg*,.venv/*' -d reports
fi
# Remove the leftover coverage files from the -p flag earlier.
rm -f .coverage.*
PEP8_RESULT=0
if [ $only_selenium -eq 0 ]; then
run_pep8
PEP8_RESULT=$?
fi
TEST_RESULT=$(($TUSKAR_UI_RESULT || $PEP8_RESULT))
if [ $TEST_RESULT -eq 0 ]; then
echo "Tests completed successfully."
else
echo "Tests failed."
fi
exit $TEST_RESULT
}
function run_integration_tests {
export INTEGRATION_TESTS=1
if [ $selenium_headless -eq 1 ]; then
export SELENIUM_HEADLESS=1
fi
echo "Running Tuskar-UI integration tests..."
if [ -z "$testargs" ]; then
${command_wrapper} nosetests tuskar_ui/test/integration_tests/tests
else
${command_wrapper} nosetests $testargs
fi
exit 0
}
function run_makemessages {
cd horizon
${command_wrapper} $root/manage.py makemessages --all --no-obsolete
HORIZON_PY_RESULT=$?
${command_wrapper} $root/manage.py makemessages -d djangojs --all --no-obsolete
HORIZON_JS_RESULT=$?
cd ../openstack_dashboard
${command_wrapper} $root/manage.py makemessages --all --no-obsolete
DASHBOARD_RESULT=$?
cd ..
exit $(($HORIZON_PY_RESULT || $HORIZON_JS_RESULT || $DASHBOARD_RESULT))
}
function run_compilemessages {
cd horizon
${command_wrapper} $root/manage.py compilemessages
HORIZON_PY_RESULT=$?
cd ../openstack_dashboard
${command_wrapper} $root/manage.py compilemessages
DASHBOARD_RESULT=$?
cd ..
exit $(($HORIZON_PY_RESULT || $DASHBOARD_RESULT))
}
# ---------PREPARE THE ENVIRONMENT------------ #
# PROCESS ARGUMENTS, OVERRIDE DEFAULTS
for arg in "$@"; do
process_option $arg
done
if [ $quiet -eq 1 ] && [ $never_venv -eq 0 ] && [ $always_venv -eq 0 ]
then
always_venv=1
fi
# If destroy is set, just blow it away and exit.
if [ $destroy -eq 1 ]; then
destroy_venv
exit 0
fi
# Ignore all of this if the -N flag was set
if [ $never_venv -eq 0 ]; then
# Restore previous environment if desired
if [ $restore_env -eq 1 ]; then
restore_environment
fi
# Remove the virtual environment if --force used
if [ $force -eq 1 ]; then
destroy_venv
fi
# Then check if it's up-to-date
environment_check
# Create a backup of the up-to-date environment if desired
if [ $backup_env -eq 1 ]; then
backup_environment
fi
fi
# ---------EXERCISE THE CODE------------ #
# Run management commands
if [ $manage -eq 1 ]; then
run_management_command
exit $?
fi
# Build the docs
if [ $just_docs -eq 1 ]; then
run_sphinx
exit $?
fi
# Update translation files
if [ $makemessages -eq 1 ]; then
run_makemessages
exit $?
fi
# Compile translation files
if [ $compilemessages -eq 1 ]; then
run_compilemessages
exit $?
fi
# PEP8
if [ $just_pep8 -eq 1 ]; then
run_pep8
exit $?
fi
if [ $just_pep8_changed -eq 1 ]; then
run_pep8_changed
exit $?
fi
# Pylint
if [ $just_pylint -eq 1 ]; then
run_pylint
exit $?
fi
# Jshint
if [ $just_jshint -eq 1 ]; then
run_jshint
exit $?
fi
# Jscs
if [ $just_jscs -eq 1 ]; then
run_jscs
exit $?
fi
# Tab checker
if [ $just_tabs -eq 1 ]; then
tab_check
exit $?
fi
# Django development server
if [ $runserver -eq 1 ]; then
run_server
exit $?
fi
# Full test suite
run_tests || exit

View File

@ -1,41 +0,0 @@
[metadata]
name = tuskar-ui
version = 2013.2
summary = Tuskar Management Dashboard
description-file =
README.rst
author = OpenStack
author-email = openstack-dev@lists.openstack.org
home-page = http://www.openstack.org/
classifier =
Development Status :: 5 - Production/Stable
Environment :: OpenStack
Framework :: Django
Intended Audience :: Developers
Intended Audience :: Information Technology
Intended Audience :: System Administrators
License :: OSI Approved :: Apache Software License
Operating System :: OS Independent
Operating System :: POSIX :: Linux
Programming Language :: Python
Programming Language :: Python :: 2
Programming Language :: Python :: 2.7
Programming Language :: Python :: 2.6
Topic :: Internet :: WWW/HTTP
[global]
setup-hooks =
pbr.hooks.setup_hook
[files]
packages =
tuskar_ui
[build_sphinx]
all_files = 1
build-dir = doc/build
source-dir = doc/source
[nosetests]
verbosity=2
detailed-errors=1

View File

@ -1,29 +0,0 @@
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
import setuptools
# In python < 2.7.4, a lazy loading of package `pbr` will break
# setuptools if some other modules registered functions in `atexit`.
# solution from: http://bugs.python.org/issue15881#msg170215
try:
import multiprocessing # noqa
except ImportError:
pass
setuptools.setup(
setup_requires=['pbr>=1.8'],
pbr=True)

View File

@ -1,37 +0,0 @@
# 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.
# Hacking already pins down pep8, pyflakes and flake8
hacking<0.11,>=0.10.0
# Testing Requirements
http://tarballs.openstack.org/horizon/horizon-master.tar.gz#egg=horizon
http://tarballs.openstack.org/python-tuskarclient/python-tuskarclient-master.tar.gz#egg=python-tuskarclient
coverage>=3.6
django-nose>=1.2
mock>=1.2
mox>=0.5.3
mox3>=0.7.0
nodeenv>=0.9.4 # BSD License
nose
nose-exclude
nosexcover
openstack.nose-plugin>=0.7
nosehtmloutput>=0.0.3
selenium
xvfbwrapper>=0.1.3 #license: MIT
# Docs Requirements
sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2
oslosphinx>=2.5.0 # Apache-2.0

View File

@ -1,154 +0,0 @@
# Copyright 2012 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Copyright 2012 OpenStack, LLC
#
# Copyright 2012 Nebula, 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.
"""
Installation script for the OpenStack Dashboard development virtualenv.
"""
import os
import subprocess
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
VENV = os.path.join(ROOT, '.venv')
WITH_VENV = os.path.join(ROOT, 'tools', 'with_venv.sh')
PIP_REQUIRES = os.path.join(ROOT, 'requirements.txt')
TEST_REQUIRES = os.path.join(ROOT, 'test-requirements.txt')
def die(message, *args):
print >> sys.stderr, message % args
sys.exit(1)
def run_command(cmd, redirect_output=True, check_exit_code=True, cwd=ROOT,
die_message=None):
"""
Runs a command in an out-of-process shell, returning the
output of that command. Working directory is ROOT.
"""
if redirect_output:
stdout = subprocess.PIPE
else:
stdout = None
proc = subprocess.Popen(cmd, cwd=cwd, stdout=stdout)
output = proc.communicate()[0]
if check_exit_code and proc.returncode != 0:
if die_message is None:
die('Command "%s" failed.\n%s', ' '.join(cmd), output)
else:
die(die_message)
return output
HAS_EASY_INSTALL = bool(run_command(['which', 'easy_install'],
check_exit_code=False).strip())
HAS_VIRTUALENV = bool(run_command(['which', 'virtualenv'],
check_exit_code=False).strip())
def check_dependencies():
"""Make sure virtualenv is in the path."""
print 'Checking dependencies...'
if not HAS_VIRTUALENV:
print 'Virtual environment not found.'
# Try installing it via easy_install...
if HAS_EASY_INSTALL:
print 'Installing virtualenv via easy_install...',
run_command(['easy_install', 'virtualenv'],
die_message='easy_install failed to install virtualenv'
'\ndevelopment requires virtualenv, please'
' install it using your favorite tool')
if not run_command(['which', 'virtualenv']):
die('ERROR: virtualenv not found in path.\n\ndevelopment '
' requires virtualenv, please install it using your'
' favorite package management tool and ensure'
' virtualenv is in your path')
print 'virtualenv installation done.'
else:
die('easy_install not found.\n\nInstall easy_install'
' (python-setuptools in ubuntu) or virtualenv by hand,'
' then rerun.')
print 'dependency check done.'
def create_virtualenv(venv=VENV):
"""Creates the virtual environment and installs PIP only into the
virtual environment
"""
print 'Creating venv...',
run_command(['virtualenv', '-q', '--no-site-packages', VENV])
print 'done.'
print 'Installing pip in virtualenv...',
if not run_command([WITH_VENV, 'easy_install', 'pip']).strip():
die("Failed to install pip.")
print 'done.'
print 'Installing distribute in virtualenv...'
pip_install('distribute>=0.6.24')
print 'done.'
def pip_install(*args):
args = [WITH_VENV, 'pip', 'install', '--upgrade'] + list(args)
run_command(args, redirect_output=False)
def install_dependencies(venv=VENV):
print "Installing dependencies..."
print "(This may take several minutes, don't panic)"
pip_install('-r', TEST_REQUIRES)
pip_install('-r', PIP_REQUIRES)
# Tell the virtual env how to "import dashboard"
py = 'python%d.%d' % (sys.version_info[0], sys.version_info[1])
pthfile = os.path.join(venv, "lib", py, "site-packages", "dashboard.pth")
f = open(pthfile, 'w')
f.write("%s\n" % ROOT)
def install_horizon():
print 'Installing horizon module in development mode...'
run_command([WITH_VENV, 'python', 'setup.py', 'develop'], cwd=ROOT)
def print_summary():
summary = """
Horizon development environment setup is complete.
To activate the virtualenv for the extent of your current shell session you
can run:
$ source .venv/bin/activate
"""
print summary
def main():
check_dependencies()
create_virtualenv()
install_dependencies()
install_horizon()
print_summary()
if __name__ == '__main__':
main()

View File

@ -1,4 +0,0 @@
#!/bin/bash
TOOLS=`dirname $0`
VENV=$TOOLS/../.venv
source $VENV/bin/activate && $@

68
tox.ini
View File

@ -1,68 +0,0 @@
[tox]
envlist = py27,py27dj14,py27dj15,py27dj16,pep8,selenium,jshint
[testenv]
setenv = VIRTUAL_ENV={envdir}
NOSE_WITH_OPENSTACK=1
NOSE_OPENSTACK_COLOR=1
NOSE_OPENSTACK_RED=0.05
NOSE_OPENSTACK_YELLOW=0.025
NOSE_OPENSTACK_SHOW_ELAPSED=1
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
commands = /bin/bash run_tests.sh -N
[testenv:pep8]
commands = /bin/bash run_tests.sh -N --pep8
[testenv:venv]
commands = {posargs}
[testenv:cover]
commands = /bin/bash run_tests.sh -N --coverage
[testenv:py27dj14]
basepython = python2.7
commands = pip install django>=1.4,<1.5
/bin/bash run_tests.sh -N
[testenv:py27dj15]
basepython = python2.7
commands = pip install django>=1.5,<1.6
/bin/bash run_tests.sh -N
[testenv:py27dj16]
basepython = python2.7
commands = pip install django>=1.6,<1.7
/bin/bash run_tests.sh -N
[testenv:selenium]
commands = /bin/bash run_tests.sh -N --only-selenium
[testenv:jshint]
commands = nodeenv -p
npm install jshint -g
/bin/bash run_tests.sh -N --jshint
[flake8]
builtins = _
exclude = .venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,
build,panel_template,dash_template,local_settings.py
[hacking]
import_exceptions = collections.defaultdict,
django.conf.settings,
django.core.urlresolvers.reverse,
django.core.urlresolvers.reverse_lazy,
django.template.loader.render_to_string,
django.utils.datastructures.SortedDict,
django.utils.encoding.force_unicode,
django.utils.html.conditional_escape,
django.utils.html.escape,
django.utils.http.urlencode,
django.utils.safestring.mark_safe,
django.utils.translation.pgettext_lazy,
django.utils.translation.ugettext_lazy,
django.utils.translation.ungettext_lazy,
operator.attrgetter,
StringIO.StringIO

View File

View File

@ -1,121 +0,0 @@
# 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.utils import memoized
from openstack_dashboard.api import nova
import tuskar_ui
from tuskar_ui.cached_property import cached_property # noqa
from tuskar_ui.handle_errors import handle_errors # noqa
LOG = logging.getLogger(__name__)
class Flavor(object):
def __init__(self, flavor):
"""Construct by wrapping Nova flavor
:param flavor: Nova flavor
:type flavor: novaclient.v2.flavors.Flavor
"""
self._flavor = flavor
def __getattr__(self, name):
return getattr(self._flavor, name)
@property
def ram_bytes(self):
"""Get RAM size in bytes
Default RAM size is in MB.
"""
return self.ram * 1024 * 1024
@property
def disk_bytes(self):
"""Get disk size in bytes
Default disk size is in GB.
"""
return self.disk * 1024 * 1024 * 1024
@cached_property
def extras_dict(self):
"""Return extra flavor parameters
:return: Nova flavor keys
:rtype: dict
"""
return self._flavor.get_keys()
@property
def cpu_arch(self):
return self.extras_dict.get('cpu_arch', '')
@property
def kernel_image_id(self):
return self.extras_dict.get('baremetal:deploy_kernel_id', '')
@property
def ramdisk_image_id(self):
return self.extras_dict.get('baremetal:deploy_ramdisk_id', '')
@classmethod
def create(cls, request, name, memory, vcpus, disk, cpu_arch,
kernel_image_id=None, ramdisk_image_id=None):
extras_dict = {
'cpu_arch': cpu_arch,
'capabilities:boot_option': 'local',
}
if kernel_image_id is not None:
extras_dict['baremetal:deploy_kernel_id'] = kernel_image_id
if ramdisk_image_id is not None:
extras_dict['baremetal:deploy_ramdisk_id'] = ramdisk_image_id
return cls(nova.flavor_create(request, name, memory, vcpus, disk,
metadata=extras_dict))
@classmethod
@handle_errors(_("Unable to load flavor."))
def get(cls, request, flavor_id):
return cls(nova.flavor_get(request, flavor_id))
@classmethod
@handle_errors(_("Unable to load flavor."))
def get_by_name(cls, request, name):
for flavor in cls.list(request):
if flavor.name == name:
return flavor
@classmethod
@handle_errors(_("Unable to retrieve flavor list."), [])
def list(cls, request):
return [cls(item) for item in nova.flavor_list(request)]
@classmethod
@memoized.memoized
@handle_errors(_("Unable to retrieve existing servers list."), [])
def list_deployed_ids(cls, request):
"""Get and memoize ID's of deployed flavors."""
servers = nova.server_list(request)[0]
deployed_ids = set(server.flavor['id'] for server in servers)
deployed_names = []
for plan in tuskar_ui.api.tuskar.Plan.list(request):
deployed_names.extend(
[plan.parameter_value(role.flavor_parameter_name)
for role in plan.role_list])
return [flavor.id for flavor in cls.list(request)
if flavor.id in deployed_ids or flavor.name in deployed_names]

View File

@ -1,553 +0,0 @@
# 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
import os
import tempfile
import urlparse
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from heatclient.common import template_utils
from heatclient.exc import HTTPNotFound
from horizon.utils import memoized
from openstack_dashboard.api import base
from openstack_dashboard.api import heat
from openstack_dashboard.api import keystone
from tuskar_ui.api import node
from tuskar_ui.api import tuskar
from tuskar_ui.cached_property import cached_property # noqa
from tuskar_ui.handle_errors import handle_errors # noqa
from tuskar_ui.utils import utils
LOG = logging.getLogger(__name__)
@memoized.memoized
def overcloud_keystoneclient(request, endpoint, password):
"""Returns a client connected to the Keystone backend.
Several forms of authentication are supported:
* Username + password -> Unscoped authentication
* Username + password + tenant id -> Scoped authentication
* Unscoped token -> Unscoped authentication
* Unscoped token + tenant id -> Scoped authentication
* Scoped token -> Scoped authentication
Available services and data from the backend will vary depending on
whether the authentication was scoped or unscoped.
Lazy authentication if an ``endpoint`` parameter is provided.
Calls requiring the admin endpoint should have ``admin=True`` passed in
as a keyword argument.
The client is cached so that subsequent API calls during the same
request/response cycle don't have to be re-authenticated.
"""
api_version = keystone.VERSIONS.get_active_version()
# TODO(lsmola) add support of certificates and secured http and rest of
# parameters according to horizon and add configuration to local settings
# (somehow plugin based, we should not maintain a copy of settings)
LOG.debug("Creating a new keystoneclient connection to %s." % endpoint)
# TODO(lsmola) we should create tripleo-admin user for this purpose
# this needs to be done first on tripleo side
conn = api_version['client'].Client(username="admin",
password=password,
tenant_name="admin",
auth_url=endpoint)
return conn
def _save_templates(templates):
"""Saves templates into tmpdir on server
This should go away and get replaced by libutils.save_templates from
tripleo-common https://github.com/openstack/tripleo-common/
"""
output_dir = tempfile.mkdtemp()
for template_name, template_content in templates.items():
# It's possible to organize the role templates and their dependent
# files into directories, in which case the template_name will carry
# the directory information. If that's the case, first create the
# directory structure (if it hasn't already been created by another
# file in the templates list).
template_dir = os.path.dirname(template_name)
output_template_dir = os.path.join(output_dir, template_dir)
if template_dir and not os.path.exists(output_template_dir):
os.makedirs(output_template_dir)
filename = os.path.join(output_dir, template_name)
with open(filename, 'w+') as template_file:
template_file.write(template_content)
return output_dir
def _process_templates(templates):
"""Process templates
Due to bug in heat api
https://bugzilla.redhat.com/show_bug.cgi?id=1212740, we need to
save the templates in tmpdir, reprocess them with template_utils
from heatclient and then we can use them in creating/updating stack.
This should be replaced by the same code that is in tripleo-common and
eventually it will not be needed at all.
"""
tpl_dir = _save_templates(templates)
tpl_files, template = template_utils.get_template_contents(
template_file=os.path.join(tpl_dir, tuskar.MASTER_TEMPLATE_NAME))
env_files, env = (
template_utils.process_multiple_environments_and_files(
env_paths=[os.path.join(tpl_dir, tuskar.ENVIRONMENT_NAME)]))
files = dict(list(tpl_files.items()) + list(env_files.items()))
return template, env, files
class Stack(base.APIResourceWrapper):
_attrs = ('id', 'stack_name', 'outputs', 'stack_status', 'parameters')
def __init__(self, apiresource, request=None):
super(Stack, self).__init__(apiresource)
self._request = request
@classmethod
def create(cls, request, stack_name, templates):
template, environment, files = _process_templates(templates)
fields = {
'stack_name': stack_name,
'template': template,
'environment': environment,
'files': files,
'timeout_mins': 240,
}
password = getattr(settings, 'UNDERCLOUD_ADMIN_PASSWORD', None)
stack = heat.stack_create(request, password, **fields)
return cls(stack, request=request)
def update(self, request, stack_name, templates):
template, environment, files = _process_templates(templates)
fields = {
'stack_name': stack_name,
'template': template,
'environment': environment,
'files': files,
}
password = getattr(settings, 'UNDERCLOUD_ADMIN_PASSWORD', None)
heat.stack_update(request, self.id, password, **fields)
@classmethod
@handle_errors(_("Unable to retrieve heat stacks"), [])
def list(cls, request):
"""Return a list of stacks in Heat
:param request: request object
:type request: django.http.HttpRequest
:return: list of Heat stacks, or an empty list if there
are none
:rtype: list of tuskar_ui.api.heat.Stack
"""
stacks, has_more_data, has_prev_data = heat.stacks_list(request)
return [cls(stack, request=request) for stack in stacks]
@classmethod
@handle_errors(_("Unable to retrieve stack"))
def get(cls, request, stack_id):
"""Return the Heat Stack associated with this Overcloud
:return: Heat Stack associated with the stack_id; or None
if no Stack is associated, or no Stack can be
found
:rtype: tuskar_ui.api.heat.Stack or None
"""
return cls(heat.stack_get(request, stack_id), request=request)
@classmethod
@handle_errors(_("Unable to retrieve stack"))
def get_by_plan(cls, request, plan):
"""Return the Heat Stack associated with a Plan
:return: Heat Stack associated with the plan; or None
if no Stack is associated, or no Stack can be
found
:rtype: tuskar_ui.api.heat.Stack or None
"""
# TODO(lsmola) until we have working deployment through Tuskar-API,
# this will not work
# for stack in Stack.list(request):
# if stack.plan and (stack.plan.id == plan.id):
# return stack
try:
stack = Stack.list(request)[0]
except IndexError:
return None
# TODO(lsmola) stack list actually does not contain all the detail
# info, there should be call for that, investigate
return Stack.get(request, stack.id)
@classmethod
@handle_errors(_("Unable to delete Heat stack"), [])
def delete(cls, request, stack_id):
heat.stack_delete(request, stack_id)
@memoized.memoized
def resources(self, with_joins=True, role=None):
"""Return list of OS::Nova::Server Resources
Return list of OS::Nova::Server Resources associated with the Stack
and which are associated with a Role
:param with_joins: should we also retrieve objects associated with each
retrieved Resource?
:type with_joins: bool
:return: list of all Resources or an empty list if there are none
:rtype: list of tuskar_ui.api.heat.Resource
"""
if role:
roles = [role]
else:
roles = self.plan.role_list
resource_dicts = []
# A provider resource is deployed as a nested stack, so we have to
# drill down and retrieve those that match a tuskar role
for role in roles:
resource_group_name = role.name
try:
resource_group = heat.resource_get(self._request,
self.id,
resource_group_name)
group_resources = heat.resources_list(
self._request, resource_group.physical_resource_id)
for group_resource in group_resources:
if not group_resource.physical_resource_id:
# Skip groups who has no physical resource.
continue
nova_resources = heat.resources_list(
self._request,
group_resource.physical_resource_id)
resource_dicts.extend([{"resource": resource,
"role": role}
for resource in nova_resources])
except HTTPNotFound:
pass
if not with_joins:
return [Resource(rd['resource'], request=self._request,
stack=self, role=rd['role'])
for rd in resource_dicts]
nodes_dict = utils.list_to_dict(node.Node.list(self._request,
associated=True),
key_attribute='instance_uuid')
joined_resources = []
for rd in resource_dicts:
resource = rd['resource']
joined_resources.append(
Resource(resource,
node=nodes_dict.get(resource.physical_resource_id,
None),
request=self._request, stack=self, role=rd['role']))
# TODO(lsmola) I want just resources with nova instance
# this could be probably filtered a better way, investigate
return [r for r in joined_resources if r.node is not None]
@memoized.memoized
def resources_count(self, overcloud_role=None):
"""Return count of associated Resources
:param overcloud_role: role of resources to be counted; None means all
:type overcloud_role: tuskar_ui.api.tuskar.Role
:return: Number of matching resources
:rtype: int
"""
# TODO(dtantsur): there should be better way to do it, rather than
# fetching and calling len()
# FIXME(dtantsur): should also be able to use with_joins=False
# but unable due to bug #1289505
if overcloud_role is None:
resources = self.resources()
else:
resources = self.resources(role=overcloud_role)
return len(resources)
@cached_property
def plan(self):
"""return associated Plan if a plan_id exists within stack parameters.
:return: associated Plan if plan_id exists and a matching plan
exists as well; None otherwise
:rtype: tuskar_ui.api.tuskar.Plan
"""
# TODO(lsmola) replace this by actual reference, I am pretty sure
# the relation won't be stored in parameters, that would mean putting
# that into template, which doesn't make sense
# if 'plan_id' in self.parameters:
# return tuskar.Plan.get(self._request,
# self.parameters['plan_id'])
try:
plan = tuskar.Plan.list(self._request)[0]
except IndexError:
return None
return plan
@cached_property
def is_initialized(self):
"""Check if this Stack is successfully initialized.
:return: True if this Stack is successfully initialized, False
otherwise
:rtype: bool
"""
return len(self.dashboard_urls) > 0
@cached_property
def is_deployed(self):
"""Check if this Stack is successfully deployed.
:return: True if this Stack is successfully deployed, False otherwise
:rtype: bool
"""
return self.stack_status in ('CREATE_COMPLETE',
'UPDATE_COMPLETE')
@cached_property
def is_deploying(self):
"""Check if this Stack is currently deploying.
:return: True if deployment is in progress, False otherwise.
:rtype: bool
"""
return self.stack_status in ('CREATE_IN_PROGRESS',)
@cached_property
def is_updating(self):
"""Check if this Stack is currently updating.
:return: True if updating is in progress, False otherwise.
:rtype: bool
"""
return self.stack_status in ('UPDATE_IN_PROGRESS',)
@cached_property
def is_failed(self):
"""Check if this Stack failed to update or deploy.
:return: True if deployment there was an error, False otherwise.
:rtype: bool
"""
return self.stack_status in ('CREATE_FAILED',
'UPDATE_FAILED',)
@cached_property
def is_deleting(self):
"""Check if this Stack is deleting.
:return: True if Stack is deleting, False otherwise.
:rtype: bool
"""
return self.stack_status in ('DELETE_IN_PROGRESS', )
@cached_property
def is_delete_failed(self):
"""Check if Stack deleting has failed.
:return: True if Stack deleting has failed, False otherwise.
:rtype: bool
"""
return self.stack_status in ('DELETE_FAILED', )
@cached_property
def events(self):
"""Return the Heat Events associated with this Stack
:return: list of Heat Events associated with this Stack;
or an empty list if there is no Stack associated with
this Stack, or there are no Events
:rtype: list of heatclient.v1.events.Event
"""
return heat.events_list(self._request,
self.stack_name)
@property
def stack_outputs(self):
return getattr(self, 'outputs', [])
@cached_property
def keystone_auth_url(self):
for output in self.stack_outputs:
if output['output_key'] == 'KeystoneURL':
return output['output_value']
@cached_property
def keystone_ip(self):
if self.keystone_auth_url:
return urlparse.urlparse(self.keystone_auth_url).hostname
@cached_property
def overcloud_keystone(self):
try:
return overcloud_keystoneclient(
self._request,
self.keystone_auth_url,
self.plan.parameter_value('Controller-1::AdminPassword'))
except Exception:
LOG.debug('Unable to connect to overcloud keystone.')
return None
@cached_property
def dashboard_urls(self):
client = self.overcloud_keystone
if not client:
return []
try:
services = client.services.list()
for service in services:
if service.name == 'horizon':
break
else:
return []
except Exception:
return []
admin_urls = [endpoint.adminurl for endpoint
in client.endpoints.list()
if endpoint.service_id == service.id]
return admin_urls
class Resource(base.APIResourceWrapper):
_attrs = ('resource_name', 'resource_type', 'resource_status',
'physical_resource_id')
def __init__(self, apiresource, request=None, **kwargs):
"""Initialize a resource
:param apiresource: apiresource we want to wrap
:type apiresource: heatclient.v1.resources.Resource
:param request: request
:type request: django.core.handlers.wsgi.WSGIRequest
:param node: node relation we want to cache
:type node: tuskar_ui.api.node.Node
:return: Resource object
:rtype: Resource
"""
super(Resource, self).__init__(apiresource)
self._request = request
if 'node' in kwargs:
self._node = kwargs['node']
if 'stack' in kwargs:
self._stack = kwargs['stack']
if 'role' in kwargs:
self._role = kwargs['role']
@classmethod
@memoized.memoized
def _resources_by_nodes(cls, request):
return {resource.physical_resource_id: resource
for resource in cls.list_all_resources(request)}
@classmethod
def get_by_node(cls, request, node):
"""Return the specified Heat Resource given a Node
:param request: request object
:type request: django.http.HttpRequest
:param node: node to match
:type node: tuskar_ui.api.node.Node
:return: matching Resource, or raises LookupError if no
resource matches the node
:rtype: tuskar_ui.api.heat.Resource
"""
return cls._resources_by_nodes(request)[node.instance_uuid]
@classmethod
def list_all_resources(cls, request):
"""Iterate through all the stacks and return all relevant resources
:param request: request object
:type request: django.http.HttpRequest
:return: list of resources
:rtype: list of tuskar_ui.api.heat.Resource
"""
all_resources = []
for stack in Stack.list(request):
all_resources.extend(stack.resources(with_joins=False))
return all_resources
@cached_property
def role(self):
"""Return the Role associated with this Resource
:return: Role associated with this Resource, or None if no
Role is associated
:rtype: tuskar_ui.api.tuskar.Role
"""
if hasattr(self, '_role'):
return self._role
@cached_property
def node(self):
"""Return the Ironic Node associated with this Resource
:return: Ironic Node associated with this Resource, or None if no
Node is associated
:rtype: tuskar_ui.api.node.Node
:raises: ironicclient.exc.HTTPNotFound if there is no Node with the
matching instance UUID
"""
if hasattr(self, '_node'):
return self._node
if self.physical_resource_id:
return node.Node.get_by_instance_uuid(self._request,
self.physical_resource_id)
return None
@cached_property
def stack(self):
"""Return the Stack associated with this Resource
:return: Stack associated with this Resource, or None if no
Stack is associated
:rtype: tuskar_ui.api.heat.Stack
"""
if hasattr(self, '_stack'):
return self._stack

View File

@ -1,445 +0,0 @@
# 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
import time
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from horizon.utils import memoized
from ironic_inspector_client import client as inspector_client
from ironicclient import client as ironic_client
from openstack_dashboard.api import base
from openstack_dashboard.api import glance
from openstack_dashboard.api import nova
from tuskar_ui.cached_property import cached_property # noqa
from tuskar_ui.handle_errors import handle_errors # noqa
from tuskar_ui.utils import utils
# power states
ERROR_STATES = set(['deploy failed', 'error'])
POWER_ON_STATES = set(['on', 'power on'])
# provision_states of ironic aggregated to reasonable groups
PROVISION_STATE_FREE = ['available', 'deleted', None]
PROVISION_STATE_PROVISIONED = ['active']
PROVISION_STATE_PROVISIONING = [
'deploying', 'wait call-back', 'rebuild', 'deploy complete']
PROVISION_STATE_DELETING = ['deleting']
PROVISION_STATE_ERROR = ['error', 'deploy failed']
# names for states of ironic used in UI,
# provison_states + discovery states
DISCOVERING_STATE = 'discovering'
DISCOVERED_STATE = 'discovered'
DISCOVERY_FAILED_STATE = 'discovery failed'
MAINTENANCE_STATE = 'manageable'
PROVISIONED_STATE = 'provisioned'
PROVISIONING_FAILED_STATE = 'provisioning failed'
PROVISIONING_STATE = 'provisioning'
DELETING_STATE = 'deleting'
FREE_STATE = 'free'
IRONIC_DISCOVERD_URL = getattr(settings, 'IRONIC_DISCOVERD_URL', None)
LOG = logging.getLogger(__name__)
@memoized.memoized
def ironicclient(request):
api_version = 1
kwargs = {'os_auth_token': request.user.token.id,
'ironic_url': base.url_for(request, 'baremetal')}
return ironic_client.get_client(api_version, **kwargs)
# FIXME(lsmola) This should be done in Horizon, they don't have caching
@memoized.memoized
@handle_errors(_("Unable to retrieve image."))
def image_get(request, image_id):
"""Returns an Image object with metadata
Returns an Image object populated with metadata for image
with supplied identifier.
:param image_id: list of objects to be put into a dict
:type image_id: list
:return: object
:rtype: glanceclient.v1.images.Image
"""
image = glance.image_get(request, image_id)
return image
class Node(base.APIResourceWrapper):
_attrs = ('id', 'uuid', 'instance_uuid', 'driver', 'driver_info',
'properties', 'power_state', 'target_power_state',
'provision_state', 'maintenance', 'extra')
def __init__(self, apiresource, request=None, instance=None):
"""Initialize a Node
:param apiresource: apiresource we want to wrap
:type apiresource: IronicNode
:param request: request
:type request: django.core.handlers.wsgi.WSGIRequest
:param instance: instance relation we want to cache
:type instance: openstack_dashboard.api.nova.Server
:return: Node object
:rtype: tusar_ui.api.node.Node
"""
super(Node, self).__init__(apiresource)
self._request = request
self._instance = instance
@classmethod
def create(cls, request, ipmi_address=None, cpu_arch=None, cpus=None,
memory_mb=None, local_gb=None, mac_addresses=[],
ipmi_username=None, ipmi_password=None, ssh_address=None,
ssh_username=None, ssh_key_contents=None,
deployment_kernel=None, deployment_ramdisk=None,
driver=None):
"""Create a Node in Ironic."""
if driver == 'pxe_ssh':
driver_info = {
'ssh_address': ssh_address,
'ssh_username': ssh_username,
'ssh_key_contents': ssh_key_contents,
'ssh_virt_type': 'virsh',
}
else:
driver_info = {
'ipmi_address': ipmi_address,
'ipmi_username': ipmi_username,
'ipmi_password': ipmi_password
}
driver_info.update(
deploy_kernel=deployment_kernel,
deploy_ramdisk=deployment_ramdisk
)
properties = {'capabilities': 'boot_option:local', }
if cpus:
properties.update(cpus=cpus)
if memory_mb:
properties.update(memory_mb=memory_mb)
if local_gb:
properties.update(local_gb=local_gb)
if cpu_arch:
properties.update(cpu_arch=cpu_arch)
node = ironicclient(request).node.create(
driver=driver,
driver_info=driver_info,
properties=properties,
)
for mac_address in mac_addresses:
ironicclient(request).port.create(
node_uuid=node.uuid,
address=mac_address
)
return cls(node, request)
@classmethod
@memoized.memoized
@handle_errors(_("Unable to retrieve node"))
def get(cls, request, uuid):
"""Return the Node that matches the ID
:param request: request object
:type request: django.http.HttpRequest
:param uuid: ID of Node to be retrieved
:type uuid: str
:return: matching Node, or None if no IronicNode matches the ID
:rtype: tuskar_ui.api.node.Node
"""
node = ironicclient(request).node.get(uuid)
if node.instance_uuid is not None:
server = nova.server_get(request, node.instance_uuid)
else:
server = None
return cls(node, request, server)
@classmethod
@handle_errors(_("Unable to retrieve node"))
def get_by_instance_uuid(cls, request, instance_uuid):
"""Return the Node associated with the instance ID
:param request: request object
:type request: django.http.HttpRequest
:param instance_uuid: ID of Instance that is deployed on the Node
to be retrieved
:type instance_uuid: str
:return: matching Node
:rtype: tuskar_ui.api.node.Node
:raises: ironicclient.exc.HTTPNotFound if there is no Node with
the matching instance UUID
"""
node = ironicclient(request).node.get_by_instance_uuid(instance_uuid)
server = nova.server_get(request, instance_uuid)
return cls(node, request, server)
@classmethod
@memoized.memoized
@handle_errors(_("Unable to retrieve nodes"), [])
def list(cls, request, associated=None, maintenance=None):
"""Return a list of Nodes
:param request: request object
:type request: django.http.HttpRequest
:param associated: should we also retrieve all Nodes, only those
associated with an Instance, or only those not
associated with an Instance?
:type associated: bool
:param maintenance: should we also retrieve all Nodes, only those
in maintenance mode, or those which are not in
maintenance mode?
:type maintenance: bool
:return: list of Nodes, or an empty list if there are none
:rtype: list of tuskar_ui.api.node.Node
"""
nodes = ironicclient(request).node.list(associated=associated,
maintenance=maintenance)
if associated is None or associated:
servers = nova.server_list(request)[0]
servers_dict = utils.list_to_dict(servers)
nodes_with_instance = []
for n in nodes:
server = servers_dict.get(n.instance_uuid, None)
nodes_with_instance.append(cls(n, instance=server,
request=request))
return [cls.get(request, node.uuid)
for node in nodes_with_instance]
return [cls.get(request, node.uuid) for node in nodes]
@classmethod
def delete(cls, request, uuid):
"""Delete an Node
Remove the IronicNode matching the ID if it
exists; otherwise, does nothing.
:param request: request object
:type request: django.http.HttpRequest
:param uuid: ID of IronicNode to be removed
:type uuid: str
"""
return ironicclient(request).node.delete(uuid)
@classmethod
def discover(cls, request, uuids):
"""Set the maintenance status of node
:param request: request object
:type request: django.http.HttpRequest
:param uuids: IDs of IronicNodes
:type uuids: list of str
"""
if not IRONIC_DISCOVERD_URL:
return
for uuid in uuids:
inspector_client.introspect(
uuid,
base_url=IRONIC_DISCOVERD_URL,
auth_token=request.user.token.id)
# NOTE(dtantsur): PXE firmware on virtual machines misbehaves when
# a lot of nodes start DHCPing simultaneously: it ignores NACK from
# DHCP server, tries to get the same address, then times out. Work
# around it by using sleep, anyway introspection takes much longer.
time.sleep(5)
@classmethod
def set_maintenance(cls, request, uuid, maintenance):
"""Set the maintenance status of node
:param request: request object
:type request: django.http.HttpRequest
:param uuid: ID of Node to be removed
:type uuid: str
:param maintenance: desired maintenance state
:type maintenance: bool
"""
patch = {
'op': 'replace',
'value': 'True' if maintenance else 'False',
'path': '/maintenance'
}
node = ironicclient(request).node.update(uuid, [patch])
return cls(node, request)
@classmethod
def set_power_state(cls, request, uuid, power_state):
"""Set the power_state of node
:param request: request object
:type request: django.http.HttpRequest
:param uuid: ID of Node
:type uuid: str
:param power_state: desired power_state
:type power_state: str
"""
node = ironicclient(request).node.set_power_state(uuid, power_state)
return cls(node, request)
@classmethod
@memoized.memoized
def list_ports(cls, request, uuid):
"""Return a list of ports associated with this Node
:param request: request object
:type request: django.http.HttpRequest
:param uuid: ID of IronicNode
:type uuid: str
"""
return ironicclient(request).node.list_ports(uuid)
@cached_property
def addresses(self):
"""Return a list of port addresses associated with this IronicNode
:return: list of port addresses associated with this IronicNode, or
an empty list if no addresses are associated with
this IronicNode
:rtype: list of str
"""
ports = self.list_ports(self._request, self.uuid)
return [port.address for port in ports]
@cached_property
def cpus(self):
return self.properties.get('cpus', None)
@cached_property
def memory_mb(self):
return self.properties.get('memory_mb', None)
@cached_property
def local_gb(self):
return self.properties.get('local_gb', None)
@cached_property
def cpu_arch(self):
return self.properties.get('cpu_arch', None)
@cached_property
def state(self):
if self.maintenance:
if not IRONIC_DISCOVERD_URL:
return MAINTENANCE_STATE
try:
status = inspector_client.get_status(
uuid=self.uuid,
base_url=IRONIC_DISCOVERD_URL,
auth_token=self._request.user.token.id,
)
except inspector_client.ClientError as e:
if getattr(e.response, 'status_code', None) == 404:
return MAINTENANCE_STATE
raise
if status['error']:
return DISCOVERY_FAILED_STATE
elif status['finished']:
return DISCOVERED_STATE
else:
return DISCOVERING_STATE
else:
if self.provision_state in PROVISION_STATE_FREE:
return FREE_STATE
if self.provision_state in PROVISION_STATE_PROVISIONING:
return PROVISIONING_STATE
if self.provision_state in PROVISION_STATE_PROVISIONED:
return PROVISIONED_STATE
if self.provision_state in PROVISION_STATE_DELETING:
return DELETING_STATE
if self.provision_state in PROVISION_STATE_ERROR:
return PROVISIONING_FAILED_STATE
# Unknown state
return None
@cached_property
def instance(self):
"""Return the Nova Instance associated with this Node
:return: Nova Instance associated with this Node; or
None if there is no Instance associated with this
Node, or no matching Instance is found
:rtype: Instance
"""
if self._instance is not None:
return self._instance
if self.instance_uuid:
servers, _has_more_data = nova.server_list(self._request)
for server in servers:
if server.id == self.instance_uuid:
return server
@cached_property
def ip_address(self):
try:
apiresource = self.instace._apiresource
except AttributeError:
LOG.error("Couldn't obtain IP address")
return None
return apiresource.addresses['ctlplane'][0]['addr']
@cached_property
def image_name(self):
"""Return image name of associated instance
Returns image name of instance associated with node
:return: Image name of instance
:rtype: string
"""
if self.instance is None:
return
image = image_get(self._request, self.instance.image['id'])
return image.name
@cached_property
def instance_status(self):
return getattr(getattr(self, 'instance', None), 'status', None)
@cached_property
def provisioning_status(self):
if self.instance_uuid:
return _("Provisioned")
return _("Free")
@classmethod
def get_all_mac_addresses(cls, request):
macs = [node.addresses for node in cls.list(request)]
return set([mac.upper() for sublist in macs for mac in sublist])

View File

@ -1,558 +0,0 @@
# 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
import random
import string
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from glanceclient import exc as glance_exceptions
from horizon.utils import memoized
from openstack_dashboard.api import base
from openstack_dashboard.api import glance
from openstack_dashboard.api import neutron
from os_cloud_config import keystone_pki
from tuskarclient import client as tuskar_client
from tuskar_ui.api import flavor
from tuskar_ui.cached_property import cached_property # noqa
from tuskar_ui.handle_errors import handle_errors # noqa
LOG = logging.getLogger(__name__)
MASTER_TEMPLATE_NAME = 'plan.yaml'
ENVIRONMENT_NAME = 'environment.yaml'
TUSKAR_SERVICE = 'management'
SSL_HIDDEN_PARAMS = ('SSLCertificate', 'SSLKey')
KEYSTONE_CERTIFICATE_PARAMS = (
'KeystoneSigningCertificate', 'KeystoneCACertificate',
'KeystoneSigningKey')
@memoized.memoized
def tuskarclient(request, password=None):
api_version = "2"
insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False)
ca_file = getattr(settings, 'OPENSTACK_SSL_CACERT', None)
endpoint = base.url_for(request, TUSKAR_SERVICE)
LOG.debug('tuskarclient connection created using token "%s" and url "%s"' %
(request.user.token.id, endpoint))
client = tuskar_client.get_client(api_version,
tuskar_url=endpoint,
insecure=insecure,
ca_file=ca_file,
username=request.user.username,
password=password,
os_auth_token=request.user.token.id)
return client
def password_generator(size=40, chars=(string.ascii_uppercase +
string.ascii_lowercase +
string.digits)):
return ''.join(random.choice(chars) for _ in range(size))
def strip_prefix(parameter_name):
return parameter_name.split('::', 1)[-1]
def _is_blank(parameter):
return not parameter['value'] or parameter['value'] == 'unset'
def _should_generate_password(parameter):
# TODO(lsmola) Filter out SSL params for now. Once it will be generated
# in TripleO add it here too. Note: this will also affect how endpoints are
# created
key = parameter['name']
return all([
parameter['hidden'],
_is_blank(parameter),
strip_prefix(key) not in SSL_HIDDEN_PARAMS,
strip_prefix(key) not in KEYSTONE_CERTIFICATE_PARAMS,
key != 'SnmpdReadonlyUserPassword',
])
def _should_generate_keystone_cert(parameter):
return all([
strip_prefix(parameter['name']) in KEYSTONE_CERTIFICATE_PARAMS,
_is_blank(parameter),
])
def _should_generate_neutron_control_plane(parameter):
return all([
strip_prefix(parameter['name']) == 'NeutronControlPlaneID',
_is_blank(parameter),
])
class Plan(base.APIResourceWrapper):
_attrs = ('uuid', 'name', 'description', 'created_at', 'modified_at',
'roles', 'parameters')
def __init__(self, apiresource, request=None):
super(Plan, self).__init__(apiresource)
self._request = request
@classmethod
def create(cls, request, name, description):
"""Create a Plan in Tuskar
:param request: request object
:type request: django.http.HttpRequest
:param name: plan name
:type name: string
:param description: plan description
:type description: string
:return: the created Plan object
:rtype: tuskar_ui.api.tuskar.Plan
"""
plan = tuskarclient(request).plans.create(name=name,
description=description)
return cls(plan, request=request)
@classmethod
def patch(cls, request, plan_id, parameters):
"""Update a Plan in Tuskar
:param request: request object
:type request: django.http.HttpRequest
:param plan_id: id of the plan we want to update
:type plan_id: string
:param parameters: new values for the plan's parameters
:type parameters: dict
:return: the updated Plan object
:rtype: tuskar_ui.api.tuskar.Plan
"""
parameter_list = [{
'name': unicode(name),
'value': unicode(value),
} for (name, value) in parameters.items()]
plan = tuskarclient(request).plans.patch(plan_id, parameter_list)
return cls(plan, request=request)
@classmethod
@memoized.memoized
def list(cls, request):
"""Return a list of Plans in Tuskar
:param request: request object
:type request: django.http.HttpRequest
:return: list of Plans, or an empty list if there are none
:rtype: list of tuskar_ui.api.tuskar.Plan
"""
plans = tuskarclient(request).plans.list()
return [cls(plan, request=request) for plan in plans]
@classmethod
@handle_errors(_("Unable to retrieve plan"))
def get(cls, request, plan_id):
"""Return the Plan that matches the ID
:param request: request object
:type request: django.http.HttpRequest
:param plan_id: id of Plan to be retrieved
:type plan_id: int
:return: matching Plan, or None if no Plan matches
the ID
:rtype: tuskar_ui.api.tuskar.Plan
"""
plan = tuskarclient(request).plans.get(plan_uuid=plan_id)
return cls(plan, request=request)
# TODO(lsmola) before will will support multiple overclouds, we
# can work only with overcloud that is named overcloud. Delete
# this once we have more overclouds. Till then, this is the overcloud
# that rules them all.
# This is how API supports it now, so we have to have it this way.
# Also till Overcloud workflow is done properly, we have to work
# with situations that overcloud is deleted, but stack is still
# there. So overcloud will pretend to exist when stack exist.
@classmethod
def get_the_plan(cls, request):
plan_list = cls.list(request)
for plan in plan_list:
return plan
# if plan doesn't exist, create it
plan = cls.create(request, 'overcloud', 'overcloud')
return plan
@classmethod
def delete(cls, request, plan_id):
"""Delete a Plan
:param request: request object
:type request: django.http.HttpRequest
:param plan_id: plan id
:type plan_id: int
"""
tuskarclient(request).plans.delete(plan_uuid=plan_id)
@cached_property
def role_list(self):
return [Role.get(self._request, role.uuid)
for role in self.roles]
@cached_property
def _roles_by_name(self):
return dict((role.name, role) for role in self.role_list)
def get_role_by_name(self, role_name):
"""Get the role with the given name."""
return self._roles_by_name[role_name]
def get_role_node_count(self, role):
"""Get the node count for the given role."""
return int(self.parameter_value(role.node_count_parameter_name,
0) or 0)
@cached_property
def templates(self):
return tuskarclient(self._request).plans.templates(self.uuid)
@cached_property
def master_template(self):
return self.templates.get(MASTER_TEMPLATE_NAME, '')
@cached_property
def environment(self):
return self.templates.get(ENVIRONMENT_NAME, '')
@cached_property
def provider_resource_templates(self):
template_dict = dict(self.templates)
del template_dict[MASTER_TEMPLATE_NAME]
del template_dict[ENVIRONMENT_NAME]
return template_dict
def parameter_list(self, include_key_parameters=True):
params = self.parameters
if not include_key_parameters:
key_params = []
for role in self.role_list:
key_params.extend([role.node_count_parameter_name,
role.image_parameter_name,
role.flavor_parameter_name])
params = [p for p in params if p['name'] not in key_params]
return [Parameter(p, plan=self) for p in params]
def parameter(self, param_name):
for parameter in self.parameters:
if parameter['name'] == param_name:
return Parameter(parameter, plan=self)
def parameter_value(self, param_name, default=None):
parameter = self.parameter(param_name)
if parameter is not None:
return parameter.value
return default
def list_generated_parameters(self, with_prefix=True):
if with_prefix:
key_format = lambda key: key
else:
key_format = strip_prefix
# Get all password like parameters
return dict(
(key_format(parameter['name']), parameter)
for parameter in self.parameter_list()
if any([
_should_generate_password(parameter),
_should_generate_keystone_cert(parameter),
_should_generate_neutron_control_plane(parameter),
])
)
def _make_keystone_certificates(self, wanted_generated_params):
generated_params = {}
for cert_param in KEYSTONE_CERTIFICATE_PARAMS:
if cert_param in wanted_generated_params.keys():
# If one of the keystone certificates is not set, we have
# to generate all of them.
generate_certificates = True
break
else:
generate_certificates = False
# Generate keystone certificates
if generate_certificates:
ca_key_pem, ca_cert_pem = keystone_pki.create_ca_pair()
signing_key_pem, signing_cert_pem = (
keystone_pki.create_signing_pair(ca_key_pem, ca_cert_pem))
generated_params['KeystoneSigningCertificate'] = (
signing_cert_pem)
generated_params['KeystoneCACertificate'] = ca_cert_pem
generated_params['KeystoneSigningKey'] = signing_key_pem
return generated_params
def make_generated_parameters(self):
wanted_generated_params = self.list_generated_parameters(
with_prefix=False)
# Generate keystone certificates
generated_params = self._make_keystone_certificates(
wanted_generated_params)
# Generate passwords and control plane id
for (key, param) in wanted_generated_params.items():
if _should_generate_password(param):
generated_params[key] = password_generator()
elif _should_generate_neutron_control_plane(param):
generated_params[key] = neutron.network_list(
self._request, name='ctlplane')[0].id
# Fill all the Tuskar parameters with generated content. There are
# parameters that has just different prefix, such parameters should
# have the same values.
wanted_prefixed_params = self.list_generated_parameters(
with_prefix=True)
tuskar_params = {}
for (key, param) in wanted_prefixed_params.items():
tuskar_params[key] = generated_params[strip_prefix(key)]
return tuskar_params
@property
def id(self):
return self.uuid
class Role(base.APIResourceWrapper):
_attrs = ('uuid', 'name', 'version', 'description', 'created')
def __init__(self, apiresource, request=None):
super(Role, self).__init__(apiresource)
self._request = request
@classmethod
@memoized.memoized
@handle_errors(_("Unable to retrieve overcloud roles"), [])
def list(cls, request):
"""Return a list of Overcloud Roles in Tuskar
:param request: request object
:type request: django.http.HttpRequest
:return: list of Overcloud Roles, or an empty list if there
are none
:rtype: list of tuskar_ui.api.tuskar.Role
"""
roles = tuskarclient(request).roles.list()
return [cls(role, request=request) for role in roles]
@classmethod
@memoized.memoized
@handle_errors(_("Unable to retrieve overcloud role"))
def get(cls, request, role_id):
"""Return the Tuskar Role that matches the ID
:param request: request object
:type request: django.http.HttpRequest
:param role_id: ID of Role to be retrieved
:type role_id: int
:return: matching Role, or None if no matching
Role can be found
:rtype: tuskar_ui.api.tuskar.Role
"""
for role in Role.list(request):
if role.uuid == role_id:
return role
@classmethod
@memoized.memoized
def _roles_by_image(cls, request, plan):
roles_by_image = {}
for role in Role.list(request):
image = plan.parameter_value(role.image_parameter_name)
if image in roles_by_image:
roles_by_image[image].append(role)
else:
roles_by_image[image] = [role]
return roles_by_image
@classmethod
@handle_errors(_("Unable to retrieve overcloud role"))
def get_by_image(cls, request, plan, image):
"""Return the Role whose ImageID parameter matches the image.
:param request: request object
:type request: django.http.HttpRequest
:param plan: associated plan to check against
:type plan: Plan
:param image: image to be matched
:type image: Image
:return: matching Role, or None if no matching
Role can be found
:rtype: tuskar_ui.api.tuskar.Role
"""
roles = cls._roles_by_image(request, plan)
try:
return roles[image.name]
except KeyError:
return []
@classmethod
@memoized.memoized
def _roles_by_resource_type(cls, request):
return {role.provider_resource_type: role
for role in Role.list(request)}
@classmethod
@handle_errors(_("Unable to retrieve overcloud role"))
def get_by_resource_type(cls, request, resource_type):
roles = cls._roles_by_resource_type(request)
try:
return roles[resource_type]
except KeyError:
return None
@property
def provider_resource_type(self):
return "Tuskar::{0}-{1}".format(self.name, self.version)
@property
def parameter_prefix(self):
return "{0}-{1}::".format(self.name, self.version)
@property
def node_count_parameter_name(self):
return self.parameter_prefix + 'count'
@property
def image_parameter_name(self):
return self.parameter_prefix + 'Image'
@property
def flavor_parameter_name(self):
return self.parameter_prefix + 'Flavor'
def image(self, plan):
image_name = plan.parameter_value(self.image_parameter_name)
if image_name:
try:
return glance.image_list_detailed(
self._request, filters={'name': image_name})[0][0]
except (glance_exceptions.HTTPNotFound, IndexError):
LOG.error("Couldn't obtain image with name %s" % image_name)
return None
def flavor(self, plan):
flavor_name = plan.parameter_value(
self.flavor_parameter_name)
if flavor_name:
return flavor.Flavor.get_by_name(self._request, flavor_name)
def parameter_list(self, plan):
return [p for p in plan.parameter_list() if self == p.role]
def is_valid_for_deployment(self, plan):
node_count = plan.get_role_node_count(self)
pending_required_params = list(Parameter.pending_parameters(
Parameter.required_parameters(self.parameter_list(plan))))
return not (
self.image(plan) is None or
(node_count and self.flavor(plan) is None) or
pending_required_params
)
@property
def id(self):
return self.uuid
class Parameter(base.APIDictWrapper):
_attrs = ['name', 'value', 'default', 'description', 'hidden', 'label',
'parameter_type', 'constraints']
def __init__(self, apidict, plan=None):
super(Parameter, self).__init__(apidict)
self._plan = plan
@property
def stripped_name(self):
return strip_prefix(self.name)
@property
def plan(self):
return self._plan
@property
def role(self):
if self.plan:
for role in self.plan.role_list:
if self.name.startswith(role.parameter_prefix):
return role
def is_required(self):
"""Boolean: True if parameter is required, False otherwise."""
return self.default is None
def get_constraint_by_type(self, constraint_type):
"""Returns parameter constraint by it's type.
For available constraint types see HOT Spec:
http://docs.openstack.org/developer/heat/template_guide/hot_spec.html
"""
constraints_of_type = [c for c in self.constraints
if c['constraint_type'] == constraint_type]
if constraints_of_type:
return constraints_of_type[0]
else:
return None
@staticmethod
def required_parameters(parameters):
"""Yields parameters which are required."""
for parameter in parameters:
if parameter.is_required():
yield parameter
@staticmethod
def pending_parameters(parameters):
"""Yields parameters which don't have value set."""
for parameter in parameters:
if not parameter.value:
yield parameter
@staticmethod
def global_parameters(parameters):
"""Yields parameters with name without role prefix."""
for parameter in parameters:
if '::' not in parameter.name:
yield parameter

View File

@ -1,63 +0,0 @@
# 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.
# Copyright (c) Django Software Foundation and individual contributors.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# 3. Neither the name of Django nor the names of its contributors may be
# used to endorse or promote products derived from this software without
# specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# We would be using django.utils.functional.cached_property, except it
# breaks when used with mox in our tests, because of
# https://code.djangoproject.com/ticket/19872
#
# So we have a copy of it here, with the bug fixed.
# FIXME: Use django's version when the bug is fixed there.
class cached_property(object):
"""Cached property decorator.
Decorator that creates converts a method with a single self argument
into a property cached on the instance.
"""
def __init__(self, func):
self.func = func
def __get__(self, instance, type):
if instance is None:
return self
res = instance.__dict__[self.func.__name__] = self.func(instance)
return res

View File

@ -1,22 +0,0 @@
#
# 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 ironicclient import exceptions as ironic_exceptions
from openstack_dashboard import exceptions
from tuskarclient.openstack.common.apiclient import exceptions as tuskarclient
NOT_FOUND = exceptions.NOT_FOUND
RECOVERABLE = exceptions.RECOVERABLE + (
ironic_exceptions.Conflict, tuskarclient.ClientException,
)
UNAUTHORIZED = exceptions.UNAUTHORIZED

View File

@ -1,174 +0,0 @@
#
# 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 re
from django import forms
from django.utils import html
from django.utils.translation import ugettext_lazy as _
import netaddr
SEPARATOR_RE = re.compile('[\s,;|]+', re.UNICODE)
def label_with_tooltip(label, tooltip=None, title=None):
if not tooltip:
return label
return html.format_html(
u'{0}&nbsp;<a class="help-icon fa fa-question-circle" '
u'data-content="{1}" tabindex="0" href="#" '
u'data-title="{2}"></a>',
html.escape(label),
html.escape(tooltip),
html.escape(title or label)
)
def fieldset(form, *args, **kwargs):
"""A helper function for grouping fields based on their names."""
prefix = kwargs.pop('prefix', '.*')
names = args or form.fields.keys()
for name in names:
if prefix is not None and re.match(prefix, name):
yield forms.forms.BoundField(form, form.fields[name], name)
class MACDialect(netaddr.mac_eui48):
"""For validating MAC addresses. Same validation as Nova uses."""
word_fmt = '%.02x'
word_sep = ':'
def normalize_MAC(value):
try:
return str(netaddr.EUI(
value.strip(), version=48, dialect=MACDialect)).upper()
except (netaddr.AddrFormatError, TypeError):
raise ValueError('Invalid MAC address')
class NumberInput(forms.widgets.TextInput):
"""A form input for numbers."""
input_type = 'number'
class NumberPickerInput(forms.widgets.TextInput):
"""A form input that is rendered as a big number picker."""
def __init__(self, attrs=None):
default_attrs = {'class': 'number-picker'}
if attrs:
default_attrs.update(attrs)
super(NumberPickerInput, self).__init__(default_attrs)
class MACField(forms.fields.Field):
"""A form field for entering a single MAC address."""
def clean(self, value):
value = super(MACField, self).clean(value)
try:
return normalize_MAC(value)
except ValueError:
raise forms.ValidationError(_(u'Enter a valid MAC address.'))
class MultiMACField(forms.fields.Field):
"""A form field for entering multiple MAC addresses.
The individual MAC addresses can be separated by any whitespace,
commas, semicolons or pipe characters.
Gives a string of normalized MAC addresses separated by spaces.
"""
def clean(self, value):
value = super(MultiMACField, self).clean(value)
macs = []
for mac in SEPARATOR_RE.split(value):
if mac:
try:
normalized_mac = normalize_MAC(mac)
except ValueError:
raise forms.ValidationError(
_(u'%r is not a valid MAC address.') % mac)
else:
macs.append(normalized_mac)
return ' '.join(sorted(set(macs)))
class NetworkField(forms.fields.Field):
"""A form field for entering a network specification with a mask."""
def clean(self, value):
value = super(NetworkField, self).clean(value)
try:
return str(netaddr.IPNetwork(value, version=4))
except netaddr.AddrFormatError:
raise forms.ValidationError(_("Enter valid IPv4 network address."))
class SelfHandlingFormset(forms.formsets.BaseFormSet):
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
super(SelfHandlingFormset, self).__init__(*args, **kwargs)
def handle(self, request, data):
success = True
for form in self:
form_success = form.handle(request, form.cleaned_data)
if not form_success:
success = False
else:
pass
return success
class LabelWidget(forms.Widget):
"""A widget for displaying information.
This is a custom widget to show context information just as text,
as readonly inputs are confusing.
Note that the field also must be required=False, as no input
is rendered, and it must be ignored in the handle() method.
"""
def render(self, name, value, attrs=None):
if value:
return html.escape(value)
return ''
class StaticTextWidget(forms.Widget):
def render(self, name, value, attrs=None):
if value is None:
value = ''
return html.format_html('<p class="form-control-static">{0}</p>',
value)
class StaticTextPasswordWidget(forms.Widget):
def render(self, name, value, attrs=None):
if value is None or value == '':
return html.format_html(u'<p class="form-control-static"></p>')
else:
return html.format_html(
u'<p class="form-control-static">'
u'<a href="" class="btn btn-default btn-xs password-button"'
u' data-content="{0}"><i class="fa fa-eye"></i>&nbsp;{1}</a>'
u'</p>', value, _(u"Reveal")
)

View File

@ -1,71 +0,0 @@
# -*- coding: utf8 -*-
#
# 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 functools
import inspect
import horizon.exceptions
def handle_errors(error_message, error_default=None, request_arg=None):
"""A decorator for adding default error handling to API calls.
It wraps the original method in a try-except block, with horizon's
error handling added.
Note: it should only be used on functions or methods that take request as
their argument (it has to be named "request", or ``request_arg`` has to be
provided, indicating which argument is the request).
The decorated method accepts a number of additional parameters:
:param _error_handle: whether to handle the errors in this call
:param _error_message: override the error message
:param _error_default: override the default value returned on error
:param _error_redirect: specify a redirect url for errors
:param _error_ignore: ignore known errors
"""
def decorator(func):
# XXX This is an ugly hack for finding the 'request' argument.
if request_arg is None:
for _request_arg, name in enumerate(inspect.getargspec(func).args):
if name == 'request':
break
else:
raise RuntimeError(
"The handle_errors decorator requires 'request' as "
"an argument of the function or method being decorated")
else:
_request_arg = request_arg
@functools.wraps(func)
def wrapper(*args, **kwargs):
_error_handle = kwargs.pop('_error_handle', True)
_error_message = kwargs.pop('_error_message', error_message)
_error_default = kwargs.pop('_error_default', error_default)
_error_redirect = kwargs.pop('_error_redirect', None)
_error_ignore = kwargs.pop('_error_ignore', False)
if not _error_handle:
return func(*args, **kwargs)
try:
return func(*args, **kwargs)
except Exception:
request = args[_request_arg]
horizon.exceptions.handle(request, _error_message,
ignore=_error_ignore,
redirect=_error_redirect)
return _error_default
wrapper.wrapped = func
return wrapper
return decorator

View File

@ -1,34 +0,0 @@
#
# 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 Infrastructure(horizon.Dashboard):
name = _("Infrastructure")
slug = "infrastructure"
panels = (
'overview',
'parameters',
'roles',
'nodes',
'flavors',
'images',
'history',
)
default_panel = 'overview'
permissions = ('openstack.roles.admin',)
horizon.register(Infrastructure)

View File

@ -1,33 +0,0 @@
# -*- coding: utf8 -*-
#
# 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 tuskar_ui.infrastructure import dashboard
from tuskar_ui.infrastructure.flavors import utils
class Flavors(horizon.Panel):
name = _("Flavors")
slug = "flavors"
def can_access(self, context):
if not utils.matching_deployment_mode():
return False
return super(Flavors, self).can_access(context)
dashboard.Infrastructure.register(Flavors)

View File

@ -1,157 +0,0 @@
# -*- coding: utf8 -*-
#
# 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 django.shortcuts
from django.utils.translation import ugettext_lazy as _
import horizon.exceptions
import horizon.messages
import horizon.tables
from openstack_dashboard.dashboards.admin.flavors import (
tables as flavor_tables)
from tuskar_ui import api
from tuskar_ui.infrastructure.flavors import utils
class CreateFlavor(flavor_tables.CreateFlavor):
verbose_name = _(u"New Flavor")
url = "horizon:infrastructure:flavors:create"
class CreateSuggestedFlavor(horizon.tables.Action):
name = 'create'
verbose_name = _(u"Create")
verbose_name_plural = _(u"Create Suggested Flavors")
method = 'POST'
icon = 'plus'
def create_flavor(self, request, node_id):
node = api.node.Node.get(request, node_id)
suggestion = utils.FlavorSuggestion.from_node(node)
return suggestion.create_flavor(request)
def handle(self, data_table, request, node_ids):
for node_id in node_ids:
try:
self.create_flavor(request, node_id)
except Exception:
horizon.exceptions.handle(
request,
_(u"Unable to create flavor for node %r") % node_id,
)
return django.shortcuts.redirect(request.get_full_path())
class EditAndCreateSuggestedFlavor(CreateFlavor):
name = 'edit_and_create'
verbose_name = _(u"Edit before creating")
icon = 'pencil'
class DeleteFlavor(flavor_tables.DeleteFlavor):
def __init__(self, **kwargs):
super(DeleteFlavor, self).__init__(**kwargs)
# NOTE(dtantsur): setting class attributes doesn't work
# probably due to metaclass magic in actions
self.data_type_singular = _("Flavor")
self.data_type_plural = _("Flavors")
def allowed(self, request, datum=None):
"""Check that action is allowed on flavor
This is overridden method from horizon.tables.BaseAction.
:param datum: flavor we're operating on
:type datum: tuskar_ui.api.Flavor
"""
if datum is not None:
deployed_flavors = api.flavor.Flavor.list_deployed_ids(
request, _error_default=None)
if deployed_flavors is None or datum.id in deployed_flavors:
return False
return super(DeleteFlavor, self).allowed(request, datum)
class FlavorsTable(horizon.tables.DataTable):
name = horizon.tables.Column('name',
link="horizon:infrastructure:flavors:details")
arch = horizon.tables.Column('cpu_arch', verbose_name=_('Architecture'))
vcpus = horizon.tables.Column('vcpus', verbose_name=_('CPUs'))
ram = horizon.tables.Column(flavor_tables.get_size,
verbose_name=_('Memory'),
attrs={'data-type': 'size'})
disk = horizon.tables.Column(flavor_tables.get_disk_size,
verbose_name=_('Disk'),
attrs={'data-type': 'size'})
class Meta(object):
name = "flavors"
verbose_name = _("Available")
table_actions = (
DeleteFlavor,
flavor_tables.FlavorFilterAction,
)
row_actions = (
DeleteFlavor,
)
template = "horizon/common/_enhanced_data_table.html"
class FlavorRolesTable(horizon.tables.DataTable):
name = horizon.tables.Column('name', verbose_name=_('Role Name'))
def __init__(self, request, *args, **kwargs):
# TODO(dtantsur): support multiple overclouds
plan = api.tuskar.Plan.get_the_plan(request)
stack = api.heat.Stack.get_by_plan(request, plan)
if stack is None:
count = lambda role: _('Not Deployed')
else:
count = stack.resources_count
self._columns['count'] = horizon.tables.Column(
count,
verbose_name=_("Instances Count")
)
super(FlavorRolesTable, self).__init__(request, *args, **kwargs)
class Meta(object):
name = "flavor_roles"
verbose_name = _("Overcloud Roles")
table_actions = ()
row_actions = ()
hidden_title = False
template = "horizon/common/_enhanced_data_table.html"
class FlavorSuggestionsTable(horizon.tables.DataTable):
name = horizon.tables.Column('name',)
arch = horizon.tables.Column('cpu_arch', verbose_name=_('Architecture'))
vcpus = horizon.tables.Column('vcpus', verbose_name=_('CPUs'))
ram = horizon.tables.Column(flavor_tables.get_size,
verbose_name=_('Memory'),
attrs={'data-type': 'size'})
disk = horizon.tables.Column(flavor_tables.get_disk_size,
verbose_name=_('Disk'),
attrs={'data-type': 'size'})
class Meta(object):
name = "suggested_flavors"
verbose_name = _("Suggested")
row_actions = (
CreateSuggestedFlavor,
EditAndCreateSuggestedFlavor,
)
template = "horizon/common/_enhanced_data_table.html"

View File

@ -1,11 +0,0 @@
{% extends 'infrastructure/base.html' %}
{% load i18n %}
{% block title %}{% trans "Create Flavor" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Create Flavor") %}
{% endblock page_header %}
{% block main %}
{% include 'horizon/common/_workflow.html' %}
{% endblock %}

View File

@ -1,31 +0,0 @@
{% extends 'infrastructure/base.html' %}
{% load i18n %}
{% block title %}{% trans 'Flavor: ' %}{{ flavor.name }}{% endblock %}
{% block page_header %}
{% include 'horizon/common/_page_header.html' with title=_('Flavor: ')|add:flavor.name %}
{% endblock page_header %}
{% block main %}
<div class="row">
<div class="col-md-4">
<h4>{% trans "Hardware Info" %}</h4>
<dl class="dl-horizontal dl-horizontal-left">
<dt>{% trans "Architecture" %}</dt>
<dd>{{ flavor.cpu_arch|default:"&mdash;" }}</dd>
<dt>{% trans "CPUs" %}</dt>
<dd>{{ flavor.vcpus|default:"&mdash;" }}</dd>
<dt>{% trans "Memory" %}</dt>
<dd>{{ flavor.ram_bytes|filesizeformat|default:"&mdash;" }}</dd>
<dt>{% trans "Disk" %}</dt>
<dd>{{ flavor.disk_bytes|filesizeformat|default:"&mdash;" }}</dd>
</dl>
</div>
</div>
<div class="row">
<div class="col-xs-12">
{{ table.render }}
</div>
</div>
{% endblock %}

View File

@ -1,27 +0,0 @@
{% extends 'infrastructure/base.html' %}
{% load i18n %}
{% load url from future %}
{% block title %}{% trans 'Flavors' %}{% endblock %}
{% block page_header %}
{% include 'horizon/common/_items_count_domain_page_header.html' with title=_('Flavors') items_count=flavors_count %}
{% endblock page_header %}
{% block main %}
{% if suggested_flavors_count %}
<div class="panel panel-default">
<div class="panel-heading" data-toggle="collapse" href="#suggestedFlavors">
<a href="#suggestedFlavors">{{ suggested_flavors_count }}&times; Suggested Flavor</a>
</div>
<div class="panel-collapse collapse" id="suggestedFlavors">
<div class="panel-body">
{{ suggested_flavors_table.render }}
</div>
</div>
</div>
{% endif %}
<div id="flavors">
{{ flavors_table.render }}
</div>
{% endblock %}

View File

@ -1,265 +0,0 @@
# -*- coding: utf8 -*-
#
# 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 contextlib
from django.core import urlresolvers
from mock import patch, call # noqa
from novaclient import exceptions as nova_exceptions
from novaclient.v2 import servers
from openstack_dashboard.test.test_data import utils
from tuskar_ui import api
from tuskar_ui.infrastructure.flavors import utils as flavors_utils
from tuskar_ui.test import helpers as test
from tuskar_ui.test.test_data import flavor_data
from tuskar_ui.test.test_data import heat_data
from tuskar_ui.test.test_data import tuskar_data
TEST_DATA = utils.TestDataContainer()
flavor_data.data(TEST_DATA)
heat_data.data(TEST_DATA)
tuskar_data.data(TEST_DATA)
INDEX_URL = urlresolvers.reverse(
'horizon:infrastructure:flavors:index')
CREATE_URL = urlresolvers.reverse(
'horizon:infrastructure:flavors:create')
DETAILS_VIEW = 'horizon:infrastructure:flavors:details'
@contextlib.contextmanager
def _prepare_create():
flavor = TEST_DATA.novaclient_flavors.first()
all_flavors = TEST_DATA.novaclient_flavors.list()
data = {'name': 'foobar',
'vcpus': 3,
'memory_mb': 1024,
'disk_gb': 40,
'arch': 'amd64'}
with contextlib.nested(
patch('tuskar_ui.api.flavor.Flavor.create',
return_value=flavor),
# Inherited code calls this directly
patch('openstack_dashboard.api.nova.flavor_list',
return_value=all_flavors),
) as mocks:
yield mocks[0], data
def _raise_nova_client_exception(*args, **kwargs):
raise nova_exceptions.ClientException("Boom!")
class FlavorsTest(test.BaseAdminViewTests):
def test_index(self):
plans = [api.tuskar.Plan(plan, self.request)
for plan in TEST_DATA.tuskarclient_plans.list()]
roles = [api.tuskar.Role(role)
for role in self.tuskarclient_roles.list()]
with contextlib.nested(
patch('tuskar_ui.api.node.ironicclient'),
patch('tuskar_ui.api.tuskar.Plan.list',
return_value=plans),
patch('tuskar_ui.api.tuskar.Role.list',
return_value=roles),
patch('openstack_dashboard.api.nova.flavor_list',
return_value=TEST_DATA.novaclient_flavors.list()),
patch('openstack_dashboard.api.nova.server_list',
return_value=([], False)),
) as (ironic_mock, plans_mock, roles_mock, flavors_mock, servers_mock):
res = self.client.get(INDEX_URL)
self.assertEqual(plans_mock.call_count, 1)
self.assertEqual(roles_mock.call_count, 4)
self.assertEqual(flavors_mock.call_count, 3)
self.assertEqual(servers_mock.call_count, 2)
self.assertTemplateUsed(res, 'infrastructure/flavors/index.html')
def test_index_recoverable_failure(self):
with patch(
'openstack_dashboard.api.nova.flavor_list',
side_effect=_raise_nova_client_exception
) as flavor_list, patch('tuskar_ui.api.node.ironicclient'):
res = self.client.get(INDEX_URL)
self.assertEqual(flavor_list.call_count, 2)
self.assertEqual(
[(m.message, m.tags) for m in res.context['messages']],
[
(u'Unable to retrieve flavor list.', u'error'),
(u'Unable to retrieve nodes', u'error'),
],
)
self.assertMessageCount(response=res, error=2, warning=0)
def test_create_get(self):
res = self.client.get(CREATE_URL)
self.assertTemplateUsed(res, 'infrastructure/flavors/create.html')
def test_create_post_ok(self):
with _prepare_create() as (create_mock, data):
res = self.client.post(CREATE_URL, data)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
request = create_mock.call_args_list[0][0][0]
self.assertListEqual(create_mock.call_args_list, [
call(request, name=u'foobar', memory=1024, vcpus=3, disk=40,
cpu_arch='amd64')
])
def test_create_post_name_exists(self):
flavor = TEST_DATA.novaclient_flavors.first()
with _prepare_create() as (create_mock, data):
data['name'] = flavor.name
res = self.client.post(CREATE_URL, data)
self.assertFormErrors(res)
def test_delete_ok(self):
flavors = TEST_DATA.novaclient_flavors.list()
data = {'action': 'flavors__delete',
'object_ids': [flavors[0].id, flavors[1].id]}
with contextlib.nested(
patch('openstack_dashboard.api.nova.flavor_delete'),
patch('openstack_dashboard.api.nova.server_list',
return_value=([], False)),
patch('tuskar_ui.api.tuskar.Role.list',
return_value=[]),
patch('tuskar_ui.api.tuskar.Plan.list',
return_value=[]),
patch('openstack_dashboard.api.nova.flavor_list',
return_value=TEST_DATA.novaclient_flavors.list())
):
res = self.client.post(INDEX_URL, data)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
def test_delete_deployed_on_servers(self):
flavors = TEST_DATA.novaclient_flavors.list()
server = servers.Server(
servers.ServerManager(None),
{'id': 'aa',
'name': 'Compute',
'image': {'id': 1},
'status': 'ACTIVE',
'flavor': {'id': flavors[0].id}}
)
data = {'action': 'flavors__delete',
'object_ids': [flavors[0].id, flavors[1].id]}
with contextlib.nested(
patch('openstack_dashboard.api.nova.flavor_delete'),
patch('openstack_dashboard.api.nova.server_list',
return_value=([server], False)),
patch('tuskar_ui.api.tuskar.Role.list',
return_value=[]),
patch('tuskar_ui.api.tuskar.Plan.list',
return_value=[]),
patch('openstack_dashboard.api.nova.flavor_list',
return_value=TEST_DATA.novaclient_flavors.list()),
patch('tuskar_ui.api.node.Node.list',
return_value=[])
):
res = self.client.post(INDEX_URL, data)
self.assertMessageCount(error=1, warning=0)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
def test_details_no_overcloud(self):
flavor = api.flavor.Flavor(TEST_DATA.novaclient_flavors.first())
plan = api.tuskar.Plan(TEST_DATA.tuskarclient_plans.first())
roles = [api.tuskar.Role(role)
for role in self.tuskarclient_roles.list()]
with contextlib.nested(
patch('tuskar_ui.api.flavor.Flavor.get',
return_value=flavor),
patch('tuskar_ui.api.tuskar.Plan.get_the_plan',
return_value=plan),
patch('tuskar_ui.api.tuskar.Role.list', return_value=roles),
patch('tuskar_ui.api.tuskar.Role.flavor', return_value=flavor),
) as (get_mock, plan_mock, roles_mock, role_flavor_mock):
res = self.client.get(urlresolvers.reverse(DETAILS_VIEW,
args=(flavor.id,)))
self.assertEqual(get_mock.call_count, 1)
self.assertEqual(plan_mock.call_count, 2)
self.assertEqual(roles_mock.call_count, 1)
self.assertEqual(role_flavor_mock.call_count, 8)
self.assertTemplateUsed(res, 'infrastructure/flavors/details.html')
def test_details(self):
flavor = api.flavor.Flavor(TEST_DATA.novaclient_flavors.first())
plan = api.tuskar.Plan(TEST_DATA.tuskarclient_plans.first())
roles = [api.tuskar.Role(role)
for role in self.tuskarclient_roles.list()]
stack = api.heat.Stack(TEST_DATA.heatclient_stacks.first())
with contextlib.nested(
patch('tuskar_ui.api.flavor.Flavor.get',
return_value=flavor),
patch('tuskar_ui.api.tuskar.Plan.get_the_plan',
return_value=plan),
patch('tuskar_ui.api.tuskar.Role.list', return_value=roles),
patch('tuskar_ui.api.tuskar.Role.flavor', return_value=flavor),
patch('tuskar_ui.api.heat.Stack.get_by_plan',
return_value=stack),
# __name__ is required for horizon.tables
patch('tuskar_ui.api.heat.Stack.resources_count',
return_value=42, __name__='')
) as (flavor_mock, plan_mock, roles_mock, role_flavor_mock,
stack_mock, count_mock):
res = self.client.get(urlresolvers.reverse(DETAILS_VIEW,
args=(flavor.id,)))
self.assertEqual(flavor_mock.call_count, 1)
self.assertEqual(plan_mock.call_count, 2)
self.assertEqual(roles_mock.call_count, 1)
self.assertEqual(role_flavor_mock.call_count, 8)
self.assertEqual(stack_mock.call_count, 1)
self.assertEqual(count_mock.call_count, 4)
self.assertTemplateUsed(res, 'infrastructure/flavors/details.html')
class FlavorsUtilsTest(test.TestCase):
def test_get_unmached_suggestions(self):
flavors = [api.flavor.Flavor(flavor)
for flavor in TEST_DATA.novaclient_flavors.list()]
nodes = [api.node.Node(api.node.Node(node))
for node in self.ironicclient_nodes.list()]
with (
patch('tuskar_ui.api.flavor.Flavor.list', return_value=flavors)
), (
patch('tuskar_ui.api.node.Node.list', return_value=nodes)
):
ret = flavors_utils.get_flavor_suggestions(None)
FS = flavors_utils.FlavorSuggestion
self.assertEqual(ret, set([
FS(vcpus=8, ram_bytes=4294967296, disk_bytes=10737418240,
cpu_arch='x86_64', node_id='aa-11'),
FS(vcpus=16, ram_bytes=4294967296, disk_bytes=107374182400,
cpu_arch='x86_64', node_id='bb-22'),
FS(vcpus=32, ram_bytes=8589934592, disk_bytes=1073741824,
cpu_arch='x86_64', node_id='cc-33'),
FS(vcpus=8, ram_bytes=4294967296, disk_bytes=10737418240,
cpu_arch='x86_64', node_id='cc-44'),
FS(vcpus=8, ram_bytes=4294967296, disk_bytes=10737418240,
cpu_arch='x86_64', node_id='dd-55'),
FS(vcpus=8, ram_bytes=4294967296, disk_bytes=10737418240,
cpu_arch='x86_64', node_id='ff-66'),
FS(vcpus=8, ram_bytes=4294967296, disk_bytes=10737418240,
cpu_arch='x86_64', node_id='gg-77'),
FS(vcpus=8, ram_bytes=4294967296, disk_bytes=10737418240,
cpu_arch='x86_64', node_id='hh-88'),
FS(vcpus=16, ram_bytes=8589934592, disk_bytes=1073741824000,
cpu_arch='x86_64', node_id='ii-99'),
]))

View File

@ -1,28 +0,0 @@
# -*- coding: utf8 -*-
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.conf import urls
from tuskar_ui.infrastructure.flavors import views
urlpatterns = urls.patterns(
'tuskar_ui.infrastructure.flavors.views',
urls.url(r'^$', views.IndexView.as_view(), name='index'),
urls.url(r'^create/(?P<suggestion_id>[^/]+)$', views.CreateView.as_view(),
name='create'),
urls.url(r'^create/$', views.CreateView.as_view(), name='create'),
urls.url(r'^(?P<flavor_id>[^/]+)/$', views.DetailView.as_view(),
name='details'),
)

View File

@ -1,121 +0,0 @@
# -*- coding: utf8 -*-
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.conf import settings
from tuskar_ui import api
from tuskar_ui.utils import utils
def matching_deployment_mode():
deployment_mode = getattr(settings, 'DEPLOYMENT_MODE', 'scale')
return deployment_mode.lower() == 'scale'
def _get_unmatched_suggestions(request):
unmatched_suggestions = []
flavor_suggestions = [FlavorSuggestion.from_flavor(flavor)
for flavor in api.flavor.Flavor.list(request)]
for node in api.node.Node.list(request):
node_suggestion = FlavorSuggestion.from_node(node)
for flavor_suggestion in flavor_suggestions:
if flavor_suggestion == node_suggestion:
break
else:
unmatched_suggestions.append(node_suggestion)
return unmatched_suggestions
def get_flavor_suggestions(request):
return set(_get_unmatched_suggestions(request))
class FlavorSuggestion(object):
"""Describe node parameters in a way that is easy to compare."""
def __init__(self, vcpus=None, ram=None, disk=None, cpu_arch=None,
ram_bytes=None, disk_bytes=None, node_id=None):
self.vcpus = vcpus
self.ram_bytes = ram_bytes or ram * 1024 * 1024 or 0
self.disk_bytes = disk_bytes or (disk or 0) * 1024 * 1024 * 1024
self.cpu_arch = cpu_arch
self.id = node_id
@classmethod
def from_node(cls, node):
return cls(
node_id=node.uuid,
vcpus=utils.safe_int_cast(node.cpus),
ram=utils.safe_int_cast(node.memory_mb),
disk=utils.safe_int_cast(node.local_gb),
cpu_arch=node.cpu_arch
)
@classmethod
def from_flavor(cls, flavor):
return cls(
vcpus=flavor.vcpus,
ram_bytes=flavor.ram_bytes,
disk_bytes=flavor.disk_bytes,
cpu_arch=flavor.cpu_arch
)
@property
def name(self):
return 'Flavor-%scpu-%s-%sMB-%sGB' % (
self.vcpus or '0',
self.cpu_arch or '',
self.ram or '0',
self.disk or '0',
)
@property
def ram(self):
return self.ram_bytes / 1024 / 1024
@property
def disk(self):
return self.disk_bytes / 1024 / 1024 / 1024
def __hash__(self):
return self.name.__hash__()
def __eq__(self, other):
return self.name == other.name
def __ne__(self, other):
return not self == other
def __repr__(self):
return (
'%s(vcpus=%r, ram_bytes=%r, disk_bytes=%r, '
'cpu_arch=%r, node_id=%r)' % (
self.__class__.__name__,
self.vcpus,
self.ram_bytes,
self.disk_bytes,
self.cpu_arch,
self.id,
)
)
def create_flavor(self, request):
return api.flavor.Flavor.create(
request,
name=self.name,
memory=self.ram,
vcpus=self.vcpus,
disk=self.disk,
cpu_arch=self.cpu_arch,
)

View File

@ -1,103 +0,0 @@
# -*- coding: utf8 -*-
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.core.urlresolvers import reverse
from django.core.urlresolvers import reverse_lazy
from django.utils.translation import ugettext_lazy as _
import horizon.exceptions
import horizon.tables
import horizon.tabs
from horizon.utils import memoized
import horizon.workflows
from tuskar_ui import api
from tuskar_ui.infrastructure.flavors import tables
from tuskar_ui.infrastructure.flavors import utils
from tuskar_ui.infrastructure.flavors import workflows
class IndexView(horizon.tables.MultiTableView):
table_classes = (tables.FlavorsTable, tables.FlavorSuggestionsTable)
template_name = 'infrastructure/flavors/index.html'
def get_context_data(self, **kwargs):
context = super(IndexView, self).get_context_data(**kwargs)
create_action = {
'name': _("New Flavor"),
'url': reverse('horizon:infrastructure:flavors:create'),
'icon': 'fa-plus',
'ajax_modal': True,
}
context['header_actions'] = [create_action]
context['flavors_count'] = self.get_flavors_count()
context['suggested_flavors_count'] = self.get_suggested_flavors_count()
return context
@memoized.memoized_method
def get_flavors_data(self):
flavors = api.flavor.Flavor.list(self.request)
flavors.sort(key=lambda np: (np.vcpus, np.ram, np.disk))
return flavors
@memoized.memoized_method
def get_suggested_flavors_data(self):
return list(utils.get_flavor_suggestions(self.request))
def get_flavors_count(self):
return len(self.get_flavors_data())
def get_suggested_flavors_count(self):
return len(self.get_suggested_flavors_data())
class CreateView(horizon.workflows.WorkflowView):
workflow_class = workflows.CreateFlavor
template_name = 'infrastructure/flavors/create.html'
def get_initial(self):
suggestion_id = self.kwargs.get('suggestion_id')
if not suggestion_id:
return super(CreateView, self).get_initial()
node = api.node.Node.get(self.request, suggestion_id)
suggestion = utils.FlavorSuggestion.from_node(node)
return {
'name': suggestion.name,
'vcpus': suggestion.vcpus,
'memory_mb': suggestion.ram,
'disk_gb': suggestion.disk,
'arch': suggestion.cpu_arch,
}
class DetailView(horizon.tables.DataTableView):
table_class = tables.FlavorRolesTable
template_name = 'infrastructure/flavors/details.html'
error_redirect = reverse_lazy('horizon:infrastructure:flavors:index')
def get_context_data(self, **kwargs):
context = super(DetailView, self).get_context_data(**kwargs)
context['flavor'] = api.flavor.Flavor.get(
self.request,
kwargs.get('flavor_id'),
_error_redirect=self.error_redirect
)
return context
def get_data(self):
flavor_id = self.kwargs.get('flavor_id')
plan = api.tuskar.Plan.get_the_plan(self.request)
return [role for role in api.tuskar.Role.list(self.request)
if role.flavor(plan)
and role.flavor(plan).id == flavor_id]

View File

@ -1,79 +0,0 @@
# -*- coding: utf8 -*-
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.forms import fields
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import workflows
from openstack_dashboard.dashboards.admin.flavors import (
workflows as flavor_workflows)
from tuskar_ui import api
class CreateFlavorAction(flavor_workflows.CreateFlavorInfoAction):
arch = fields.ChoiceField(choices=(('i386', 'i386'), ('amd64', 'amd64'),
('x86_64', 'x86_64')),
label=_("Architecture"))
def __init__(self, *args, **kwrds):
super(CreateFlavorAction, self).__init__(*args, **kwrds)
# Delete what is not applicable to hardware
del self.fields['eph_gb']
del self.fields['swap_mb']
# Alter user-visible strings
self.fields['vcpus'].label = _("CPUs")
self.fields['disk_gb'].label = _("Disk GB")
# No idea why Horizon exposes this database detail
del self.fields['flavor_id']
class Meta(object):
name = _("Flavor")
help_text = _("Flavors define the sizes for RAM, disk, number of "
"cores, and other resources. Flavors should be "
"associated with roles when planning a deployment.")
class CreateFlavorStep(workflows.Step):
action_class = CreateFlavorAction
contributes = ("name",
"vcpus",
"memory_mb",
"disk_gb",
"arch")
class CreateFlavor(flavor_workflows.CreateFlavor):
slug = "create_flavor"
name = _("Create Flavor")
finalize_button_name = _("Create Flavor")
success_message = _('Created new flavor "%s".')
failure_message = _('Unable to create flavor "%s".')
success_url = "horizon:infrastructure:flavors:index"
default_steps = (CreateFlavorStep,)
def handle(self, request, data):
try:
self.object = api.flavor.Flavor.create(
request,
name=data['name'],
memory=data['memory_mb'],
vcpus=data['vcpus'],
disk=data['disk_gb'],
cpu_arch=data['arch']
)
except Exception:
exceptions.handle(request, _("Unable to create flavor"))
return False
return True

View File

@ -1,26 +0,0 @@
# -*- coding: utf8 -*-
#
# 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 tuskar_ui.infrastructure import dashboard
class History(horizon.Panel):
name = _("Deployment Log")
slug = "history"
dashboard.Infrastructure.register(History)

View File

@ -1,37 +0,0 @@
# -*- coding: utf8 -*-
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.utils.translation import ugettext_lazy as _
from horizon import tables
class HistoryTable(tables.DataTable):
timestamp = tables.Column('event_time',
verbose_name=_("Timestamp"),
attrs={'data-type': 'timestamp'})
resource_name = tables.Column('resource_name',
verbose_name=_("Resource Name"))
resource_status = tables.Column('resource_status',
verbose_name=_("Status"))
resource_status_reason = tables.Column('resource_status_reason',
verbose_name=_("Reason"))
class Meta(object):
name = "log"
verbose_name = _("Deployment Log")
multi_select = False
table_actions = ()
row_actions = ()
template = "horizon/common/_enhanced_data_table.html"

View File

@ -1,16 +0,0 @@
{% extends 'infrastructure/base.html' %}
{% load i18n %}
{% block title %}{% trans 'Deployment Log' %}{% endblock %}
{% block page_header %}
{% include 'horizon/common/_page_header.html' with title=_('Deployment Log') %}
{% endblock page_header %}
{% block main %}
<div class="row">
<div class="col-xs-12">
{{ table.render }}
</div>
</div>
{% endblock %}

View File

@ -1,53 +0,0 @@
# -*- coding: utf8 -*-
#
# 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 contextlib
from django.core import urlresolvers
from mock import patch, call # noqa
from openstack_dashboard.test.test_data import utils
from tuskar_ui import api
from tuskar_ui.test import helpers as test
from tuskar_ui.test.test_data import heat_data
from tuskar_ui.test.test_data import tuskar_data
TEST_DATA = utils.TestDataContainer()
heat_data.data(TEST_DATA)
tuskar_data.data(TEST_DATA)
INDEX_URL = urlresolvers.reverse(
'horizon:infrastructure:history:index')
class HistoryTest(test.BaseAdminViewTests):
def test_index(self):
plan = api.tuskar.Plan(
TEST_DATA.tuskarclient_plans.first())
stack = api.heat.Stack(
TEST_DATA.heatclient_stacks.first())
events = TEST_DATA.heatclient_events.list()
with contextlib.nested(
patch('tuskar_ui.api.tuskar.Plan.get_the_plan',
return_value=plan),
patch('tuskar_ui.api.heat.Stack.get_by_plan',
return_value=stack),
patch('tuskar_ui.api.heat.Stack.events',
return_value=events)
):
res = self.client.get(INDEX_URL)
self.assertTemplateUsed(res, 'infrastructure/history/index.html')

View File

@ -1,23 +0,0 @@
# -*- coding: utf8 -*-
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.conf import urls
from tuskar_ui.infrastructure.history import views
urlpatterns = urls.patterns(
'',
urls.url(r'^$', views.IndexView.as_view(), name='index'),
)

View File

@ -1,31 +0,0 @@
# -*- coding: utf8 -*-
#
# 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 import tables as horizon_tables
from tuskar_ui import api
from tuskar_ui.infrastructure.history import tables
class IndexView(horizon_tables.DataTableView):
table_class = tables.HistoryTable
template_name = "infrastructure/history/index.html"
def get_data(self):
plan = api.tuskar.Plan.get_the_plan(self.request)
if plan:
stack = api.heat.Stack.get_by_plan(self.request, plan)
if stack:
return stack.events
return []

View File

@ -1,17 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from openstack_dashboard.dashboards.project.images.images import forms
class UpdateImageForm(forms.UpdateImageForm):
pass

View File

@ -1,26 +0,0 @@
# -*- coding: utf8 -*-
#
# 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 tuskar_ui.infrastructure import dashboard
class Images(horizon.Panel):
name = _("Provisioning Images")
slug = "images"
dashboard.Infrastructure.register(Images)

View File

@ -1,74 +0,0 @@
# -*- coding: utf8 -*-
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.utils.translation import ugettext_lazy as _
from horizon import tables
from openstack_dashboard import api
from openstack_dashboard.dashboards.project.images.images import (
tables as project_tables)
class DeleteImage(project_tables.DeleteImage):
def allowed(self, request, image=None):
if image and image.protected:
return False
else:
return True
class CreateImage(project_tables.CreateImage):
url = "horizon:infrastructure:images:create"
class UpdateRow(tables.Row):
ajax = True
def get_data(self, request, image_id):
image = api.glance.image_get(request, image_id)
return image
class ImageFilterAction(tables.FilterAction):
filter_type = "server"
filter_choices = (('name', _("Image Name ="), True),
('status', _('Status ='), True),
('disk_format', _('Format ='), True),
('size_min', _('Min. Size (MB)'), True),
('size_max', _('Max. Size (MB)'), True))
class EditImage(project_tables.EditImage):
url = "horizon:infrastructure:images:update"
def allowed(self, request, image=None):
return True
class ImagesTable(tables.DataTable):
name = tables.Column('name',
verbose_name=_("Image Name"))
disk_format = tables.Column('disk_format',
verbose_name=_("Format"))
roles = tables.Column(lambda image:
', '.join([r.name for r in image.roles]),
verbose_name=_("Deployment Roles"))
class Meta(object):
name = "images"
row_class = UpdateRow
verbose_name = _("Provisioning Images")
table_actions = (CreateImage, DeleteImage, ImageFilterAction)
row_actions = (EditImage, DeleteImage)
template = "horizon/common/_enhanced_data_table.html"

View File

@ -1,15 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% load url from future %}
{% block form_id %}create_image_form{% endblock %}
{% block form_action %}{% url 'horizon:infrastructure:images:create' %}{% endblock %}
{% block form_attrs %}enctype="multipart/form-data"{% endblock %}
{% block modal_id %}create_image_modal{% endblock %}
{% block modal-header %}{% trans "Create Image" %}{% endblock %}
{% block modal-body-right %}
<h3>{% trans "Description" %}:</h3>
<p>{% trans "Modify different properties of an image." %}</p>
{% endblock %}

View File

@ -1,14 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% load url from future %}
{% block form_id %}update_image_form{% endblock %}
{% block form_action %}{% url 'horizon:infrastructure:images:update' image.id %}{% endblock %}
{% block modal_id %}update_image_modal{% endblock %}
{% block modal-header %}{% trans "Update Image" %}{% endblock %}
{% block modal-body-right %}
<h3>{% trans "Description" %}:</h3>
<p>{% trans "Modify different properties of an image." %}</p>
{% endblock %}

View File

@ -1,12 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Create Image" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Create Image") %}
{% endblock page_header %}
{% block main %}
{% include 'infrastructure/images/_create.html' %}
{% endblock %}

View File

@ -1,16 +0,0 @@
{% extends 'infrastructure/base.html' %}
{% load i18n %}
{% block title %}{% trans 'Provisioning Images' %}{% endblock %}
{% block page_header %}
{% include 'horizon/common/_items_count_domain_page_header.html' with title=_('Provisioning Images') %}
{% endblock page_header %}
{% block main %}
<div class="row">
<div class="col-xs-12">
{{ table.render }}
</div>
</div>
{% endblock %}

View File

@ -1,12 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Update Image" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Update Image") %}
{% endblock page_header %}
{% block main %}
{% include 'infrastructure/images/_update.html' %}
{% endblock %}

View File

@ -1,168 +0,0 @@
# -*- coding: utf8 -*-
#
# 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 contextlib
import mock
from mock import patch, call # noqa
from django.core import urlresolvers
from openstack_dashboard.dashboards.project.images.images import forms
from tuskar_ui import api
from tuskar_ui.test import helpers as test
INDEX_URL = urlresolvers.reverse('horizon:infrastructure:images:index')
CREATE_URL = 'horizon:infrastructure:images:create'
UPDATE_URL = 'horizon:infrastructure:images:update'
class ImagesTest(test.BaseAdminViewTests):
def test_index(self):
roles = [api.tuskar.Role(role) for role in
self.tuskarclient_roles.list()]
plans = [api.tuskar.Plan(plan) for plan in
self.tuskarclient_plans.list()]
with contextlib.nested(
patch('tuskar_ui.api.tuskar.Role.list',
return_value=roles),
patch('tuskar_ui.api.tuskar.Plan.list',
return_value=plans),
patch('openstack_dashboard.api.glance.image_list_detailed',
return_value=[self.glanceclient_images.list(),
False, False]),):
res = self.client.get(INDEX_URL)
self.assertTemplateUsed(res, 'infrastructure/images/index.html')
def test_create_get(self):
res = self.client.get(urlresolvers.reverse(CREATE_URL))
self.assertTemplateUsed(res, 'infrastructure/images/create.html')
def test_create_post(self):
image = self.images.list()[0]
data = {
'name': 'Fedora',
'description': 'Login with admin/admin',
'source_type': 'url',
'image_url': 'http://www.test.com/test.iso',
'disk_format': 'qcow2',
'architecture': 'x86-64',
'minimum_disk': 15,
'minimum_ram': 512,
'is_public': True,
'protected': False}
forms.IMAGE_FORMAT_CHOICES = [('qcow2', 'qcow2')]
with contextlib.nested(
patch('openstack_dashboard.api.glance.image_create',
return_value=image),) as (mocked_create,):
res = self.client.post(
urlresolvers.reverse(CREATE_URL), data)
self.assertNoFormErrors(res)
self.assertEqual(res.status_code, 302)
self.assertRedirectsNoFollow(res, INDEX_URL)
mocked_create.assert_called_once_with(
mock.ANY, name='Fedora', container_format='bare',
min_ram=512, disk_format='qcow2', protected=False,
is_public=True, min_disk=15,
location='http://www.test.com/test.iso',
properties={'description': 'Login with admin/admin',
'architecture': 'x86-64'})
def test_update_get(self):
image = self.images.list()[0]
with contextlib.nested(
patch('openstack_dashboard.api.glance.image_get',
return_value=image),) as (mocked_get,):
res = self.client.get(
urlresolvers.reverse(UPDATE_URL, args=(image.id,)))
mocked_get.assert_called_once_with(mock.ANY, image.id)
self.assertTemplateUsed(res, 'infrastructure/images/update.html')
def test_update_post(self):
image = self.images.list()[0]
data = {
'image_id': image.id,
'name': 'Fedora',
'description': 'Login with admin/admin',
'source_type': 'url',
'copy_from': 'http://test_url.com',
'disk_format': 'qcow2',
'architecture': 'x86-64',
'minimum_disk': 15,
'minimum_ram': 512,
'is_public': True,
'protected': False}
forms.IMAGE_FORMAT_CHOICES = [('qcow2', 'qcow2')]
with contextlib.nested(
patch('openstack_dashboard.api.glance.image_get',
return_value=image),
patch('openstack_dashboard.api.glance.image_update',
return_value=image),) as (mocked_get, mocked_update,):
res = self.client.post(
urlresolvers.reverse(UPDATE_URL, args=(image.id,)), data)
self.assertNoFormErrors(res)
self.assertEqual(res.status_code, 302)
self.assertRedirectsNoFollow(res, INDEX_URL)
mocked_get.assert_called_once_with(mock.ANY, image.id)
mocked_update.assert_called_once_with(
mock.ANY, image.id, name='Fedora', container_format='bare',
min_ram=512, disk_format='qcow2', protected=False,
is_public=False, min_disk=15, purge_props=False,
properties={'description': 'Login with admin/admin',
'architecture': 'x86-64'})
def test_delete_ok(self):
roles = [api.tuskar.Role(role) for role in
self.tuskarclient_roles.list()]
plans = [api.tuskar.Plan(plan) for plan in
self.tuskarclient_plans.list()]
images = self.glanceclient_images.list()
data = {'action': 'images__delete',
'object_ids': [images[0].id, images[1].id]}
with contextlib.nested(
patch('tuskar_ui.api.tuskar.Role.list',
return_value=roles),
patch('tuskar_ui.api.tuskar.Plan.list',
return_value=plans),
patch('openstack_dashboard.api.glance.image_list_detailed',
return_value=[images, False, False]),
patch('openstack_dashboard.api.glance.image_delete',
return_value=None),) as (
mock_role_list, plan_list, mock_image_lict, mock_image_delete):
res = self.client.post(INDEX_URL, data)
mock_image_delete.has_calls(
call(mock.ANY, images[0].id),
call(mock.ANY, images[1].id))
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)

View File

@ -1,25 +0,0 @@
# -*- coding: utf8 -*-
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.conf import urls
from tuskar_ui.infrastructure.images import views
urlpatterns = urls.patterns(
'',
urls.url(r'^$', views.IndexView.as_view(), name='index'),
urls.url(r'^create/$', views.CreateView.as_view(), name='create'),
urls.url(r'^(?P<image_id>[^/]+)/update/$',
views.UpdateView.as_view(), name='update'),
)

View File

@ -1,109 +0,0 @@
# -*- coding: utf8 -*-
#
# 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_lazy
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import tables as horizon_tables
from horizon.utils import memoized
from openstack_dashboard import api
from openstack_dashboard.dashboards.project.images.images import views
from tuskar_ui import api as tuskar_api
from tuskar_ui.infrastructure.images import forms
from tuskar_ui.infrastructure.images import tables
import tuskar_ui.infrastructure.views as infrastructure_views
from tuskar_ui.utils import utils
LOG = logging.getLogger(__name__)
class IndexView(infrastructure_views.ItemCountMixin,
horizon_tables.DataTableView):
table_class = tables.ImagesTable
template_name = "infrastructure/images/index.html"
@memoized.memoized_method
def get_data(self):
images = []
filters = self.get_filters()
sort_dir = 'desc'
try:
images, self._more, self._prev = api.glance.image_list_detailed(
self.request,
paginate=False,
filters=filters,
sort_dir=sort_dir)
images = [image for image in images
if utils.check_image_type(image,
'overcloud provisioning')]
except Exception:
msg = _('Unable to retrieve image list.')
exceptions.handle(self.request, msg)
plan = tuskar_api.tuskar.Plan.get_the_plan(self.request)
for image in images:
image.roles = tuskar_api.tuskar.Role.get_by_image(
self.request, plan, image)
return images
def get_filters(self):
filters = {'is_public': None}
filter_field = self.table.get_filter_field()
filter_string = self.table.get_filter_string()
filter_action = self.table._meta._filter_action
if filter_field and filter_string and (
filter_action.is_api_filter(filter_field)):
if filter_field in ['size_min', 'size_max']:
invalid_msg = ('API query is not valid and is ignored: %s=%s'
% (filter_field, filter_string))
try:
filter_string = long(float(filter_string) * (1024 ** 2))
if filter_string >= 0:
filters[filter_field] = filter_string
else:
LOG.warning(invalid_msg)
except ValueError:
LOG.warning(invalid_msg)
else:
filters[filter_field] = filter_string
return filters
class CreateView(views.CreateView):
submit_url = "horizon:infrastructure:images:create"
template_name = 'infrastructure/images/create.html'
success_url = reverse_lazy("horizon:infrastructure:images:index")
page_title = _("Create Image")
class UpdateView(views.UpdateView):
template_name = 'infrastructure/images/update.html'
form_class = forms.UpdateImageForm
success_url = reverse_lazy('horizon:infrastructure:images:index')
submit_url = "horizon:infrastructure:images:update"
submit_label = _("Update Image")
@memoized.memoized_method
def get_object(self):
try:
return api.glance.image_get(self.request, self.kwargs['image_id'])
except Exception:
msg = _('Unable to retrieve image.')
url = reverse_lazy('horizon:infrastructure:images:index')
exceptions.handle(self.request, msg, redirect=url)

View File

@ -1,319 +0,0 @@
# -*- coding: utf8 -*-
#
# 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 django.forms
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import forms
from horizon import messages
from tuskar_ui import api
import tuskar_ui.forms
from tuskar_ui.utils import utils
DEFAULT_KERNEL_IMAGE_NAME = 'bm-deploy-kernel'
DEFAULT_RAMDISK_IMAGE_NAME = 'bm-deploy-ramdisk'
CPU_ARCH_CHOICES = [
('', _("unspecified")),
('amd64', _("amd64")),
('x86', _("x86")),
('x86_64', _("x86_64")),
]
DRIVER_CHOICES = [
('pxe_ipmitool', _("IPMI Driver")),
('pxe_ssh', _("PXE + SSH")),
]
def get_driver_info_dict(data):
driver = data['driver']
driver_dict = {'driver': driver,
'deployment_kernel': data['deployment_kernel'],
'deployment_ramdisk': data['deployment_ramdisk'],
}
if driver == 'pxe_ipmitool':
driver_dict.update(
ipmi_address=data['ipmi_address'],
ipmi_username=data.get('ipmi_username'),
ipmi_password=data.get('ipmi_password'),
)
elif driver == 'pxe_ssh':
driver_dict.update(
ssh_address=data['ssh_address'],
ssh_username=data['ssh_username'],
ssh_key_contents=data['ssh_key_contents'],
)
return driver_dict
def create_node(request, data):
cpu_arch = data.get('cpu_arch')
cpus = data.get('cpus')
memory_mb = data.get('memory_mb')
local_gb = data.get('local_gb')
kwargs = get_driver_info_dict(data)
kwargs.update(
cpu_arch=cpu_arch,
cpus=cpus,
memory_mb=memory_mb,
local_gb=local_gb,
mac_addresses=data['mac_addresses'].split(),
)
success = True
try:
node = api.node.Node.create(request, **kwargs)
except Exception:
success = False
exceptions.handle(request, _(u"Unable to register node."))
else:
# If not all the parameters have been filled in,
# run the auto-discovery. Note, that the node has been created,
# so even if we fail here, we report success.
if not all([cpu_arch, cpus, memory_mb, local_gb]):
node_uuid = node.uuid
try:
api.node.Node.set_maintenance(request, node_uuid, True)
except Exception:
exceptions.handle(request, _(
u"Can't set maintenance mode on node {0}."
).format(node_uuid))
else:
try:
api.node.Node.discover(request, [node_uuid])
except Exception:
exceptions.handle(request, _(
u"Can't start discovery on node {0}."
).format(node_uuid))
return success
class NodeForm(django.forms.Form):
id = django.forms.IntegerField(
label="",
required=False,
widget=django.forms.HiddenInput(),
)
driver = django.forms.ChoiceField(
label=_("Driver"),
choices=DRIVER_CHOICES,
required=True,
widget=django.forms.Select(attrs={
'class': 'form-control switchable',
'data-slug': 'driver',
}),
)
ipmi_address = django.forms.IPAddressField(
label=_("IPMI Address"),
required=False,
widget=django.forms.TextInput(attrs={
'class': 'form-control switched',
'data-switch-on': 'driver',
'data-driver-pxe_ipmitool': _("IPMI Driver"),
}),
)
ipmi_username = django.forms.CharField(
label=_("IPMI User"),
required=False,
widget=django.forms.TextInput(attrs={
'class': 'form-control switched',
'data-switch-on': 'driver',
'data-driver-pxe_ipmitool': _("IPMI Driver"),
}),
)
ipmi_password = django.forms.CharField(
label=_("IPMI Password"),
required=False,
widget=django.forms.PasswordInput(render_value=True, attrs={
'class': 'form-control switched',
'data-switch-on': 'driver',
'data-driver-pxe_ipmitool': _("IPMI Driver"),
}),
)
ssh_address = django.forms.IPAddressField(
label=_("SSH Address"),
required=False,
widget=django.forms.TextInput(attrs={
'class': 'form-control switched',
'data-switch-on': 'driver',
'data-driver-pxe_ssh': _("PXE + SSH"),
}),
)
ssh_username = django.forms.CharField(
label=_("SSH User"),
required=False,
widget=django.forms.TextInput(attrs={
'class': 'form-control switched',
'data-switch-on': 'driver',
'data-driver-pxe_ssh': _("PXE + SSH"),
}),
)
ssh_key_contents = django.forms.CharField(
label=_("SSH Key Contents"),
required=False,
widget=django.forms.Textarea(attrs={
'class': 'form-control switched',
'data-switch-on': 'driver',
'data-driver-pxe_ssh': _("PXE + SSH"),
'rows': 2,
}),
)
mac_addresses = tuskar_ui.forms.MultiMACField(
label=_("NIC MAC Addresses"),
required=True,
widget=django.forms.Textarea(attrs={
'placeholder': _('unspecified'),
'rows': '2',
}),
)
cpu_arch = django.forms.ChoiceField(
label=_("Architecture"),
required=False,
choices=CPU_ARCH_CHOICES,
widget=django.forms.Select(
attrs={'placeholder': _('unspecified')}),
)
cpus = django.forms.IntegerField(
label=_("CPUs"),
required=False,
min_value=0,
widget=tuskar_ui.forms.NumberInput(
attrs={'placeholder': _('unspecified')}),
)
memory_mb = django.forms.IntegerField(
label=_("Memory"),
required=False,
min_value=0,
widget=tuskar_ui.forms.NumberInput(
attrs={'placeholder': _('unspecified')}),
)
local_gb = django.forms.IntegerField(
label=_("Local Disk"),
required=False,
min_value=0,
widget=tuskar_ui.forms.NumberInput(
attrs={'placeholder': _('unspecified')}),
)
deployment_kernel = django.forms.ChoiceField(
label=_("Kernel"),
required=False,
choices=[],
widget=django.forms.Select(),
)
deployment_ramdisk = django.forms.ChoiceField(
label=_("Ramdisk"),
required=False,
choices=[],
widget=django.forms.Select(),
)
def get_name(self):
try:
name = (self.fields['ipmi_address'].value() or
self.fields['ssh_address'].value())
except AttributeError:
# when the field is not bound
name = _("Undefined node")
return name
def handle(self, request, data):
return create_node(request, data)
def clean_ipmi_username(self):
return self.cleaned_data.get('ipmi_username') or None
def clean_ipmi_password(self):
return self.cleaned_data.get('ipmi_password') or None
def _require_field(self, field_name, cleaned_data):
if cleaned_data.get(field_name):
return
self._errors[field_name] = self.error_class([_(
u"This field is required"
)])
def clean(self):
cleaned_data = super(NodeForm, self).clean()
driver = cleaned_data['driver']
if driver == 'pxe_ipmitool':
self._require_field('ipmi_address', cleaned_data)
elif driver == 'pxe_ssh':
self._require_field('ssh_address', cleaned_data)
self._require_field('ssh_username', cleaned_data)
self._require_field('ssh_key_contents', cleaned_data)
return cleaned_data
class BaseNodeFormset(tuskar_ui.forms.SelfHandlingFormset):
def __init__(self, *args, **kwargs):
self.kernel_images = kwargs.pop('kernel_images')
self.ramdisk_images = kwargs.pop('ramdisk_images')
super(BaseNodeFormset, self).__init__(*args, **kwargs)
def add_fields(self, form, index):
deployment_kernel_choices = [(kernel.id, kernel.name)
for kernel in self.kernel_images]
deployment_ramdisk_choices = [(ramdisk.id, ramdisk.name)
for ramdisk in self.ramdisk_images]
form.fields['deployment_kernel'].choices = deployment_kernel_choices
form.fields['deployment_ramdisk'].choices = deployment_ramdisk_choices
def clean(self):
all_macs = api.node.Node.get_all_mac_addresses(self.request)
bad_macs = set()
bad_macs_error = _("Duplicate MAC addresses submitted: %s.")
for form in self:
if not form.cleaned_data:
raise django.forms.ValidationError(
_("Please provide node data for all nodes."))
new_macs = form.cleaned_data.get('mac_addresses')
if not new_macs:
continue
new_macs = set(new_macs.split())
# Prevent submitting duplicated MAC addresses
# or MAC addresses of existing nodes
bad_macs |= all_macs & new_macs
all_macs |= new_macs
if bad_macs:
raise django.forms.ValidationError(
bad_macs_error % ", ".join(bad_macs))
class UploadNodeForm(forms.SelfHandlingForm):
csv_file = forms.FileField(label='', required=False)
def handle(self, request, data):
return True
def get_data(self):
try:
output = utils.parse_csv_file(self.cleaned_data['csv_file'])
except ValueError as e:
messages.error(self.request, e.message)
output = []
return output
RegisterNodeFormset = django.forms.formsets.formset_factory(
NodeForm, extra=1, formset=BaseNodeFormset)

View File

@ -1,26 +0,0 @@
# -*- coding: utf8 -*-
#
# 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 tuskar_ui.infrastructure import dashboard
class Nodes(horizon.Panel):
name = _("Nodes")
slug = "nodes"
dashboard.Infrastructure.register(Nodes)

View File

@ -1,269 +0,0 @@
# -*- coding: utf8 -*-
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from horizon import messages
from horizon import tables
from horizon.utils import memoized
from tuskar_ui import api
class DeleteNode(tables.BatchAction):
name = "delete"
action_present = _("Delete")
action_past = _("Deleting")
data_type_singular = _("Node")
data_type_plural = _("Nodes")
classes = ('btn-danger',)
def allowed(self, request, obj=None):
if not obj:
# this is necessary because table actions use this function
# with obj=None
return True
return (getattr(obj, 'instance_uuid', None) is None and
obj.power_state not in api.node.POWER_ON_STATES)
def action(self, request, obj_id):
if obj_id is None:
messages.error(request, _("Select some nodes to delete."))
return
api.node.Node.delete(request, obj_id)
class ActivateNode(tables.BatchAction):
name = "activate"
action_present = _("Activate")
action_past = _("Activated")
data_type_singular = _("Node")
data_type_plural = _("Nodes")
def allowed(self, request, obj=None):
if not obj:
# this is necessary because table actions use this function
# with obj=None
return True
return (obj.cpus and obj.memory_mb and obj.local_gb and
obj.cpu_arch)
def action(self, request, obj_id):
if obj_id is None:
messages.error(request, _("Select some nodes to activate."))
return
api.node.Node.set_maintenance(request, obj_id, False)
api.node.Node.set_power_state(request, obj_id, 'off')
class SetPowerStateOn(tables.BatchAction):
name = "set_power_state_on"
action_present = _("Power On")
action_past = _("Powering On")
data_type_singular = _("Node")
data_type_plural = _("Nodes")
def allowed(self, request, obj=None):
if not obj:
# this is necessary because table actions use this function
# with obj=None
return True
return obj.power_state not in api.node.POWER_ON_STATES
def action(self, request, obj_id):
if obj_id is None:
messages.error(request, _("Select some nodes to power on."))
return
api.node.Node.set_power_state(request, obj_id, 'on')
class SetPowerStateOff(tables.BatchAction):
name = "set_power_state_off"
action_present = _("Power Off")
action_past = _("Powering Off")
data_type_singular = _("Node")
data_type_plural = _("Nodes")
def allowed(self, request, obj=None):
if not obj:
# this is necessary because table actions use this function
# with obj=None
return True
return (
obj.power_state in api.node.POWER_ON_STATES and
getattr(obj, 'instance_uuid', None) is None
)
def action(self, request, obj_id):
if obj_id is None:
messages.error(request, _("Select some nodes to power off."))
return
api.node.Node.set_power_state(request, obj_id, 'off')
class NodeFilterAction(tables.FilterAction):
def filter(self, table, nodes, filter_string):
"""Really naive case-insensitive search."""
q = filter_string.lower()
def comp(node):
return any(q in unicode(value).lower() for value in (
node.ip_address,
node.cpus,
node.memory_mb,
node.local_gb,
))
return filter(comp, nodes)
class DiscoverNode(tables.BatchAction):
name = "discover_nodes"
action_present = _("Discover")
action_past = _("Discovered")
data_type_singular = _("Node")
data_type_plural = _("Nodes")
def allowed(self, request, obj=None):
if not obj:
# this is necessary because table actions use this function
# with obj=None
return True
return obj.state == api.node.MAINTENANCE_STATE
def action(self, request, obj_id):
if obj_id is None:
messages.error(request, _("Select some nodes to discover."))
return
api.node.Node.discover(request, [obj_id])
@memoized.memoized
def _get_role_link(role_id):
if role_id:
return reverse('horizon:infrastructure:roles:detail',
kwargs={'role_id': role_id})
def get_role_link(datum):
return _get_role_link(getattr(datum, 'role_id', None))
def get_power_state_with_transition(node):
if node.target_power_state and (
node.power_state != node.target_power_state):
return "{0} -> {1}".format(
node.power_state, node.target_power_state)
return node.power_state
def get_state_string(node):
state_dict = {
api.node.DISCOVERING_STATE: _('Discovering'),
api.node.DISCOVERED_STATE: _('Discovered'),
api.node.PROVISIONED_STATE: _('Provisioned'),
api.node.PROVISIONING_FAILED_STATE: _('Provisioning Failed'),
api.node.PROVISIONING_STATE: _('Provisioning'),
api.node.FREE_STATE: _('Free'),
}
node_state = node.state
return state_dict.get(node_state, node_state)
class BaseNodesTable(tables.DataTable):
node = tables.Column('uuid',
link="horizon:infrastructure:nodes:node_detail",
verbose_name=_("Node Name"))
role_name = tables.Column('role_name',
link=get_role_link,
verbose_name=_("Deployment Role"))
cpus = tables.Column('cpus',
verbose_name=_("CPU (cores)"))
memory_mb = tables.Column('memory_mb',
verbose_name=_("Memory (MB)"))
local_gb = tables.Column('local_gb',
verbose_name=_("Disk (GB)"))
power_status = tables.Column(get_power_state_with_transition,
verbose_name=_("Power Status"))
state = tables.Column(get_state_string,
verbose_name=_("Status"))
class Meta(object):
name = "nodes_table"
verbose_name = _("Nodes")
table_actions = (NodeFilterAction, SetPowerStateOn, SetPowerStateOff,
DeleteNode)
row_actions = (SetPowerStateOn, SetPowerStateOff, DeleteNode)
template = "horizon/common/_enhanced_data_table.html"
def get_object_id(self, datum):
return datum.uuid
def get_object_display(self, datum):
return datum.uuid
class AllNodesTable(BaseNodesTable):
class Meta(object):
name = "all_nodes_table"
verbose_name = _("All")
hidden_title = False
columns = ('node', 'cpus', 'memory_mb', 'local_gb', 'power_status',
'state')
table_actions = (NodeFilterAction, SetPowerStateOn, SetPowerStateOff,
DeleteNode)
row_actions = (SetPowerStateOn, SetPowerStateOff, DeleteNode)
template = "horizon/common/_enhanced_data_table.html"
class ProvisionedNodesTable(BaseNodesTable):
class Meta(object):
name = "provisioned_nodes_table"
verbose_name = _("Provisioned")
hidden_title = False
table_actions = (NodeFilterAction, SetPowerStateOn, SetPowerStateOff,
DeleteNode)
row_actions = (SetPowerStateOn, SetPowerStateOff, DeleteNode)
template = "horizon/common/_enhanced_data_table.html"
class FreeNodesTable(BaseNodesTable):
class Meta(object):
name = "free_nodes_table"
verbose_name = _("Free")
hidden_title = False
columns = ('node', 'cpus', 'memory_mb', 'local_gb', 'power_status')
table_actions = (NodeFilterAction, SetPowerStateOn, SetPowerStateOff,
DeleteNode)
row_actions = (SetPowerStateOn, SetPowerStateOff, DeleteNode,)
template = "horizon/common/_enhanced_data_table.html"
class MaintenanceNodesTable(BaseNodesTable):
class Meta(object):
name = "maintenance_nodes_table"
verbose_name = _("Maintenance")
hidden_title = False
columns = ('node', 'cpus', 'memory_mb', 'local_gb', 'power_status',
'state')
table_actions = (NodeFilterAction, ActivateNode, SetPowerStateOn,
SetPowerStateOff, DiscoverNode, DeleteNode)
row_actions = (ActivateNode, SetPowerStateOn, SetPowerStateOff,
DeleteNode)
template = "horizon/common/_enhanced_data_table.html"

View File

@ -1,378 +0,0 @@
# Copyright 2012 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import itertools
from django.core import urlresolvers
from django.utils.translation import ugettext_lazy as _
from horizon import tabs
from horizon.utils import functions
from openstack_dashboard.api import base as api_base
from tuskar_ui import api
from tuskar_ui.cached_property import cached_property # noqa
from tuskar_ui.infrastructure.nodes import tables
from tuskar_ui.utils import metering as metering_utils
from tuskar_ui.utils import utils
def filter_extra(nodes, index, value):
return (node for node in nodes
if node.extra.get(index, None) == value)
class OverviewTab(tabs.Tab):
name = _("Overview")
slug = "overview"
template_name = "infrastructure/nodes/_overview.html"
def get_context_data(self, request):
nodes = self.tab_group.kwargs['nodes']
cpus = sum(int(node.cpus) for node in nodes if node.cpus)
memory_mb = sum(int(node.memory_mb) for node in nodes if
node.memory_mb)
local_gb = sum(int(node.local_gb) for node in nodes if node.local_gb)
nodes_provisioned = set(utils.filter_items(
nodes, provision_state__in=api.node.PROVISION_STATE_PROVISIONED))
nodes_free = set(utils.filter_items(
nodes, provision_state__in=api.node.PROVISION_STATE_FREE))
nodes_deleting = set(utils.filter_items(
nodes, provision_state__in=api.node.PROVISION_STATE_DELETING))
nodes_error = set(utils.filter_items(
nodes, provision_state__in=api.node.PROVISION_STATE_ERROR))
nodes_provisioned_maintenance = set(utils.filter_items(
nodes_provisioned, maintenance=True))
nodes_provisioned_not_maintenance = (
nodes_provisioned - nodes_provisioned_maintenance)
nodes_provisioning = set(utils.filter_items(
nodes,
provision_state__in=api.node.PROVISION_STATE_PROVISIONING))
nodes_free_maintenance = set(utils.filter_items(
nodes_free, maintenance=True))
nodes_free_not_maintenance = (
nodes_free - nodes_free_maintenance)
nodes_maintenance = (
nodes_provisioned_maintenance | nodes_free_maintenance)
nodes_provisioned_down = utils.filter_items(
nodes_provisioned, power_state__not_in=api.node.POWER_ON_STATES)
nodes_free_down = utils.filter_items(
nodes_free, power_state__not_in=api.node.POWER_ON_STATES)
nodes_on_discovery = filter_extra(
nodes_maintenance, 'on_discovery', 'true')
nodes_discovered = filter_extra(
nodes_maintenance, 'newly_discovered', 'true')
nodes_discovery_failed = filter_extra(
nodes_maintenance, 'discovery_failed', 'true')
nodes_down = itertools.chain(nodes_provisioned_down, nodes_free_down)
nodes_up = utils.filter_items(
nodes, power_state__in=api.node.POWER_ON_STATES)
nodes_free_count = len(nodes_free_not_maintenance)
nodes_provisioned_count = len(
nodes_provisioned_not_maintenance)
nodes_provisioning_count = len(nodes_provisioning)
nodes_maintenance_count = len(nodes_maintenance)
nodes_deleting_count = len(nodes_deleting)
nodes_error_count = len(nodes_error)
context = {
'cpus': cpus,
'memory_gb': memory_mb / 1024.0,
'local_gb': local_gb,
'nodes_up_count': utils.length(nodes_up),
'nodes_down_count': utils.length(nodes_down),
'nodes_provisioned_count': nodes_provisioned_count,
'nodes_provisioning_count': nodes_provisioning_count,
'nodes_free_count': nodes_free_count,
'nodes_deleting_count': nodes_deleting_count,
'nodes_error_count': nodes_error_count,
'nodes_maintenance_count': nodes_maintenance_count,
'nodes_all_count': len(nodes),
'nodes_on_discovery_count': utils.length(nodes_on_discovery),
'nodes_discovered_count': utils.length(nodes_discovered),
'nodes_discovery_failed_count': utils.length(
nodes_discovery_failed),
'nodes_status_data':
'Provisioned={0}|Free={1}|Maintenance={2}'.format(
nodes_provisioned_count, nodes_free_count,
nodes_maintenance_count)
}
# additional node status pie chart data, showing only if it appears
if nodes_provisioning_count:
context['nodes_status_data'] += '|Provisioning={0}'.format(
nodes_provisioning_count)
if nodes_deleting_count:
context['nodes_status_data'] += '|Deleting={0}'.format(
nodes_deleting_count)
if nodes_error_count:
context['nodes_status_data'] += '|Error={0}'.format(
nodes_error_count)
if api_base.is_service_enabled(self.request, 'metering'):
context['meter_conf'] = (
(_('System Load'),
metering_utils.url_part('hardware.cpu.load.1min', False),
None),
(_('CPU Utilization'),
metering_utils.url_part('hardware.system_stats.cpu.util',
True),
'100'),
(_('Swap Utilization'),
metering_utils.url_part('hardware.memory.swap.util',
True),
'100'),
)
# TODO(akrivoka): Ajaxize these calls so that they don't hold up the
# whole page load
context['top_5'] = {
'fan': metering_utils.get_top_5(request, 'hardware.ipmi.fan'),
'voltage': metering_utils.get_top_5(
request, 'hardware.ipmi.voltage'),
'temperature': metering_utils.get_top_5(
request, 'hardware.ipmi.temperature'),
'current': metering_utils.get_top_5(
request, 'hardware.ipmi.current'),
}
return context
class BaseTab(tabs.TableTab):
table_classes = (tables.BaseNodesTable,)
name = _("Nodes")
slug = "nodes"
template_name = "horizon/common/_detail_table.html"
def __init__(self, tab_group, request):
super(BaseTab, self).__init__(tab_group, request)
@cached_property
def _nodes(self):
return []
def get_items_count(self):
return len(self._nodes)
@cached_property
def _nodes_info(self):
page_size = functions.get_page_size(self.request)
prev_marker = self.request.GET.get(
self.table_classes[0]._meta.prev_pagination_param, None)
if prev_marker is not None:
sort_dir = 'asc'
marker = prev_marker
else:
sort_dir = 'desc'
marker = self.request.GET.get(
self.table_classes[0]._meta.pagination_param, None)
nodes = self._nodes
if marker:
node_ids = [node.uuid for node in self._nodes]
position = node_ids.index(marker)
if sort_dir == 'asc':
start = max(0, position - page_size)
end = position
else:
start = position + 1
end = start + page_size
else:
start = 0
end = page_size
prev = start != 0
more = len(nodes) > end
return nodes[start:end], prev, more
def get_base_nodes_table_data(self):
nodes, prev, more = self._nodes_info
return nodes
def has_prev_data(self, table):
return self._nodes_info[1]
def has_more_data(self, table):
return self._nodes_info[2]
class AllTab(BaseTab):
table_classes = (tables.AllNodesTable,)
name = _("All")
slug = "all"
def __init__(self, tab_group, request):
super(AllTab, self).__init__(tab_group, request)
@cached_property
def _nodes(self):
return self.tab_group.kwargs['nodes']
def get_all_nodes_table_data(self):
nodes, prev, more = self._nodes_info
return nodes
class ProvisionedTab(BaseTab):
table_classes = (tables.ProvisionedNodesTable,)
name = _("Provisioned")
slug = "provisioned"
def __init__(self, tab_group, request):
super(ProvisionedTab, self).__init__(tab_group, request)
@cached_property
def _nodes(self):
redirect = urlresolvers.reverse('horizon:infrastructure:nodes:index')
return api.node.Node.list(self.request, associated=True,
maintenance=False, _error_redirect=redirect)
def get_provisioned_nodes_table_data(self):
nodes, prev, more = self._nodes_info
if nodes:
for node in nodes:
try:
resource = api.heat.Resource.get_by_node(
self.request, node)
except LookupError:
node.role_name = '-'
else:
node.role_name = resource.role.name
node.role_id = resource.role.id
node.stack_id = resource.stack.id
return nodes
class FreeTab(BaseTab):
table_classes = (tables.FreeNodesTable,)
name = _("Free")
slug = "free"
def __init__(self, tab_group, request):
super(FreeTab, self).__init__(tab_group, request)
@cached_property
def _nodes(self):
redirect = urlresolvers.reverse('horizon:infrastructure:nodes:index')
return api.node.Node.list(self.request, associated=False,
maintenance=False, _error_redirect=redirect)
def get_free_nodes_table_data(self):
nodes, prev, more = self._nodes_info
return nodes
class MaintenanceTab(BaseTab):
table_classes = (tables.MaintenanceNodesTable,)
name = _("Maintenance")
slug = "maintenance"
def __init__(self, tab_group, request):
super(MaintenanceTab, self).__init__(tab_group, request)
@cached_property
def _nodes(self):
nodes = self.tab_group.kwargs['nodes']
return list(utils.filter_items(nodes, maintenance=True))
def get_maintenance_nodes_table_data(self):
return self._nodes
class DetailOverviewTab(tabs.Tab):
name = _("Overview")
slug = "detail_overview"
template_name = 'infrastructure/nodes/_detail_overview.html'
def get_context_data(self, request):
node = self.tab_group.kwargs['node']
context = {'node': node}
try:
resource = api.heat.Resource.get_by_node(self.request, node)
except LookupError:
pass
else:
context['role'] = resource.role
context['stack'] = resource.stack
kernel_id = node.driver_info.get('deploy_kernel')
if kernel_id:
context['kernel_image'] = api.node.image_get(request, kernel_id)
ramdisk_id = node.driver_info.get('deploy_ramdisk')
if ramdisk_id:
context['ramdisk_image'] = api.node.image_get(request, ramdisk_id)
if node.instance_uuid:
if api_base.is_service_enabled(self.request, 'metering'):
# Meter configuration in the following format:
# (meter label, url part, y_max)
context['meter_conf'] = (
(_('System Load'),
metering_utils.url_part('hardware.cpu.load.1min', False),
None),
(_('CPU Utilization'),
metering_utils.url_part('hardware.system_stats.cpu.util',
True),
'100'),
(_('Swap Utilization'),
metering_utils.url_part('hardware.memory.swap.util',
True),
'100'),
(_('Current'),
metering_utils.url_part('hardware.ipmi.current', False),
None),
(_('Network IO'),
metering_utils.url_part('network-io', False),
None),
(_('Disk IO'),
metering_utils.url_part('disk-io', False),
None),
(_('Temperature'),
metering_utils.url_part('hardware.ipmi.temperature',
False),
None),
(_('Fan Speed'),
metering_utils.url_part('hardware.ipmi.fan', False),
None),
(_('Voltage'),
metering_utils.url_part('hardware.ipmi.voltage', False),
None),
)
return context
class NodeTabs(tabs.TabGroup):
slug = "nodes"
tabs = (OverviewTab, AllTab, ProvisionedTab, FreeTab, MaintenanceTab,)
sticky = True
template_name = "horizon/common/_items_count_tab_group.html"
class NodeDetailTabs(tabs.TabGroup):
slug = "node_details"
tabs = (DetailOverviewTab,)

View File

@ -1,596 +0,0 @@
# -*- coding: utf8 -*-
#
# 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 contextlib
import json
from ceilometerclient.v2 import client as ceilometer_client
from django.core import urlresolvers
from horizon import exceptions as horizon_exceptions
from ironicclient import exceptions as ironic_exceptions
import mock
from novaclient import exceptions as nova_exceptions
from openstack_dashboard.test.test_data import utils
from tuskar_ui import api
from tuskar_ui.handle_errors import handle_errors # noqa
from tuskar_ui.infrastructure.nodes import forms
from tuskar_ui.test import helpers as test
from tuskar_ui.test.test_data import heat_data
from tuskar_ui.test.test_data import node_data
from tuskar_ui.test.test_data import tuskar_data
INDEX_URL = urlresolvers.reverse('horizon:infrastructure:nodes:index')
REGISTER_URL = urlresolvers.reverse('horizon:infrastructure:nodes:register')
DETAIL_VIEW = 'horizon:infrastructure:nodes:node_detail'
PERFORMANCE_VIEW = 'horizon:infrastructure:nodes:performance'
TEST_DATA = utils.TestDataContainer()
node_data.data(TEST_DATA)
heat_data.data(TEST_DATA)
tuskar_data.data(TEST_DATA)
def _raise_nova_client_exception(*args, **kwargs):
raise nova_exceptions.ClientException("Boom!")
class NodesTests(test.BaseAdminViewTests):
@handle_errors("Error!", [])
def _raise_tuskar_exception(self, request, *args, **kwargs):
raise self.exceptions.tuskar
@handle_errors("Error!", [])
def _raise_horizon_exception_not_found(self, request, *args, **kwargs):
raise horizon_exceptions.NotFound
def _raise_ironic_exception(self, request, *args, **kwargs):
raise ironic_exceptions.Conflict
def stub_ceilometerclient(self):
if not hasattr(self, "ceilometerclient"):
self.mox.StubOutWithMock(ceilometer_client, 'Client')
self.ceilometerclient = self.mox.CreateMock(
ceilometer_client.Client,
)
return self.ceilometerclient
def test_index_get(self):
with mock.patch('tuskar_ui.api.node.Node', **{
'spec_set': ['list'],
'list.return_value': [],
}) as mocked:
res = self.client.get(INDEX_URL)
self.assertEqual(mocked.list.call_count, 3)
self.assertTemplateUsed(
res, 'infrastructure/nodes/index.html')
self.assertTemplateUsed(res, 'infrastructure/nodes/_overview.html')
def _all_mocked_nodes(self):
request = mock.MagicMock()
return [api.node.Node(api.node.Node(node, request))
for node in self.ironicclient_nodes.list()]
def _test_index_tab(self, tab_name, nodes):
with mock.patch('tuskar_ui.api.node.Node', **{
'spec_set': ['list'],
'list.return_value': nodes,
}) as Node:
res = self.client.get(INDEX_URL + '?tab=nodes__' + tab_name)
self.assertEqual(Node.list.call_count, 3)
self.assertTemplateUsed(
res, 'infrastructure/nodes/index.html')
self.assertTemplateUsed(res, 'horizon/common/_detail_table.html')
self.assertItemsEqual(
res.context[tab_name + '_nodes_table_table'].data,
nodes)
def test_all_nodes(self):
nodes = self._all_mocked_nodes()
self._test_index_tab('all', nodes)
def test_provisioned_nodes(self):
nodes = self._all_mocked_nodes()
self._test_index_tab('provisioned', nodes)
def test_free_nodes(self):
nodes = self._all_mocked_nodes()
self._test_index_tab('free', nodes)
def test_maintenance_nodes(self):
nodes = self._all_mocked_nodes()[6:]
self._test_index_tab('maintenance', nodes)
def _test_index_tab_list_exception(self, tab_name):
with mock.patch('tuskar_ui.api.node.Node', **{
'spec_set': ['list'],
'list.side_effect': self._raise_tuskar_exception,
}) as mocked:
res = self.client.get(INDEX_URL + '?tab=nodes__' + tab_name)
self.assertEqual(mocked.list.call_count, 2)
self.assertRedirectsNoFollow(res, INDEX_URL)
def test_all_nodes_list_exception(self):
self._test_index_tab_list_exception('all')
def test_provisioned_nodes_list_exception(self):
self._test_index_tab_list_exception('provisioned')
def test_free_nodes_list_exception(self):
self._test_index_tab_list_exception('free')
def test_maintenance_nodes_list_exception(self):
self._test_index_tab_list_exception('maintenance')
def test_register_get(self):
with mock.patch('openstack_dashboard.api.glance.image_list_detailed',
return_value=([], False)) as mocked:
res = self.client.get(REGISTER_URL)
self.assertEqual(mocked.call_count, 2)
self.assertTemplateUsed(
res, 'infrastructure/nodes/register.html')
def test_register_post(self):
node = TEST_DATA.ironicclient_nodes.first
nodes = self._all_mocked_nodes()
images = self.glanceclient_images.list()
data = {
'register_nodes-TOTAL_FORMS': 2,
'register_nodes-INITIAL_FORMS': 1,
'register_nodes-MAX_NUM_FORMS': 1000,
'register_nodes-0-driver': 'pxe_ipmitool',
'register_nodes-0-ipmi_address': '127.0.0.1',
'register_nodes-0-ipmi_username': 'username',
'register_nodes-0-ipmi_password': 'password',
'register_nodes-0-mac_addresses': 'de:ad:be:ef:ca:fe',
'register_nodes-0-cpu_arch': 'x86',
'register_nodes-0-cpus': '1',
'register_nodes-0-memory_mb': '2',
'register_nodes-0-local_gb': '3',
'register_nodes-0-deployment_kernel': images[3].id,
'register_nodes-0-deployment_ramdisk': images[4].id,
'register_nodes-1-driver': 'pxe_ipmitool',
'register_nodes-1-ipmi_address': '127.0.0.2',
'register_nodes-1-mac_addresses': 'de:ad:be:ef:ca:ff',
'register_nodes-1-cpu_arch': 'x86',
'register_nodes-1-cpus': '4',
'register_nodes-1-memory_mb': '5',
'register_nodes-1-local_gb': '6',
'register_nodes-1-deployment_kernel': images[3].id,
'register_nodes-1-deployment_ramdisk': images[4].id,
}
with mock.patch('tuskar_ui.api.node.Node', **{
'spec_set': ['create', 'get_all_mac_addresses'],
'create.return_value': node,
'get_all_mac_addresses.return_value': set(nodes),
}) as Node, mock.patch(
'openstack_dashboard.api.glance.image_list_detailed',
return_value=[images, False, False]
):
res = self.client.post(REGISTER_URL, data)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.assertListEqual(Node.create.call_args_list, [
mock.call(
mock.ANY,
ipmi_address=u'127.0.0.1',
cpu_arch='x86',
cpus=1,
memory_mb=2,
local_gb=3,
mac_addresses=['DE:AD:BE:EF:CA:FE'],
ipmi_username=u'username',
ipmi_password=u'password',
driver='pxe_ipmitool',
deployment_kernel=images[3].id,
deployment_ramdisk=images[4].id,
),
mock.call(
mock.ANY,
ipmi_address=u'127.0.0.2',
cpu_arch='x86',
cpus=4,
memory_mb=5,
local_gb=6,
mac_addresses=['DE:AD:BE:EF:CA:FF'],
ipmi_username=None,
ipmi_password=None,
driver='pxe_ipmitool',
deployment_kernel=images[3].id,
deployment_ramdisk=images[4].id,
),
])
def test_register_post_exception(self):
nodes = self._all_mocked_nodes()
images = self.glanceclient_images.list()
data = {
'register_nodes-TOTAL_FORMS': 2,
'register_nodes-INITIAL_FORMS': 1,
'register_nodes-MAX_NUM_FORMS': 1000,
'register_nodes-0-driver': 'pxe_ipmitool',
'register_nodes-0-ipmi_address': '127.0.0.1',
'register_nodes-0-ipmi_username': 'username',
'register_nodes-0-ipmi_password': 'password',
'register_nodes-0-mac_addresses': 'de:ad:be:ef:ca:fe',
'register_nodes-0-cpu_arch': 'x86',
'register_nodes-0-cpus': '1',
'register_nodes-0-memory_mb': '2',
'register_nodes-0-local_gb': '3',
'register_nodes-0-deployment_kernel': images[3].id,
'register_nodes-0-deployment_ramdisk': images[4].id,
'register_nodes-1-driver': 'pxe_ipmitool',
'register_nodes-1-ipmi_address': '127.0.0.2',
'register_nodes-1-mac_addresses': 'de:ad:be:ef:ca:ff',
'register_nodes-1-cpu_arch': 'x86',
'register_nodes-1-cpus': '4',
'register_nodes-1-memory_mb': '5',
'register_nodes-1-local_gb': '6',
'register_nodes-1-deployment_kernel': images[3].id,
'register_nodes-1-deployment_ramdisk': images[4].id,
}
with mock.patch('tuskar_ui.api.node.Node', **{
'spec_set': ['create', 'get_all_mac_addresses'],
'create.side_effect': self.exceptions.tuskar,
'get_all_mac_addresses.return_value': set(nodes),
}) as Node, mock.patch(
'openstack_dashboard.api.glance.image_list_detailed',
return_value=[images, False, False]
):
res = self.client.post(REGISTER_URL, data)
self.assertEqual(res.status_code, 200)
self.assertListEqual(Node.create.call_args_list, [
mock.call(
mock.ANY,
ipmi_address=u'127.0.0.1',
cpu_arch='x86',
cpus=1,
memory_mb=2,
local_gb=3,
mac_addresses=['DE:AD:BE:EF:CA:FE'],
ipmi_username=u'username',
ipmi_password=u'password',
driver='pxe_ipmitool',
deployment_kernel=images[3].id,
deployment_ramdisk=images[4].id,
),
mock.call(
mock.ANY,
ipmi_address=u'127.0.0.2',
cpu_arch='x86',
cpus=4,
memory_mb=5,
local_gb=6,
mac_addresses=['DE:AD:BE:EF:CA:FF'],
ipmi_username=None,
ipmi_password=None,
driver='pxe_ipmitool',
deployment_kernel=images[3].id,
deployment_ramdisk=images[4].id,
),
])
self.assertTemplateUsed(
res, 'infrastructure/nodes/register.html')
def test_node_detail(self):
node = api.node.Node(self.ironicclient_nodes.list()[0])
def get_node(request, uuid, **kwargs):
node._request = request
node.addresses = []
return node
image = self.glanceclient_images.first()
with contextlib.nested(
mock.patch('tuskar_ui.api.node.Node', **{
'spec_set': ['get'],
'get.side_effect': get_node,
}),
mock.patch('tuskar_ui.api.heat.Resource', **{
'spec_set': ['get_by_node'],
'get_by_node.side_effect': lambda *args, **kwargs: {}[None],
# Raises LookupError
}),
mock.patch(
'openstack_dashboard.api.glance.image_get',
return_value=image,
),
mock.patch(
'openstack_dashboard.api.nova.server_list',
return_value=([], False),
),
) as (mock_node, mock_heat, mock_glance, mock_nova):
res = self.client.get(
urlresolvers.reverse(DETAIL_VIEW, args=(node.uuid,))
)
self.assertEqual(mock_node.get.call_count, 1)
self.assertTemplateUsed(res, 'infrastructure/nodes/detail.html')
self.assertEqual(res.context['node'], node)
def test_node_detail_exception(self):
with mock.patch('tuskar_ui.api.node.Node', **{
'spec_set': ['get'],
'get.side_effect': self._raise_tuskar_exception,
}) as mocked:
res = self.client.get(
urlresolvers.reverse(DETAIL_VIEW, args=('no-such-node',))
)
self.assertEqual(mocked.get.call_count, 1)
self.assertRedirectsNoFollow(res, INDEX_URL)
def test_node_set_power_on(self):
all_nodes = [api.node.Node(api.node.Node(node))
for node in self.ironicclient_nodes.list()]
node = all_nodes[6]
roles = [api.tuskar.Role(r)
for r in TEST_DATA.tuskarclient_roles.list()]
instance = TEST_DATA.novaclient_servers.first()
image = TEST_DATA.glanceclient_images.first()
data = {'action': "all_nodes_table__set_power_state_on__{0}".format(
node.uuid)}
with contextlib.nested(
mock.patch('tuskar_ui.api.node.Node', **{
'spec_set': ['list', 'set_power_state'],
'list.return_value': all_nodes,
'set_power_state.return_value': node,
}),
mock.patch('tuskar_ui.api.tuskar.Role', **{
'spec_set': ['list', 'name'],
'list.return_value': roles,
}),
mock.patch('tuskar_ui.api.node.nova', **{
'spec_set': ['server_get', 'server_list'],
'server_get.return_value': instance,
'server_list.return_value': ([instance], False),
}),
mock.patch('tuskar_ui.api.node.glance', **{
'spec_set': ['image_get'],
'image_get.return_value': image,
}),
mock.patch('tuskar_ui.api.heat.Resource', **{
'spec_set': ['get_by_node', 'list_all_resources'],
'get_by_node.side_effect': (
self._raise_horizon_exception_not_found),
'list_all_resources.return_value': [],
}),
) as (mock_node, mock_role, mock_nova, mock_glance, mock_resource):
res = self.client.post(INDEX_URL + '?tab=nodes__all', data)
self.assertNoFormErrors(res)
self.assertEqual(mock_node.set_power_state.call_count, 1)
self.assertRedirectsNoFollow(res, INDEX_URL + '?tab=nodes__all')
def test_node_set_power_on_empty(self):
all_nodes = [api.node.Node(api.node.Node(node))
for node in self.ironicclient_nodes.list()]
node = all_nodes[6]
roles = [api.tuskar.Role(r)
for r in TEST_DATA.tuskarclient_roles.list()]
instance = TEST_DATA.novaclient_servers.first()
image = TEST_DATA.glanceclient_images.first()
data = {
'action': 'all_nodes_table__set_power_state_on',
'object_ids': '',
}
with contextlib.nested(
mock.patch('tuskar_ui.api.node.Node', **{
'spec_set': ['list', 'set_power_state'],
'list.return_value': all_nodes,
'set_power_state.return_value': node,
}),
mock.patch('tuskar_ui.api.tuskar.Role', **{
'spec_set': ['list', 'name'],
'list.return_value': roles,
}),
mock.patch('tuskar_ui.api.node.nova', **{
'spec_set': ['server_get', 'server_list'],
'server_get.return_value': instance,
'server_list.return_value': ([instance], False),
}),
mock.patch('tuskar_ui.api.node.glance', **{
'spec_set': ['image_get'],
'image_get.return_value': image,
}),
mock.patch('tuskar_ui.api.heat.Resource', **{
'spec_set': ['get_by_node', 'list_all_resources'],
'get_by_node.side_effect': (
self._raise_horizon_exception_not_found),
'list_all_resources.return_value': [],
}),
) as (mock_node, mock_role, mock_nova, mock_glance, mock_resource):
res = self.client.post(INDEX_URL + '?tab=nodes__all', data)
self.assertEqual(mock_node.set_power_state.call_count, 0)
self.assertRedirectsNoFollow(res, INDEX_URL)
def test_node_set_power_off(self):
all_nodes = [api.node.Node(api.node.Node(node))
for node in self.ironicclient_nodes.list()]
node = all_nodes[8]
roles = [api.tuskar.Role(r)
for r in TEST_DATA.tuskarclient_roles.list()]
instance = TEST_DATA.novaclient_servers.first()
image = TEST_DATA.glanceclient_images.first()
data = {'action': "all_nodes_table__set_power_state_off__{0}".format(
node.uuid)}
with contextlib.nested(
mock.patch('tuskar_ui.api.node.Node', **{
'spec_set': ['list', 'set_power_state'],
'list.return_value': all_nodes,
'set_power_state.return_value': node,
}),
mock.patch('tuskar_ui.api.tuskar.Role', **{
'spec_set': ['list', 'name'],
'list.return_value': roles,
}),
mock.patch('tuskar_ui.api.node.nova', **{
'spec_set': ['server_get', 'server_list'],
'server_get.return_value': instance,
'server_list.return_value': ([instance], False),
}),
mock.patch('tuskar_ui.api.node.glance', **{
'spec_set': ['image_get'],
'image_get.return_value': image,
}),
mock.patch('tuskar_ui.api.heat.Resource', **{
'spec_set': ['get_by_node', 'list_all_resources'],
'get_by_node.side_effect': (
self._raise_horizon_exception_not_found),
'list_all_resources.return_value': [],
}),
) as (mock_node, mock_role, mock_nova, mock_glance, mock_resource):
res = self.client.post(INDEX_URL + '?tab=nodes__all', data)
self.assertNoFormErrors(res)
self.assertEqual(mock_node.set_power_state.call_count, 1)
self.assertRedirectsNoFollow(res,
INDEX_URL + '?tab=nodes__all')
def test_performance(self):
node = api.node.Node(self.ironicclient_nodes.list()[0])
instance = TEST_DATA.novaclient_servers.first()
ceilometerclient = self.stub_ceilometerclient()
ceilometerclient.resources = self.mox.CreateMockAnything()
ceilometerclient.meters = self.mox.CreateMockAnything()
self.mox.ReplayAll()
with contextlib.nested(
mock.patch('tuskar_ui.api.node.Node', **{
'spec_set': ['get'],
'get.return_value': node,
}),
mock.patch('tuskar_ui.api.node.nova', **{
'spec_set': ['servers', 'server_get', 'server_list'],
'servers.return_value': [instance],
'server_list.return_value': ([instance], None),
}),
mock.patch('tuskar_ui.utils.metering.query_data',
return_value=[]),
):
url = urlresolvers.reverse(PERFORMANCE_VIEW, args=(node.uuid,))
url += '?meter=cpu&date_options=7'
res = self.client.get(url)
json_content = json.loads(res.content)
self.assertEqual(res.status_code, 200)
self.assertIn('series', json_content)
self.assertIn('settings', json_content)
def test_get_driver_info_dict(self):
data = {
'driver': 'pxe_ipmitool',
'ipmi_address': '127.0.0.1',
'ipmi_username': 'root',
'ipmi_password': 'P@55W0rd',
'deployment_kernel': '7',
'deployment_ramdisk': '8',
}
ret = forms.get_driver_info_dict(data)
self.assertEqual(ret, {
'driver': 'pxe_ipmitool',
'ipmi_address': '127.0.0.1',
'ipmi_username': 'root',
'ipmi_password': 'P@55W0rd',
'deployment_kernel': '7',
'deployment_ramdisk': '8',
})
data = {
'driver': 'pxe_ssh',
'ssh_address': '127.0.0.1',
'ssh_username': 'root',
'ssh_key_contents': 'P@55W0rd',
'deployment_kernel': '7',
'deployment_ramdisk': '8',
}
ret = forms.get_driver_info_dict(data)
self.assertEqual(ret, {
'driver': 'pxe_ssh',
'ssh_address': '127.0.0.1',
'ssh_username': 'root',
'ssh_key_contents': 'P@55W0rd',
'deployment_kernel': '7',
'deployment_ramdisk': '8',
})
def test_create_node(self):
data = {
'ipmi_address': '127.0.0.1',
'cpu_arch': 'x86',
'cpus': 1,
'memory_mb': 2,
'local_gb': 3,
'mac_addresses': 'DE:AD:BE:EF:CA:FE',
'ipmi_username': 'username',
'ipmi_password': 'password',
'driver': 'pxe_ipmitool',
'deployment_kernel': '7',
'deployment_ramdisk': '8',
}
with mock.patch('tuskar_ui.api.node.Node', **{
'spec_set': ['create', 'set_maintenance', 'discover'],
'create.return_value': None,
}) as Node:
forms.create_node(None, data)
self.assertListEqual(Node.create.call_args_list, [
mock.call(
mock.ANY,
ipmi_address=u'127.0.0.1',
cpu_arch='x86',
cpus=1,
memory_mb=2,
local_gb=3,
mac_addresses=['DE:AD:BE:EF:CA:FE'],
ipmi_username=u'username',
ipmi_password=u'password',
driver='pxe_ipmitool',
deployment_kernel='7',
deployment_ramdisk='8',
),
])
def test_delete_deployed_on_servers(self):
all_nodes = [api.node.Node(node)
for node in self.ironicclient_nodes.list()]
node = all_nodes[6]
data = {'action': 'all_nodes_table__delete',
'object_ids': [node.uuid]}
with contextlib.nested(
mock.patch('tuskar_ui.api.node.Node', **{
'spec_set': [
'list',
'delete',
],
'list.return_value': [node],
'delete.side_effect': self._raise_ironic_exception,
}),
mock.patch('openstack_dashboard.api.nova.server_list',
return_value=([], False)),
):
res = self.client.post(INDEX_URL, data)
self.assertMessageCount(error=1, warning=0)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)

View File

@ -1,31 +0,0 @@
# -*- coding: utf8 -*-
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.conf import urls
from tuskar_ui.infrastructure.nodes import views
urlpatterns = urls.patterns(
'',
urls.url(r'^$', views.IndexView.as_view(), name='index'),
urls.url(r'^register/$', views.RegisterView.as_view(),
name='register'),
urls.url(r'^nodes_performance/$',
views.PerformanceView.as_view(), name='nodes_performance'),
urls.url(r'^(?P<node_uuid>[^/]+)/$', views.DetailView.as_view(),
name='node_detail'),
urls.url(r'^(?P<node_uuid>[^/]+)/performance/$',
views.PerformanceView.as_view(), name='performance'),
)

View File

@ -1,199 +0,0 @@
# -*- coding: utf8 -*-
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
from django.core.urlresolvers import reverse
from django.core.urlresolvers import reverse_lazy
import django.forms
import django.http
from django.utils.translation import ugettext_lazy as _
from django.views.generic import base
from horizon import exceptions
from horizon import forms as horizon_forms
from horizon import tabs as horizon_tabs
from horizon.utils import memoized
from openstack_dashboard.api import glance
from tuskar_ui import api
from tuskar_ui.infrastructure.nodes import forms
from tuskar_ui.infrastructure.nodes import tables
from tuskar_ui.infrastructure.nodes import tabs
import tuskar_ui.infrastructure.views as infrastructure_views
from tuskar_ui.utils import metering as metering_utils
def get_kernel_images(request):
try:
kernel_images = glance.image_list_detailed(
request, filters={'disk_format': 'aki'})[0]
except Exception:
exceptions.handle(request, _('Unable to retrieve kernel image list.'))
kernel_images = []
return kernel_images
def get_ramdisk_images(request):
try:
ramdisk_images = glance.image_list_detailed(
request, filters={'disk_format': 'ari'})[0]
except Exception:
exceptions.handle(request, _('Unable to retrieve ramdisk image list.'))
ramdisk_images = []
return ramdisk_images
class IndexView(infrastructure_views.ItemCountMixin,
horizon_tabs.TabbedTableView):
tab_group_class = tabs.NodeTabs
template_name = 'infrastructure/nodes/index.html'
def get_context_data(self, **kwargs):
context = super(IndexView, self).get_context_data(**kwargs)
register_action = {
'name': _('Register Nodes'),
'url': reverse('horizon:infrastructure:nodes:register'),
'icon': 'fa-plus',
'ajax_modal': True,
}
context['header_actions'] = [register_action]
return context
@memoized.memoized_method
def get_data(self):
return api.node.Node.list(self.request)
def get_tabs(self, request, **kwargs):
nodes = self.get_data()
return self.tab_group_class(request, nodes=nodes, **kwargs)
class RegisterView(horizon_forms.ModalFormView):
form_class = forms.RegisterNodeFormset
form_prefix = 'register_nodes'
template_name = 'infrastructure/nodes/register.html'
success_url = reverse_lazy('horizon:infrastructure:nodes:index')
submit_label = _("Register Nodes")
def get_data(self):
return []
def get_form(self, form_class):
initial = []
if self.request.FILES:
csv_form = forms.UploadNodeForm(self.request,
data=self.request.POST,
files=self.request.FILES)
if csv_form.is_valid():
initial = csv_form.get_data()
formset = forms.RegisterNodeFormset(
self.request.POST,
prefix=self.form_prefix,
request=self.request,
kernel_images=get_kernel_images(self.request),
ramdisk_images=get_ramdisk_images(self.request)
)
if formset.is_valid():
initial += formset.cleaned_data
formset = forms.RegisterNodeFormset(
None,
initial=initial,
prefix=self.form_prefix,
request=self.request,
kernel_images=get_kernel_images(self.request),
ramdisk_images=get_ramdisk_images(self.request)
)
formset.extra = 0
return formset
return forms.RegisterNodeFormset(
self.request.POST or None,
initial=initial,
prefix=self.form_prefix,
request=self.request,
kernel_images=get_kernel_images(self.request),
ramdisk_images=get_ramdisk_images(self.request)
)
def get_context_data(self, **kwargs):
context = super(RegisterView, self).get_context_data(**kwargs)
context['upload_form'] = forms.UploadNodeForm(self.request)
return context
class DetailView(horizon_tabs.TabView):
tab_group_class = tabs.NodeDetailTabs
template_name = 'infrastructure/nodes/detail.html'
def get_context_data(self, **kwargs):
context = super(DetailView, self).get_context_data(**kwargs)
node = self.get_data()
if node.maintenance:
table = tables.MaintenanceNodesTable(self.request)
else:
table = tables.ProvisionedNodesTable(self.request)
context['node'] = node
context['title'] = _("Node: %(uuid)s") % {'uuid': node.uuid}
context['url'] = self.get_redirect_url()
context['actions'] = table.render_row_actions(node)
return context
@memoized.memoized_method
def get_data(self):
node_uuid = self.kwargs.get('node_uuid')
node = api.node.Node.get(self.request, node_uuid,
_error_redirect=self.get_redirect_url())
return node
def get_tabs(self, request, **kwargs):
node = self.get_data()
return self.tab_group_class(self.request, node=node, **kwargs)
@staticmethod
def get_redirect_url():
return reverse_lazy('horizon:infrastructure:nodes:index')
class PerformanceView(base.TemplateView):
def get(self, request, *args, **kwargs):
meter = request.GET.get('meter')
date_options = request.GET.get('date_options')
date_from = request.GET.get('date_from')
date_to = request.GET.get('date_to')
stats_attr = request.GET.get('stats_attr', 'avg')
barchart = bool(request.GET.get('barchart'))
node_uuid = kwargs.get('node_uuid', None)
if node_uuid:
node = api.node.Node.get(request, node_uuid)
instance_uuids = [node.instance_uuid]
else:
# Aggregated stats for all nodes
instance_uuids = []
json_output = metering_utils.get_nodes_stats(
request=request,
node_uuid=node_uuid,
instance_uuids=instance_uuids,
meter=meter,
date_options=date_options,
date_from=date_from,
date_to=date_to,
stats_attr=stats_attr,
barchart=barchart)
return django.http.HttpResponse(
json.dumps(json_output), content_type='application/json')

View File

@ -1,488 +0,0 @@
# -*- coding: utf8 -*-
#
# 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
import six
import uuid
from django.conf import settings
import django.forms
from django.utils.translation import ugettext_lazy as _
import horizon.exceptions
import horizon.forms
import horizon.messages
from os_cloud_config import keystone as keystone_config
from os_cloud_config.utils import clients
from tuskar_ui import api
import tuskar_ui.api.heat
import tuskar_ui.api.tuskar
import tuskar_ui.forms
import tuskar_ui.infrastructure.flavors.utils as flavors_utils
import tuskar_ui.utils.utils as tuskar_utils
MATCHING_DEPLOYMENT_MODE = flavors_utils.matching_deployment_mode()
LOG = logging.getLogger(__name__)
MESSAGE_ICONS = {
'ok': 'fa-check-square-o text-success',
'pending': 'fa-square-o text-info',
'error': 'fa-exclamation-circle text-danger',
'warning': 'fa-exclamation-triangle text-warning',
None: 'fa-exclamation-triangle text-warning',
}
WEBROOT = getattr(settings, 'WEBROOT', '/')
def validate_roles(request, plan):
"""Validates the roles in plan and returns dict describing the issues"""
for role in plan.role_list:
if (
plan.get_role_node_count(role) and
not role.is_valid_for_deployment(plan)
):
message = {
'text': _(u"Configure Roles."),
'is_critical': True,
'status': 'pending',
}
break
else:
message = {
'text': _(u"Configure Roles."),
'status': 'ok',
}
return message
def validate_global_parameters(request, plan):
pending_required_global_params = list(
api.tuskar.Parameter.pending_parameters(
api.tuskar.Parameter.required_parameters(
api.tuskar.Parameter.global_parameters(
plan.parameter_list()))))
if pending_required_global_params:
message = {
'text': _(u"Global Service Configuration."),
'is_critical': True,
'status': 'pending',
}
else:
message = {
'text': _(u"Global Service Configuration."),
'status': 'ok',
}
return message
def validate_plan(request, plan):
"""Validates the plan and returns a list of dicts describing the issues."""
messages = []
requested_nodes = 0
for role in plan.role_list:
node_count = plan.get_role_node_count(role)
requested_nodes += node_count
available_flavors = len(api.flavor.Flavor.list(request))
if available_flavors == 0:
messages.append({
'text': _(u"Define Flavors."),
'is_critical': True,
'status': 'pending',
})
else:
messages.append({
'text': _(u"Define Flavors."),
'status': 'ok',
})
available_nodes = len(api.node.Node.list(request, associated=False,
maintenance=False))
if available_nodes == 0:
messages.append({
'text': _(u"Register Nodes."),
'is_critical': True,
'status': 'pending',
})
elif requested_nodes > available_nodes:
messages.append({
'text': _(u"Not enough registered nodes for this plan. "
u"You need {0} more.").format(
requested_nodes - available_nodes),
'is_critical': True,
'status': 'error',
})
else:
messages.append({
'text': _(u"Register Nodes."),
'status': 'ok',
})
messages.append(validate_roles(request, plan))
messages.append(validate_global_parameters(request, plan))
if not MATCHING_DEPLOYMENT_MODE:
# All roles have to have the same flavor.
default_flavor_name = api.flavor.Flavor.list(request)[0].name
for role in plan.role_list:
if role.flavor(plan).name != default_flavor_name:
messages.append({
'text': _(u"Role {0} doesn't use default flavor.").format(
role.name,
),
'is_critical': False,
'statis': 'error',
})
roles_assigned = True
messages.append({
'text': _(u"Assign roles."),
'status': lambda: 'ok' if roles_assigned else 'pending',
})
try:
controller_role = plan.get_role_by_name("Controller")
except KeyError:
messages.append({
'text': _(u"Controller Role Needed."),
'is_critical': True,
'status': 'error',
'indent': 1,
})
roles_assigned = False
else:
if plan.get_role_node_count(controller_role) not in (1, 3):
messages.append({
'text': _(u"1 or 3 Controllers Needed."),
'is_critical': True,
'status': 'pending',
'indent': 1,
})
roles_assigned = False
else:
messages.append({
'text': _(u"1 or 3 Controllers Needed."),
'status': 'ok',
'indent': 1,
})
try:
compute_role = plan.get_role_by_name("Compute")
except KeyError:
messages.append({
'text': _(u"Compute Role Needed."),
'is_critical': True,
'status': 'error',
'indent': 1,
})
roles_assigned = False
else:
if plan.get_role_node_count(compute_role) < 1:
messages.append({
'text': _(u"1 Compute Needed."),
'is_critical': True,
'status': 'pending',
'indent': 1,
})
roles_assigned = False
else:
messages.append({
'text': _(u"1 Compute Needed."),
'status': 'ok',
'indent': 1,
})
for message in messages:
status = message.get('status')
if callable(status):
message['status'] = status = status()
message['classes'] = MESSAGE_ICONS.get(status, MESSAGE_ICONS[None])
return messages
class EditPlan(horizon.forms.SelfHandlingForm):
def __init__(self, *args, **kwargs):
super(EditPlan, self).__init__(*args, **kwargs)
self.plan = api.tuskar.Plan.get_the_plan(self.request)
self.fields.update(self._role_count_fields(self.plan))
def _role_count_fields(self, plan):
fields = {}
for role in plan.role_list:
field = django.forms.IntegerField(
label=role.name,
widget=tuskar_ui.forms.NumberPickerInput(attrs={
'min': 1 if role.name in ('Controller', 'Compute') else 0,
'step': 2 if role.name == 'Controller' else 1,
}),
initial=plan.get_role_node_count(role),
required=False
)
field.role = role
fields['%s-count' % role.id] = field
return fields
def handle(self, request, data):
parameters = dict(
(field.role.node_count_parameter_name, data[name])
for (name, field) in self.fields.items() if name.endswith('-count')
)
# NOTE(gfidente): this is a bad hack meant to magically add the
# parameter which enables Neutron L3 HA when the number of
# Controllers is > 1
try:
controller_role = self.plan.get_role_by_name('Controller')
compute_role = self.plan.get_role_by_name('Compute')
except Exception as e:
LOG.warning('Unable to find a required role: %s', e.message)
else:
number_controllers = parameters[
controller_role.node_count_parameter_name]
if number_controllers > 1:
for role in [controller_role, compute_role]:
l3ha_param = role.parameter_prefix + 'NeutronL3HA'
parameters[l3ha_param] = 'True'
l3agent_param = (role.parameter_prefix +
'NeutronAllowL3AgentFailover')
parameters[l3agent_param] = 'True'
dhcp_agents_per_net = (number_controllers if number_controllers and
number_controllers > 3 else 3)
dhcp_agents_param = (controller_role.parameter_prefix +
'NeutronDhcpAgentsPerNetwork')
parameters[dhcp_agents_param] = dhcp_agents_per_net
try:
ceph_storage_role = self.plan.get_role_by_name('Ceph-Storage')
except Exception as e:
LOG.warning('Unable to find role: %s', 'Ceph-Storage')
else:
if parameters[ceph_storage_role.node_count_parameter_name] > 0:
parameters.update({
'CephClusterFSID': six.text_type(uuid.uuid4()),
'CephMonKey': tuskar_utils.create_cephx_key(),
'CephAdminKey': tuskar_utils.create_cephx_key()
})
cinder_enable_rbd_param = (controller_role.parameter_prefix
+ 'CinderEnableRbdBackend')
glance_backend_param = (controller_role.parameter_prefix +
'GlanceBackend')
nova_enable_rbd_param = (compute_role.parameter_prefix +
'NovaEnableRbdBackend')
cinder_enable_iscsi_param = (
controller_role.parameter_prefix +
'CinderEnableIscsiBackend')
parameters.update({
cinder_enable_rbd_param: True,
glance_backend_param: 'rbd',
nova_enable_rbd_param: True,
cinder_enable_iscsi_param: False
})
try:
self.plan = self.plan.patch(request, self.plan.uuid, parameters)
except Exception as e:
horizon.exceptions.handle(request, _("Unable to update the plan."))
LOG.exception(e)
return False
return True
class ScaleOut(EditPlan):
def __init__(self, *args, **kwargs):
super(ScaleOut, self).__init__(*args, **kwargs)
for name, field in self.fields.items():
if name.endswith('-count'):
field.widget.attrs['min'] = field.initial
def handle(self, request, data):
if not super(ScaleOut, self).handle(request, data):
return False
plan = self.plan
try:
stack = api.heat.Stack.get_by_plan(self.request, plan)
stack.update(request, plan.name, plan.templates)
except Exception as e:
LOG.exception(e)
if hasattr(e, 'error'):
horizon.exceptions.handle(
request,
_(
"Unable to deploy overcloud. Reason: {0}"
).format(e.error['error']['message']),
)
return False
else:
raise
else:
msg = _('Deployment in progress.')
horizon.messages.success(request, msg)
return True
class DeployOvercloud(horizon.forms.SelfHandlingForm):
network_isolation = horizon.forms.BooleanField(
label=_("Enable Network Isolation"),
required=False)
def handle(self, request, data):
try:
plan = api.tuskar.Plan.get_the_plan(request)
except Exception as e:
LOG.exception(e)
horizon.exceptions.handle(request,
_("Unable to deploy overcloud."))
return False
# If network isolation selected, read environment file data
# and add to plan
env_temp = '/usr/share/openstack-tripleo-heat-templates/environments'
try:
if self.cleaned_data['network_isolation']:
with open(env_temp, 'r') as env_file:
env_contents = ''.join(
[line for line in
env_file.readlines() if '#' not in line]
)
plan.environment += env_contents
except Exception as e:
LOG.exception(e)
pass
# Auto-generate missing passwords and certificates
if plan.list_generated_parameters():
generated_params = plan.make_generated_parameters()
plan = plan.patch(request, plan.uuid, generated_params)
# Validate plan and create stack
for message in validate_plan(request, plan):
if message.get('is_critical'):
horizon.messages.success(request, message.text)
return False
try:
stack = api.heat.Stack.get_by_plan(self.request, plan)
if not stack:
api.heat.Stack.create(request, plan.name, plan.templates)
except Exception as e:
LOG.exception(e)
horizon.exceptions.handle(
request, _("Unable to deploy overcloud. Reason: {0}").format(
e.error['error']['message']))
return False
else:
msg = _('Deployment in progress.')
horizon.messages.success(request, msg)
return True
class UndeployOvercloud(horizon.forms.SelfHandlingForm):
def handle(self, request, data):
try:
plan = api.tuskar.Plan.get_the_plan(request)
stack = api.heat.Stack.get_by_plan(self.request, plan)
if stack:
api.heat.Stack.delete(request, stack.id)
except Exception as e:
LOG.exception(e)
horizon.exceptions.handle(request,
_("Unable to undeploy overcloud."))
return False
else:
msg = _('Undeployment in progress.')
horizon.messages.success(request, msg)
return True
class PostDeployInit(horizon.forms.SelfHandlingForm):
admin_email = horizon.forms.CharField(label=_("Admin Email"))
public_host = horizon.forms.CharField(
label=_("Public Host"), initial="", required=False)
region = horizon.forms.CharField(
label=_("Region"), initial="regionOne")
def build_endpoints(self, plan, controller_role):
return {
"ceilometer": {
"password": plan.parameter_value(
controller_role.parameter_prefix + 'CeilometerPassword')},
"cinder": {
"password": plan.parameter_value(
controller_role.parameter_prefix + 'CinderPassword')},
"cinderv2": {
"password": plan.parameter_value(
controller_role.parameter_prefix + 'CinderPassword')},
"ec2": {
"password": plan.parameter_value(
controller_role.parameter_prefix + 'GlancePassword')},
"glance": {
"password": plan.parameter_value(
controller_role.parameter_prefix + 'GlancePassword')},
"heat": {
"password": plan.parameter_value(
controller_role.parameter_prefix + 'HeatPassword')},
"neutron": {
"password": plan.parameter_value(
controller_role.parameter_prefix + 'NeutronPassword')},
"nova": {
"password": plan.parameter_value(
controller_role.parameter_prefix + 'NovaPassword')},
"novav3": {
"password": plan.parameter_value(
controller_role.parameter_prefix + 'NovaPassword')},
"swift": {
"password": plan.parameter_value(
controller_role.parameter_prefix + 'SwiftPassword'),
'path': '/v1/AUTH_%(tenant_id)s',
'admin_path': '/v1'},
"horizon": {
'port': '80',
'path': WEBROOT,
'admin_path': '%sadmin' % WEBROOT}}
def handle(self, request, data):
try:
plan = api.tuskar.Plan.get_the_plan(request)
controller_role = plan.get_role_by_name("Controller")
stack = api.heat.Stack.get_by_plan(self.request, plan)
admin_token = plan.parameter_value(
controller_role.parameter_prefix + 'AdminToken')
admin_password = plan.parameter_value(
controller_role.parameter_prefix + 'AdminPassword')
admin_email = data['admin_email']
auth_ip = stack.keystone_ip
auth_url = stack.keystone_auth_url
auth_tenant = 'admin'
auth_user = 'admin'
# do the keystone init
keystone_config.initialize(
auth_ip, admin_token, admin_email, admin_password,
region='regionOne', ssl=None, public=None, user='heat-admin',
pki_setup=False)
# retrieve needed Overcloud clients
keystone_client = clients.get_keystone_client(
auth_user, admin_password, auth_tenant, auth_url)
# do the setup endpoints
keystone_config.setup_endpoints(
self.build_endpoints(plan, controller_role),
public_host=data['public_host'],
region=data['region'],
os_auth_url=auth_url,
client=keystone_client)
except Exception as e:
LOG.exception(e)
horizon.exceptions.handle(request,
_("Unable to initialize Overcloud."))
return False
else:
msg = _('Overcloud has been initialized.')
horizon.messages.success(request, msg)
return True

View File

@ -1,26 +0,0 @@
# -*- coding: utf8 -*-
#
# 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 tuskar_ui.infrastructure import dashboard
class Overview(horizon.Panel):
name = _("Overview")
slug = "overview"
dashboard.Infrastructure.register(Overview)

View File

@ -1,364 +0,0 @@
# -*- coding: utf8 -*-
#
# 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 contextlib
from django.core import urlresolvers
from mock import patch, call # noqa
from openstack_dashboard.test.test_data import utils
from tuskar_ui import api
from tuskar_ui.infrastructure.overview import forms
from tuskar_ui.infrastructure.overview import views
from tuskar_ui.test import helpers as test
from tuskar_ui.test.test_data import heat_data
from tuskar_ui.test.test_data import tuskar_data
INDEX_URL = urlresolvers.reverse(
'horizon:infrastructure:overview:index')
DEPLOY_URL = urlresolvers.reverse(
'horizon:infrastructure:overview:deploy_confirmation')
DELETE_URL = urlresolvers.reverse(
'horizon:infrastructure:overview:undeploy_confirmation')
POST_DEPLOY_INIT_URL = urlresolvers.reverse(
'horizon:infrastructure:overview:post_deploy_init')
TEST_DATA = utils.TestDataContainer()
heat_data.data(TEST_DATA)
tuskar_data.data(TEST_DATA)
@contextlib.contextmanager
def _mock_plan(**kwargs):
plan = None
params = {
'spec_set': [
'create',
'delete',
'get',
'get_the_plan',
'id',
'uuid',
'patch',
'parameters',
'role_list',
'parameter_value',
'get_role_by_name',
'get_role_node_count',
'list_generated_parameters',
'make_generated_parameters',
'parameter_list',
],
'create.side_effect': lambda *args, **kwargs: plan,
'delete.return_value': None,
'get.side_effect': lambda *args, **kwargs: plan,
'get_the_plan.side_effect': lambda *args, **kwargs: plan,
'id': 'plan-1',
'uuid': 'plan-1',
'patch.side_effect': lambda *args, **kwargs: plan,
'role_list': [],
'parameter_list.return_value': [],
'parameter_value.return_value': None,
'get_role_by_name.side_effect': KeyError,
'get_role_node_count.return_value': 0,
'list_generated_parameters.return_value': {},
'make_generated_parameters.return_value': {},
}
params.update(kwargs)
with patch(
'tuskar_ui.api.tuskar.Plan', **params) as Plan:
plan = Plan
yield Plan
class OverviewTests(test.BaseAdminViewTests):
def test_index_stack_not_created(self):
with contextlib.nested(
_mock_plan(),
patch('tuskar_ui.api.heat.Stack.list', return_value=[]),
patch('tuskar_ui.api.node.Node.list', return_value=[]),
patch('tuskar_ui.api.flavor.Flavor.list', return_value=[]),
):
res = self.client.get(INDEX_URL)
get_the_plan = api.tuskar.Plan.get_the_plan
request = get_the_plan.call_args_list[0][0][0]
self.assertListEqual(get_the_plan.call_args_list, [
call(request),
call(request),
call(request),
])
self.assertListEqual(api.heat.Stack.list.call_args_list, [
call(request),
])
self.assertListEqual(api.node.Node.list.call_args_list, [
call(request, associated=False, maintenance=False),
])
self.assertListEqual(api.flavor.Flavor.list.call_args_list, [
call(request),
])
self.assertTemplateUsed(
res, 'infrastructure/overview/index.html')
self.assertTemplateUsed(
res, 'infrastructure/overview/role_nodes_edit.html')
def test_index_stack_not_created_post(self):
with contextlib.nested(
_mock_plan(),
patch('tuskar_ui.api.heat.Stack.list', return_value=[]),
patch('tuskar_ui.api.node.Node.list', return_value=[]),
patch('tuskar_ui.api.flavor.Flavor.list', return_value=[]),
) as (plan, _stack_list, _node_list, _flavor_list):
data = {
'role-1-count': 1,
'role-2-count': 0,
'role-3-count': 0,
'role-4-count': 0,
}
res = self.client.post(INDEX_URL, data)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
get_the_plan = api.tuskar.Plan.get_the_plan
request = get_the_plan.call_args_list[0][0][0]
self.assertListEqual(get_the_plan.call_args_list, [
call(request),
])
self.assertListEqual(
api.tuskar.Plan.patch.call_args_list,
[call(request, plan.id, {})],
)
def test_index_stack_deployed(self):
stack = api.heat.Stack(TEST_DATA.heatclient_stacks.first())
roles = [api.tuskar.Role(role)
for role in self.tuskarclient_roles.list()]
with contextlib.nested(
_mock_plan(**{'get_role_by_name.side_effect': None,
'get_role_by_name.return_value': roles[0]}),
patch('tuskar_ui.api.heat.Stack.get_by_plan',
return_value=stack),
patch('tuskar_ui.api.heat.Stack.events',
return_value=[]),
) as (Plan, stack_get_mock, stack_events_mock):
res = self.client.get(INDEX_URL)
request = Plan.get_the_plan.call_args_list[0][0][0]
self.assertListEqual(
Plan.get_the_plan.call_args_list,
[
call(request),
call(request),
call(request),
])
self.assertTemplateUsed(
res, 'infrastructure/overview/index.html')
self.assertTemplateUsed(
res, 'infrastructure/overview/deployment_live.html')
def test_index_stack_undeploy_in_progress(self):
stack = api.heat.Stack(TEST_DATA.heatclient_stacks.first())
with contextlib.nested(
_mock_plan(),
patch('tuskar_ui.api.heat.Stack.get_by_plan',
return_value=stack),
patch('tuskar_ui.api.heat.Stack.is_deleting',
return_value=True),
patch('tuskar_ui.api.heat.Stack.is_deployed',
return_value=False),
patch('tuskar_ui.api.heat.Stack.resources',
return_value=[]),
patch('tuskar_ui.api.heat.Stack.events',
return_value=[]),
):
res = self.client.get(INDEX_URL)
self.assertTemplateUsed(
res, 'infrastructure/overview/index.html')
self.assertTemplateUsed(
res, 'infrastructure/overview/deployment_progress.html')
def test_deploy_get(self):
with _mock_plan():
res = self.client.get(DEPLOY_URL)
self.assertTemplateUsed(
res, 'infrastructure/overview/deploy_confirmation.html')
def test_delete_get(self):
stack = api.heat.Stack(TEST_DATA.heatclient_stacks.first())
with contextlib.nested(
_mock_plan(),
patch('tuskar_ui.api.heat.Stack.get_by_plan',
return_value=stack),
):
res = self.client.get(DELETE_URL)
self.assertTemplateUsed(
res, 'infrastructure/overview/undeploy_confirmation.html')
def test_delete_post(self):
stack = api.heat.Stack(TEST_DATA.heatclient_stacks.first())
with contextlib.nested(
_mock_plan(),
patch('tuskar_ui.api.heat.Stack.get_by_plan',
return_value=stack),
patch('tuskar_ui.api.heat.Stack.delete',
return_value=None),
):
res = self.client.post(DELETE_URL)
self.assertRedirectsNoFollow(res, INDEX_URL)
def test_post_deploy_init_get(self):
stack = api.heat.Stack(TEST_DATA.heatclient_stacks.first())
with contextlib.nested(
_mock_plan(),
patch('tuskar_ui.api.heat.Stack.get_by_plan',
return_value=stack),
):
res = self.client.get(POST_DEPLOY_INIT_URL)
self.assertEqual(res.context['form']['admin_email'].value(), '')
self.assertTemplateUsed(
res, 'infrastructure/overview/post_deploy_init.html')
def test_post_deploy_init_post(self):
stack = api.heat.Stack(TEST_DATA.heatclient_stacks.first())
roles = [api.tuskar.Role(role)
for role in self.tuskarclient_roles.list()]
data = {
'admin_email': "example@example.org",
'public_host': '',
'region': 'regionOne',
}
with contextlib.nested(
_mock_plan(**{'get_role_by_name.side_effect': None,
'get_role_by_name.return_value': roles[0]}),
patch('tuskar_ui.api.heat.Stack.get_by_plan',
return_value=stack),
patch('os_cloud_config.keystone.initialize',
return_value=None),
patch('os_cloud_config.keystone.setup_endpoints',
return_value=None),
patch('os_cloud_config.utils.clients.get_keystone_client',
return_value='keystone_client'),
) as (mock_plan, mock_get_by_plan, mock_initialize,
mock_setup_endpoints, mock_get_keystone_client):
res = self.client.post(POST_DEPLOY_INIT_URL, data)
self.assertNoFormErrors(res)
self.assertEqual(res.status_code, 302)
self.assertRedirectsNoFollow(res, INDEX_URL)
mock_initialize.assert_called_once_with(
'192.0.2.23', None, 'example@example.org', None, ssl=None,
region='regionOne', user='heat-admin', public=None,
pki_setup=False)
mock_setup_endpoints.assert_called_once_with(
{'nova': {'password': None},
'heat': {'password': None},
'ceilometer': {'password': None},
'ec2': {'password': None},
"horizon": {
'port': '80',
'path': '/',
'admin_path': '/admin'},
'cinder': {'password': None},
'cinderv2': {'password': None},
'glance': {'password': None},
'swift': {'password': None,
'path': '/v1/AUTH_%(tenant_id)s',
'admin_path': '/v1'},
'novav3': {'password': None},
'neutron': {'password': None}},
os_auth_url=stack.keystone_auth_url,
client='keystone_client',
region='regionOne',
public_host='')
mock_get_keystone_client.assert_called_once_with(
'admin', None, 'admin', stack.keystone_auth_url)
def test_get_role_data(self):
plan = api.tuskar.Plan(self.tuskarclient_plans.first())
stack = api.heat.Stack(self.heatclient_stacks.first())
role = api.tuskar.Role(self.tuskarclient_roles.first())
stack.resources = lambda *args, **kwargs: []
ret = views._get_role_data(plan, stack, None, role)
self.assertEqual(ret, {
'deployed_node_count': 0,
'deploying_node_count': 0,
'error_node_count': 0,
'field': '',
'finished': False,
'icon': 'fa-exclamation',
'id': 'role-1',
'name': 'Controller',
'planned_node_count': 1,
'role': role,
'status': 'warning',
'total_node_count': 0,
'waiting_node_count': 0,
})
def test_validate_plan_empty(self):
with (
_mock_plan()
) as plan, (
patch('tuskar_ui.api.node.Node.list', return_value=[])
), (
patch('tuskar_ui.api.flavor.Flavor.list', return_value=[])
):
ret = forms.validate_plan(None, plan)
for m in ret:
m['text'] = unicode(m['text'])
self.assertEqual(ret, [
{
'is_critical': True,
'text': u'Define Flavors.',
'status': 'pending',
'classes': 'fa-square-o text-info',
}, {
'is_critical': True,
'text': u'Register Nodes.',
'status': 'pending',
'classes': 'fa-square-o text-info',
}, {
'status': 'ok',
'text': u'Configure Roles.',
'classes': 'fa-check-square-o text-success',
}, {
'status': 'ok',
'text': u'Global Service Configuration.',
'classes': 'fa-check-square-o text-success',
}, {
'status': 'pending',
'text': u'Assign roles.',
'classes': 'fa-square-o text-info',
}, {
'is_critical': True,
'text': u'Controller Role Needed.',
'status': 'error',
'indent': 1,
'classes': 'fa-exclamation-circle text-danger',
}, {
'is_critical': True,
'text': u'Compute Role Needed.',
'status': 'error',
'indent': 1,
'classes': 'fa-exclamation-circle text-danger',
},
])

View File

@ -1,38 +0,0 @@
# -*- coding: utf8 -*-
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.conf import urls
from tuskar_ui.infrastructure.overview import views
urlpatterns = urls.patterns(
'',
urls.url(r'^$', views.IndexView.as_view(), name='index'),
urls.url(r'^deploy-confirmation$',
views.DeployConfirmationView.as_view(),
name='deploy_confirmation'),
urls.url(r'^undeploy-confirmation$',
views.UndeployConfirmationView.as_view(),
name='undeploy_confirmation'),
urls.url(r'^post-deploy-init$',
views.PostDeployInitView.as_view(),
name='post_deploy_init'),
urls.url(r'^scale-out$',
views.ScaleOutView.as_view(),
name='scale_out'),
urls.url(r'^download-overcloudrc$',
views.download_overcloudrc_file,
name='download_overcloudrc'),
)

View File

@ -1,390 +0,0 @@
# -*- coding: utf8 -*-
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
import logging
import urlparse
from django.core.urlresolvers import reverse
from django.core.urlresolvers import reverse_lazy
from django import http
from django import shortcuts
import django.utils.text
from django.utils.translation import ugettext_lazy as _
import heatclient
import horizon.forms
from horizon import messages
from tuskar_ui import api
from tuskar_ui.infrastructure.overview import forms
from tuskar_ui.infrastructure import views
INDEX_URL = 'horizon:infrastructure:overview:index'
LOG = logging.getLogger(__name__)
def _steps_message(messages):
total_steps = len(messages)
completed_steps = len([m for m in messages if not m.get('is_critical')])
return _("{0} of {1} Steps Completed").format(completed_steps, total_steps)
def _get_role_data(plan, stack, form, role):
"""Gathers data about a single deployment role.
Gathers data about a single deployment role from the related Overcloud
and Role objects, and presents it in the form convenient for use
from the template.
"""
data = {
'id': role.id,
'role': role,
'name': role.name,
'planned_node_count': plan.get_role_node_count(role),
'field': form['%s-count' % role.id] if form else '',
}
if stack:
resources = stack.resources(role=role, with_joins=True)
nodes = [r.node for r in resources]
node_count = len(nodes)
deployed_node_count = 0
deploying_node_count = 0
error_node_count = 0
waiting_node_count = node_count
status = 'warning'
if nodes:
deployed_node_count = sum(1 for node in nodes
if node.instance.status == 'ACTIVE')
deploying_node_count = sum(1 for node in nodes
if node.instance.status == 'BUILD')
error_node_count = sum(1 for node in nodes
if node.instance.status == 'ERROR')
waiting_node_count = (node_count - deployed_node_count -
deploying_node_count - error_node_count)
if error_node_count or 'FAILED' in stack.stack_status:
status = 'danger'
elif deployed_node_count == data['planned_node_count']:
status = 'success'
else:
status = 'info'
finished = deployed_node_count == data['planned_node_count']
if finished:
icon = 'fa-check'
elif status in ('danger', 'warning'):
icon = 'fa-exclamation'
else:
icon = 'fa-spinner fa-spin'
data.update({
'status': status,
'finished': finished,
'total_node_count': node_count,
'deployed_node_count': deployed_node_count,
'deploying_node_count': deploying_node_count,
'waiting_node_count': waiting_node_count,
'error_node_count': error_node_count,
'icon': icon,
})
# TODO(rdopieralski) get this from ceilometer
# data['capacity'] = 20
return data
class IndexView(horizon.forms.ModalFormView, views.StackMixin):
template_name = 'infrastructure/overview/index.html'
form_class = forms.EditPlan
success_url = reverse_lazy(INDEX_URL)
def get_progress_update(self, request, data):
return {
'progress': data.get('progress'),
'show_last_events': data.get('show_last_events'),
'last_events_title': unicode(data.get('last_events_title')),
'last_events': [{
'event_time': event.event_time,
'resource_name': event.resource_name,
'resource_status': event.resource_status,
'resource_status_reason': event.resource_status_reason,
} for event in data.get('last_events', [])],
'roles': [{
'status': role.get('status', 'warning'),
'finished': role.get('finished', False),
'name': role.get('name', ''),
'slug': django.utils.text.slugify(role.get('name', '')),
'id': role.get('id', ''),
'total_node_count': role.get('node_count', 0),
'deployed_node_count': role.get('deployed_node_count', 0),
'deploying_node_count': role.get('deploying_node_count', 0),
'waiting_node_count': role.get('waiting_node_count', 0),
'error_node_count': role.get('error_node_count', 0),
'planned_node_count': role.get('planned_node_count', 0),
'icon': role.get('icon', ''),
} for role in data.get('roles', [])],
}
def get(self, request, *args, **kwargs):
if request.META.get('HTTP_X_HORIZON_PROGRESS', ''):
# If it's an AJAX call for progress update, send it.
data = self.get_data(request, {})
return http.HttpResponse(
json.dumps(self.get_progress_update(request, data)),
content_type='application/json',
)
return super(IndexView, self).get(request, *args, **kwargs)
def get_form(self, form_class):
return form_class(self.request, **self.get_form_kwargs())
def get_context_data(self, *args, **kwargs):
context = super(IndexView, self).get_context_data(*args, **kwargs)
context.update(self.get_data(self.request, context))
return context
def get_data(self, request, context, *args, **kwargs):
plan = api.tuskar.Plan.get_the_plan(request)
stack = self.get_stack()
form = context.get('form')
context['plan'] = plan
context['stack'] = stack
roles = [_get_role_data(plan, stack, form, role)
for role in plan.role_list]
context['roles'] = roles
if stack:
context['show_last_events'] = True
failed_events = [e for e in stack.events
if 'FAILED' in e.resource_status and
'aborted' not in e.resource_status_reason][-3:]
if failed_events:
context['last_events_title'] = _('Last failed events')
context['last_events'] = failed_events
else:
context['last_events_title'] = _('Last event')
context['last_events'] = [stack.events[0]]
if stack.is_deleting or stack.is_delete_failed:
# TODO(lsmola) since at this point we don't have total number
# of nodes we will hack this around, till API can show this
# information. So it will actually show progress like the total
# number is 10, or it will show progress of 5%. Ugly, but
# workable.
total_num_nodes_count = 10
try:
resources_count = len(
stack.resources(with_joins=False))
except heatclient.exc.HTTPNotFound:
# Immediately after undeploying has started, heat returns
# this exception so we can take it as kind of init of
# undeploying.
resources_count = total_num_nodes_count
# TODO(lsmola) same as hack above
total_num_nodes_count = max(
resources_count, total_num_nodes_count)
context['progress'] = min(95, max(
5, 100 * float(resources_count) / total_num_nodes_count))
elif stack.is_deploying or stack.is_updating:
total = sum(d['total_node_count'] for d in roles)
context['progress'] = min(95, max(
5, 100 * sum(float(d.get('deployed_node_count', 0))
for d in roles) / (total or 1)
))
else:
# stack is active
if not stack.is_failed:
context['show_last_events'] = False
context['progress'] = 100
controller_role = plan.get_role_by_name("Controller")
context['admin_password'] = plan.parameter_value(
controller_role.parameter_prefix + 'AdminPassword')
context['dashboard_urls'] = stack.dashboard_urls
no_proxy = [urlparse.urlparse(url).hostname
for url in stack.dashboard_urls]
context['no_proxy'] = ",".join(no_proxy)
context['auth_url'] = stack.keystone_auth_url
else:
messages = forms.validate_plan(request, plan)
context['plan_messages'] = messages
context['plan_invalid'] = any(message.get('is_critical')
for message in messages)
context['steps_message'] = _steps_message(messages)
return context
def post(self, request, *args, **kwargs):
"""If the post comes from ajax, return validation results as json."""
if not request.META.get('HTTP_X_HORIZON_VALIDATE', ''):
return super(IndexView, self).post(request, *args, **kwargs)
form_class = self.get_form_class()
form = self.get_form(form_class)
if form.is_valid():
handled = form.handle(self.request, form.cleaned_data)
else:
handled = False
if handled:
messages = forms.validate_plan(request, form.plan)
else:
messages = [{
'text': _(u"Error saving the plan."),
'is_critical': True,
}]
messages.extend({
'text': repr(error),
} for error in form.non_field_errors)
messages.extend({
'text': repr(error),
} for field in form.fields for error in field.errors)
# We need to unlazify all the lazy urls and translations.
return http.HttpResponse(json.dumps({
'plan_invalid': any(m.get('is_critical') for m in messages),
'steps_message': _steps_message(messages),
'messages': [{
'text': unicode(m.get('text', '')),
'is_critical': m.get('is_critical', False),
'indent': m.get('indent', 0),
'classes': m['classes'],
} for m in messages],
}), content_type='application/json')
class DeployConfirmationView(horizon.forms.ModalFormView, views.StackMixin):
form_class = forms.DeployOvercloud
template_name = 'infrastructure/overview/deploy_confirmation.html'
submit_label = _("Deploy")
def get_context_data(self, **kwargs):
context = super(DeployConfirmationView,
self).get_context_data(**kwargs)
plan = api.tuskar.Plan.get_the_plan(self.request)
context['autogenerated_parameters'] = (
plan.list_generated_parameters(with_prefix=False).keys())
return context
def get_success_url(self):
return reverse(INDEX_URL)
class UndeployConfirmationView(horizon.forms.ModalFormView, views.StackMixin):
form_class = forms.UndeployOvercloud
template_name = 'infrastructure/overview/undeploy_confirmation.html'
submit_label = _("Undeploy")
def get_success_url(self):
return reverse(INDEX_URL)
def get_context_data(self, **kwargs):
context = super(UndeployConfirmationView,
self).get_context_data(**kwargs)
context['stack_id'] = self.get_stack().id
return context
def get_initial(self, **kwargs):
initial = super(UndeployConfirmationView, self).get_initial(**kwargs)
initial['stack_id'] = self.get_stack().id
return initial
class PostDeployInitView(horizon.forms.ModalFormView, views.StackMixin):
form_class = forms.PostDeployInit
template_name = 'infrastructure/overview/post_deploy_init.html'
submit_label = _("Initialize")
def get_success_url(self):
return reverse(INDEX_URL)
def get_context_data(self, **kwargs):
context = super(PostDeployInitView,
self).get_context_data(**kwargs)
context['stack_id'] = self.get_stack().id
return context
def get_initial(self, **kwargs):
initial = super(PostDeployInitView, self).get_initial(**kwargs)
initial['stack_id'] = self.get_stack().id
initial['admin_email'] = getattr(self.request.user, 'email', '')
return initial
class ScaleOutView(horizon.forms.ModalFormView, views.StackMixin):
form_class = forms.ScaleOut
template_name = "infrastructure/overview/scale_out.html"
submit_label = _("Deploy Changes")
def get_success_url(self):
return reverse(INDEX_URL)
def get_form(self, form_class):
return form_class(self.request, **self.get_form_kwargs())
def get_context_data(self, *args, **kwargs):
context = super(ScaleOutView, self).get_context_data(*args, **kwargs)
plan = api.tuskar.Plan.get_the_plan(self.request)
form = context.get('form')
roles = [_get_role_data(plan, None, form, role)
for role in plan.role_list]
context.update({
'roles': roles,
'plan': plan,
})
return context
def _get_openrc_credentials(request):
plan = api.tuskar.Plan.get_the_plan(request)
stack = api.heat.Stack.get_by_plan(request, plan)
no_proxy = [urlparse.urlparse(url).hostname
for url in stack.dashboard_urls]
controller_role = plan.get_role_by_name("Controller")
credentials = dict(tenant_name='admin',
auth_url=stack.keystone_auth_url,
admin_password=plan.parameter_value(
controller_role.parameter_prefix + 'AdminPassword'),
no_proxy=",".join(no_proxy))
return credentials
def download_overcloudrc_file(request):
template = 'infrastructure/overview/overcloudrc.sh.template'
try:
context = _get_openrc_credentials(request)
response = shortcuts.render(request,
template,
context,
content_type="text/plain")
response['Content-Disposition'] = ('attachment; '
'filename="overcloudrc"')
response['Content-Length'] = str(len(response.content))
return response
except Exception as e:
LOG.exception("Exception in DownloadOvercloudrcForm.")
messages.error(request, _('Error Downloading RC File: %s') % e)
return shortcuts.redirect(request.build_absolute_uri())

View File

@ -1,281 +0,0 @@
# -*- coding: utf8 -*-
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
import logging
import django.forms
from django.utils.datastructures import SortedDict
from django.utils.translation import ugettext_lazy as _
import horizon.exceptions
import horizon.forms
import horizon.messages
from tuskar_ui import api
import tuskar_ui.forms
from tuskar_ui.utils import utils
LOG = logging.getLogger(__name__)
VIRT_TYPE_CHOICES = [
('qemu', _("Virtualized (qemu)")),
('kvm', _("Baremetal (kvm)")),
]
CINDER_ISCSI_HELPER_CHOICES = [
('tgtadm', _('tgtadm')),
('lioadm', _('lioadm')),
]
class ParameterAwareMixin(object):
parameter = None
def parameter_fields(request, prefix=None, read_only=False):
fields = SortedDict()
plan = api.tuskar.Plan.get_the_plan(request)
parameters = plan.parameter_list(include_key_parameters=False)
for p in parameters:
if prefix and not p.name.startswith(prefix):
continue
Field = django.forms.CharField
field_kwargs = {}
widget = None
if read_only:
if p.hidden:
widget = tuskar_ui.forms.StaticTextPasswordWidget
else:
widget = tuskar_ui.forms.StaticTextWidget
else:
if p.hidden:
widget = django.forms.PasswordInput(render_value=True)
elif p.parameter_type == 'number':
Field = django.forms.IntegerField
elif p.parameter_type == 'boolean':
Field = django.forms.BooleanField
elif (p.parameter_type == 'string' and
p.get_constraint_by_type('allowed_values')):
Field = django.forms.ChoiceField
field_kwargs['choices'] = [
(choice, choice) for choice in
p.get_constraint_by_type('allowed_values')['definition']]
elif (p.parameter_type in ['json', 'comma_delimited_list'] or
'Certificate' in p.name):
widget = django.forms.Textarea
fields[p.name] = Field(
required=False,
label=_parameter_label(p),
initial=p.value,
widget=widget,
**field_kwargs
)
fields[p.name].__class__ = type('ParameterAwareField',
(ParameterAwareMixin, Field), {})
fields[p.name].parameter = p
return fields
def _parameter_label(parameter):
return tuskar_ui.forms.label_with_tooltip(
parameter.label or utils.de_camel_case(parameter.stripped_name),
parameter.description)
class ServiceConfig(horizon.forms.SelfHandlingForm):
def __init__(self, *args, **kwargs):
super(ServiceConfig, self).__init__(*args, **kwargs)
self.fields.update(parameter_fields(self.request, read_only=True))
def global_fieldset(self):
return tuskar_ui.forms.fieldset(self, prefix='^(?!.*::)')
def controller_fieldset(self):
return tuskar_ui.forms.fieldset(self, prefix='Controller-1')
def compute_fieldset(self):
return tuskar_ui.forms.fieldset(self, prefix='Compute-1')
def block_storage_fieldset(self):
return tuskar_ui.forms.fieldset(self, prefix='Cinder-Storage-1')
def object_storage_fieldset(self):
return tuskar_ui.forms.fieldset(self, prefix='Swift-Storage-1')
def ceph_storage_fieldset(self):
return tuskar_ui.forms.fieldset(self, prefix='Ceph-Storage-1')
def handle():
pass
class AdvancedEditServiceConfig(ServiceConfig):
def __init__(self, *args, **kwargs):
super(AdvancedEditServiceConfig, self).__init__(*args, **kwargs)
self.fields.update(parameter_fields(self.request))
def handle(self, request, data):
plan = api.tuskar.Plan.get_the_plan(self.request)
# TODO(bcrochet): Commenting this out.
# For advanced config, we should have a whitelist of which params
# must be synced across roles.
# data = self._sync_common_params_across_roles(plan, data)
try:
plan.patch(request, plan.uuid, data)
except Exception as e:
horizon.exceptions.handle(
request,
_("Unable to update the service configuration."))
LOG.exception(e)
return False
else:
horizon.messages.success(
request,
_("Service configuration updated."))
return True
@staticmethod
def _sync_common_params_across_roles(plan, parameters_dict):
for (p_key, p_value) in parameters_dict.iteritems():
for role in plan.role_list:
role_parameter_key = (role.parameter_prefix +
api.tuskar.strip_prefix(p_key))
if role_parameter_key in parameters_dict:
parameters_dict[role_parameter_key] = p_value
return parameters_dict
class SimpleEditServiceConfig(horizon.forms.SelfHandlingForm):
virt_type = django.forms.ChoiceField(
label=_("Deployment Type"),
choices=VIRT_TYPE_CHOICES,
required=True,
help_text=_('If you are testing OpenStack in a virtual machine, '
'you must configure Compute to use qemu without KVM '
'and hardware virtualization.'))
neutron_public_interface = django.forms.CharField(
label=_("Public Interface"),
required=True,
initial='eth0',
help_text=_('What interface to bridge onto br-ex for network nodes. '
'If you are testing OpenStack in a virtual machine'
'you must configure interface to eth0.'))
snmp_password = django.forms.CharField(
label=_("SNMP Password"),
required=True,
help_text=_('The user password for SNMPd with readonly '
'rights running on all Overcloud nodes'),
widget=django.forms.PasswordInput(render_value=True))
cloud_name = django.forms.CharField(
label=_("Cloud name"),
required=True,
initial="overcloud",
help_text=_('The DNS name of this cloud. '
'E.g. ci-overcloud.tripleo.org'))
cinder_iscsi_helper = django.forms.ChoiceField(
label=_("Cinder ISCSI helper"),
choices=CINDER_ISCSI_HELPER_CHOICES,
required=True,
help_text=_('The iSCSI helper to use with cinder.'))
ntp_server = django.forms.CharField(
label=_("NTP server"),
required=False,
initial="",
help_text=_('Address of the NTP server. If blank, public NTP servers '
'will be used.'))
extra_config = django.forms.CharField(
label=_("Extra Config"),
required=False,
widget=django.forms.Textarea(attrs={'rows': 2}),
help_text=("Additional configuration to inject into the cluster."
"The data format of this field is JSON."
"See http://git.io/PuwLXQ for more information."))
def clean_extra_config(self):
data = self.cleaned_data['extra_config']
try:
json.loads(data)
except Exception as json_error:
raise django.forms.ValidationError(
_("%(err_msg)s"), params={'err_msg': json_error.message})
return data
@staticmethod
def _load_additional_parameters(plan, data, form_key, param_name):
params = {}
param_value = data.get(form_key)
# Set the same parameter and value in all roles.
for role in plan.role_list:
key = role.parameter_prefix + param_name
if key in [parameter.name
for parameter in role.parameter_list(plan)]:
params[key] = param_value
return params
def handle(self, request, data):
plan = api.tuskar.Plan.get_the_plan(self.request)
compute_prefix = plan.get_role_by_name('Compute').parameter_prefix
controller_prefix = plan.get_role_by_name(
'Controller').parameter_prefix
cinder_prefix = plan.get_role_by_name(
'Cinder-Storage').parameter_prefix
virt_type = data.get('virt_type')
neutron_public_interface = data.get('neutron_public_interface')
cloud_name = data.get('cloud_name')
cinder_iscsi_helper = data.get('cinder_iscsi_helper')
ntp_server = data.get('ntp_server')
parameters = {
compute_prefix + 'NovaComputeLibvirtType': virt_type,
controller_prefix + 'CinderISCSIHelper': cinder_iscsi_helper,
cinder_prefix + 'CinderISCSIHelper': cinder_iscsi_helper,
controller_prefix + 'CloudName': cloud_name,
controller_prefix + 'NeutronPublicInterface':
neutron_public_interface,
compute_prefix + 'NeutronPublicInterface':
neutron_public_interface,
controller_prefix + 'NtpServer':
ntp_server,
compute_prefix + 'NtpServer':
ntp_server,
}
parameters.update(self._load_additional_parameters(
plan, data,
'snmp_password', 'SnmpdReadonlyUserPassword'))
parameters.update(self._load_additional_parameters(
plan, data,
'extra_config', 'ExtraConfig'))
try:
plan.patch(request, plan.uuid, parameters)
except Exception as e:
horizon.exceptions.handle(
request,
_("Unable to update the service configuration."))
LOG.exception(e)
return False
else:
horizon.messages.success(
request,
_("Service configuration updated."))
return True

View File

@ -1,26 +0,0 @@
# -*- coding: utf8 -*-
#
# 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 tuskar_ui.infrastructure import dashboard
class Parameters(horizon.Panel):
name = _("Service Configuration")
slug = "parameters"
dashboard.Infrastructure.register(Parameters)

View File

@ -1,26 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% load url from future %}
{% block form_id %}configuration_form{% endblock %}
{% block form_action %}{% url 'horizon:infrastructure:parameters:simple_service_configuration' %}{% endblock %}
{% block modal_id %}provision_modal{% endblock %}
{% block modal-header %}{% trans "Service Configuration" %}{% endblock %}
{% block modal-body %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="right">
<h3>{% trans "Description:" %}</h3>
<p>
{% trans "Configure values that cannot be defaulted" %}
</p>
<p>
{% trans "These values cannot be defaulted. Please choose values for them and save them before you deploy your overcloud." %}
</p>
</div>
{% endblock %}

View File

@ -1,88 +0,0 @@
{% extends "infrastructure/base.html" %}
{% load i18n %}
{% load url from future %}
{% block title %}{% trans "Advanced Service Configuration" %}{% endblock %}
{% block page_header %}
{% include 'horizon/common/_items_count_domain_page_header.html' with title=_('Advanced Service Configuration') %}
{% endblock %}
{% block main %}
<div class="row">
<form id="{% block form_id %}{{ form_id }}{% endblock %}"
name="{% block form_name %}{% endblock %}"
autocomplete="{% block autocomplete %}{% if form.no_autocomplete %}off{% endif %}{% endblock %}"
class="{% block form_class %}{% endblock %} form-horizontal"
action="{% block form_action %}{{ submit_url }}{% endblock %}"
method="{% block form-method %}POST{% endblock %}"
{% block form_validation %}{% endblock %}
{% if add_to_field %}data-add-to-field="{{ add_to_field }}"{% endif %} {% block form_attrs %}{% endblock %}>{% csrf_token %}
<div class="col-sm-12 page_form_actions">
<div class="pull-right">
<a href="{% block cancel_url %}{{ cancel_url }}{% endblock %}"
class="btn btn-default cancel">
{{ cancel_label }}
</a>
<input class="btn btn-primary" type="submit" value="{{ submit_label }}">
</div>
</div>
{% include 'horizon/common/_form_errors.html' with form=form %}
<div class="col-md-2">
<ul class="nav nav-pills nav-stacked nav-arrow" role="tablist">
<li class="active"><a href="#global" role="tab" data-toggle="tab">{% trans "Global" %}</a></li>
<li><a href="#controller" role="tab" data-toggle="tab">{% trans "Controller" %}</a></li>
<li><a href="#compute" role="tab" data-toggle="tab">{% trans "Compute" %}</a></li>
<li><a href="#block-storage" role="tab" data-toggle="tab">{% trans "Block Storage" %}</a></li>
<li><a href="#object-storage" role="tab" data-toggle="tab">{% trans "Object Storage" %}</a></li>
<li><a href="#ceph-storage" role="tab" data-toggle="tab">{% trans "Ceph Storage" %}</a></li>
</ul>
</div>
<div class="col-md-10">
<div class="tab-content panel panel-default configuration-panel">
<div class="tab-pane active" id="global">
{% for field in form.global_fieldset %}
{% include 'horizon/common/_horizontal_field.html' with field=field %}
{% endfor %}
</div>
<div class="tab-pane" id="controller">
{% for field in form.controller_fieldset %}
{% include 'horizon/common/_horizontal_field.html' with field=field %}
{% endfor %}
</div>
<div class="tab-pane" id="compute">
{% for field in form.compute_fieldset %}
{% include 'horizon/common/_horizontal_field.html' with field=field %}
{% endfor %}
</div>
<div class="tab-pane" id="block-storage">
{% for field in form.block_storage_fieldset %}
{% include 'horizon/common/_horizontal_field.html' with field=field %}
{% endfor %}
</div>
<div class="tab-pane" id="object-storage">
{% for field in form.object_storage_fieldset %}
{% include 'horizon/common/_horizontal_field.html' with field=field %}
{% endfor %}
</div>
<div class="tab-pane" id="ceph-storage">
{% for field in form.ceph_storage_fieldset %}
{% include 'horizon/common/_horizontal_field.html' with field=field %}
{% endfor %}
</div>
</div>
</div>
</form>
</div>
<script type="text/javascript">
(window.$ || window.addHorizonLoadEvent)(function () {
$(document).tooltip('hide'); // prevent horizon from adding tooltip
$('a.help-icon').click(function () {
return false;
}).popover({
trigger: 'focus',
placement: 'right'
});
});
</script>
{% endblock %}

View File

@ -1,76 +0,0 @@
{% extends "infrastructure/base.html" %}
{% load i18n %}
{% load url from future %}
{% block title %}{% trans "Service Configuration" %}{% endblock %}
{% block page_header %}
{% include 'horizon/common/_items_count_domain_page_header.html' with title=_('Service Configuration') %}
{% endblock %}
{% block main %}
<div class="row">
<form class="form-horizontal">
{% include 'horizon/common/_form_errors.html' with form=form %}
<div class="col-md-2">
<ul class="nav nav-pills nav-stacked nav-arrow" role="tablist">
<li class="active"><a href="#global" role="tab" data-toggle="tab">{% trans "Global" %}</a></li>
<li><a href="#controller" role="tab" data-toggle="tab">{% trans "Controller" %}</a></li>
<li><a href="#compute" role="tab" data-toggle="tab">{% trans "Compute" %}</a></li>
<li><a href="#block-storage" role="tab" data-toggle="tab">{% trans "Block Storage" %}</a></li>
<li><a href="#object-storage" role="tab" data-toggle="tab">{% trans "Object Storage" %}</a></li>
<li><a href="#ceph-storage" role="tab" data-toggle="tab">{% trans "Ceph Storage" %}</a></li>
</ul>
</div>
<div class="col-md-10">
<div class="tab-content panel panel-default configuration-panel">
<div class="tab-pane active" id="global">
{% for field in form.global_fieldset %}
{% include 'horizon/common/_horizontal_field.html' with field=field %}
{% endfor %}
</div>
<div class="tab-pane" id="controller">
{% for field in form.controller_fieldset %}
{% include 'horizon/common/_horizontal_field.html' with field=field %}
{% endfor %}
</div>
<div class="tab-pane" id="compute">
{% for field in form.compute_fieldset %}
{% include 'horizon/common/_horizontal_field.html' with field=field %}
{% endfor %}
</div>
<div class="tab-pane" id="block-storage">
{% for field in form.block_storage_fieldset %}
{% include 'horizon/common/_horizontal_field.html' with field=field %}
{% endfor %}
</div>
<div class="tab-pane" id="object-storage">
{% for field in form.object_storage_fieldset %}
{% include 'horizon/common/_horizontal_field.html' with field=field %}
{% endfor %}
</div>
<div class="tab-pane" id="ceph-storage">
{% for field in form.ceph_storage_fieldset %}
{% include 'horizon/common/_horizontal_field.html' with field=field %}
{% endfor %}
</div>
</div>
</div>
</form>
</div>
<script type="text/javascript">
(window.$ || window.addHorizonLoadEvent)(function () {
$(document).tooltip('hide'); // prevent horizon from adding tooltip
$('a.help-icon').click(function () {
return false;
}).popover({
trigger: 'focus',
placement: 'right'
});
$('a.password-button').popover({
trigger: 'click',
placement: 'right'
});
});
</script>
{% endblock %}

View File

@ -1,11 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{% trans "Simple Service Configuration" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Simple Service Configuration") %}
{% endblock %}
{% block main %}
{% include "infrastructure/parameters/_simple_service_config.html" %}
{% endblock %}

View File

@ -1,130 +0,0 @@
# -*- coding: utf8 -*-
#
# 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 contextlib
from django.core import urlresolvers
from mock import patch, call, ANY # noqa
from openstack_dashboard.test.test_data import utils
from tuskar_ui import api
from tuskar_ui.test import helpers as test
from tuskar_ui.test.test_data import tuskar_data
INDEX_URL = urlresolvers.reverse(
'horizon:infrastructure:parameters:index')
SIMPLE_SERVICE_CONFIG_URL = urlresolvers.reverse(
'horizon:infrastructure:parameters:simple_service_configuration')
ADVANCED_SERVICE_CONFIG_URL = urlresolvers.reverse(
'horizon:infrastructure:parameters:advanced_service_configuration')
TEST_DATA = utils.TestDataContainer()
tuskar_data.data(TEST_DATA)
class ParametersTest(test.BaseAdminViewTests):
def test_index(self):
plans = [api.tuskar.Plan(plan)
for plan in self.tuskarclient_plans.list()]
roles = [api.tuskar.Role(role)
for role in self.tuskarclient_roles.list()]
with contextlib.nested(
patch('tuskar_ui.api.tuskar.Plan.list',
return_value=plans),
patch('tuskar_ui.api.tuskar.Role.list',
return_value=roles),
):
res = self.client.get(INDEX_URL)
self.assertTemplateUsed(res, 'infrastructure/parameters/index.html')
def test_simple_service_config_get(self):
plan = api.tuskar.Plan(self.tuskarclient_plans.first())
role = api.tuskar.Role(self.tuskarclient_roles.first())
with contextlib.nested(
patch('tuskar_ui.api.tuskar.Plan.get_the_plan',
return_value=plan),
patch('tuskar_ui.api.tuskar.Plan.get_role_by_name',
return_value=role),
):
res = self.client.get(SIMPLE_SERVICE_CONFIG_URL)
self.assertTemplateUsed(
res, 'infrastructure/parameters/simple_service_config.html')
def test_advanced_service_config_post(self):
plan = api.tuskar.Plan(self.tuskarclient_plans.first())
roles = [api.tuskar.Role(role)
for role in self.tuskarclient_roles.list()]
parameters = [api.tuskar.Parameter(p, plan=self)
for p in plan.parameters]
data = {p.name: unicode(p.value) for p in parameters}
with contextlib.nested(
patch('tuskar_ui.api.tuskar.Plan.get_the_plan',
return_value=plan),
patch('tuskar_ui.api.tuskar.Plan.role_list',
return_value=roles),
patch('tuskar_ui.api.tuskar.Plan.parameter_list',
return_value=parameters),
patch('tuskar_ui.api.tuskar.Plan.patch',
return_value=plan),
) as (get_the_plan, role_list, parameter_list, plan_patch):
res = self.client.post(ADVANCED_SERVICE_CONFIG_URL, data)
self.assertRedirectsNoFollow(res, INDEX_URL)
plan_patch.assert_called_once_with(ANY, plan.uuid, data)
def test_simple_service_config_post(self):
plan = api.tuskar.Plan(self.tuskarclient_plans.first())
roles = [api.tuskar.Role(role) for role in
self.tuskarclient_roles.list()]
plan.role_list = roles
data = {
'virt_type': 'qemu',
'snmp_password': 'password',
'cinder_iscsi_helper': 'lioadm',
'cloud_name': 'cloud_name',
'neutron_public_interface': 'eth0',
'extra_config': '{}'
}
with contextlib.nested(
patch('tuskar_ui.api.tuskar.Plan.get_the_plan',
return_value=plan),
patch('tuskar_ui.api.tuskar.Plan.patch',
return_value=plan),
patch('tuskar_ui.api.tuskar.Plan.get_role_by_name',
return_value=roles[0]),
) as (get_the_plan, plan_patch, get_role_by_name):
res = self.client.post(SIMPLE_SERVICE_CONFIG_URL, data)
self.assertRedirectsNoFollow(res, INDEX_URL)
plan_patch.assert_called_once_with(ANY, plan.uuid, {
'Controller-1::CloudName': u'cloud_name',
'Controller-1::SnmpdReadonlyUserPassword': u'password',
'Controller-1::NeutronPublicInterface': u'eth0',
'Controller-1::CinderISCSIHelper': u'lioadm',
'Controller-1::NovaComputeLibvirtType': u'qemu',
'Compute-1::SnmpdReadonlyUserPassword': u'password',
'Controller-1::NtpServer': u'',
'Controller-1::ExtraConfig': u'{}',
'Compute-1::ExtraConfig': u'{}',
'Block Storage-1::ExtraConfig': u'{}',
'Object Storage-1::ExtraConfig': u'{}'})

View File

@ -1,29 +0,0 @@
# -*- coding: utf8 -*-
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.conf import urls
from tuskar_ui.infrastructure.parameters import views
urlpatterns = urls.patterns(
'',
urls.url(r'^$', views.IndexView.as_view(), name='index'),
urls.url(r'^simple-service-config$',
views.SimpleServiceConfigView.as_view(),
name='simple_service_configuration'),
urls.url(r'^advanced-service-config$',
views.AdvancedServiceConfigView.as_view(),
name='advanced_service_configuration'),
)

View File

@ -1,107 +0,0 @@
# -*- coding: utf8 -*-
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.core.urlresolvers import reverse
from django.core.urlresolvers import reverse_lazy
from django.utils.translation import ugettext_lazy as _
import horizon.forms
import horizon.tables
from tuskar_ui import api
from tuskar_ui.infrastructure.parameters import forms
class SimpleServiceConfigView(horizon.forms.ModalFormView):
form_class = forms.SimpleEditServiceConfig
success_url = reverse_lazy('horizon:infrastructure:parameters:index')
submit_label = _("Save Configuration")
template_name = "infrastructure/parameters/simple_service_config.html"
def get_initial(self):
plan = api.tuskar.Plan.get_the_plan(self.request)
compute_prefix = plan.get_role_by_name('Compute').parameter_prefix
controller_prefix = plan.get_role_by_name(
'Controller').parameter_prefix
cinder_iscsi_helper = plan.parameter_value(
controller_prefix + 'CinderISCSIHelper')
cloud_name = plan.parameter_value(
controller_prefix + 'CloudName')
extra_config = plan.parameter_value(
controller_prefix + 'ExtraConfig')
neutron_public_interface = plan.parameter_value(
controller_prefix + 'NeutronPublicInterface')
ntp_server = plan.parameter_value(
controller_prefix + 'NtpServer')
snmp_password = plan.parameter_value(
controller_prefix + 'SnmpdReadonlyUserPassword')
virt_type = plan.parameter_value(
compute_prefix + 'NovaComputeLibvirtType')
return {
'cinder_iscsi_helper': cinder_iscsi_helper,
'cloud_name': cloud_name,
'neutron_public_interface': neutron_public_interface,
'ntp_server': ntp_server,
'extra_config': extra_config,
'neutron_public_interface': neutron_public_interface,
'snmp_password': snmp_password,
'virt_type': virt_type}
class IndexView(horizon.forms.ModalFormView):
form_class = forms.ServiceConfig
form_id = "service_config"
template_name = "infrastructure/parameters/index.html"
def get_initial(self):
self.plan = api.tuskar.Plan.get_the_plan(self.request)
self.parameters = self.plan.parameter_list(
include_key_parameters=False)
return {p.name: p.value for p in self.parameters}
def get_context_data(self, **kwargs):
context = super(IndexView, self).get_context_data(**kwargs)
advanced_edit_action = {
'name': _('Advanced Configuration'),
'url': reverse('horizon:infrastructure:parameters:'
'advanced_service_configuration'),
'icon': 'fa-pencil',
'ajax_modal': False,
}
simplified_edit_action = {
'name': _('Simplified Configuration'),
'url': reverse('horizon:infrastructure:parameters:'
'simple_service_configuration'),
'icon': 'fa-pencil-square-o',
'ajax_modal': True,
}
context['header_actions'] = [advanced_edit_action,
simplified_edit_action]
return context
class AdvancedServiceConfigView(IndexView):
form_class = forms.AdvancedEditServiceConfig
form_id = "advanced_service_config"
success_url = reverse_lazy('horizon:infrastructure:parameters:index')
submit_label = _("Save Configuration")
submit_url = reverse_lazy('horizon:infrastructure:parameters:'
'advanced_service_configuration')
template_name = "infrastructure/parameters/advanced_service_config.html"
def get_context_data(self, **kwargs):
context = super(AdvancedServiceConfigView,
self) .get_context_data(**kwargs)
context['header_actions'] = []
return context

View File

@ -1,26 +0,0 @@
# -*- coding: utf8 -*-
#
# 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 tuskar_ui.infrastructure import dashboard
class Roles(horizon.Panel):
name = _("Deployment Roles")
slug = "roles"
dashboard.Infrastructure.register(Roles)

View File

@ -1,66 +0,0 @@
# -*- coding: utf8 -*-
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.utils.translation import ugettext_lazy as _
from horizon import tables
from tuskar_ui import api
from tuskar_ui.infrastructure.nodes import tables as nodes_tables
class UpdateRole(tables.LinkAction):
name = "update"
verbose_name = _("Edit Role")
url = "horizon:infrastructure:roles:update"
classes = ("ajax-modal",)
icon = "pencil"
def allowed(self, request, datum):
plan = api.tuskar.Plan.get_the_plan(request)
if datum.id in [role.id for role in plan.role_list]:
return True
return False
class RolesTable(tables.DataTable):
name = tables.Column('name',
link="horizon:infrastructure:roles:detail",
verbose_name=_("Role"))
flavor = tables.Column('flavor',
verbose_name=_("Flavor"))
image = tables.Column('image',
verbose_name=_("Image"))
def get_object_id(self, datum):
return datum.uuid
class Meta(object):
name = "roles"
verbose_name = _("Deployment Roles")
table_actions = ()
row_actions = (UpdateRole,)
template = "horizon/common/_enhanced_data_table.html"
class NodeTable(nodes_tables.ProvisionedNodesTable):
class Meta(object):
name = "nodetable"
verbose_name = _("Nodes")
hidden_title = False
table_actions = ()
row_actions = ()
template = "horizon/common/_enhanced_data_table.html"

Some files were not shown because too many files have changed in this diff Show More