Retire the Tuskar UI codebase
Change-Id: I469fdc1339d4991586bf2e1d62d99fd5b68289eb Depends-On: I904b2f27591333e104bf9080bb8c3876fcb3596c
This commit is contained in:
parent
0a5b41bad6
commit
31e0bb84f6
12
.mailmap
12
.mailmap
@ -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>
|
42
.pylintrc
42
.pylintrc
@ -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=_
|
60
HACKING.rst
60
HACKING.rst
@ -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
176
LICENSE
@ -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.
|
||||
|
19
MANIFEST.in
19
MANIFEST.in
@ -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
|
24
Makefile
24
Makefile
@ -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
10
README
Normal 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.
|
41
README.rst
41
README.rst
@ -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.
|
@ -1,2 +0,0 @@
|
||||
DASHBOARD = 'admin'
|
||||
DISABLED = True
|
@ -1,2 +0,0 @@
|
||||
DASHBOARD = 'project'
|
||||
DISABLED = True
|
@ -1,2 +0,0 @@
|
||||
DASHBOARD = 'identity'
|
||||
DISABLED = True
|
@ -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
|
56
dev_env.sh
56
dev_env.sh
@ -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"
|
153
doc/Makefile
153
doc/Makefile
@ -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."
|
190
doc/make.bat
190
doc/make.bat
@ -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
|
@ -1 +0,0 @@
|
||||
../../HACKING.rst
|
@ -1 +0,0 @@
|
||||
../../README.rst
|
@ -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'
|
@ -1,20 +0,0 @@
|
||||
Tuskar UI
|
||||
=========
|
||||
|
||||
Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
README
|
||||
install
|
||||
user_guide
|
||||
HACKING
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
@ -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
|
||||
|
@ -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.
|
11
manage.py
11
manage.py
@ -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)
|
20
nodes.sh
20
nodes.sh
@ -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
|
@ -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
|
552
run_tests.sh
552
run_tests.sh
@ -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
|
41
setup.cfg
41
setup.cfg
@ -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
|
29
setup.py
29
setup.py
@ -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)
|
@ -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
|
||||
|
@ -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()
|
@ -1,4 +0,0 @@
|
||||
#!/bin/bash
|
||||
TOOLS=`dirname $0`
|
||||
VENV=$TOOLS/../.venv
|
||||
source $VENV/bin/activate && $@
|
68
tox.ini
68
tox.ini
@ -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
|
@ -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]
|
@ -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
|
@ -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])
|
@ -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
|
@ -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
|
@ -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
|
@ -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} <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> {1}</a>'
|
||||
u'</p>', value, _(u"Reveal")
|
||||
)
|
@ -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
|
@ -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)
|
@ -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)
|
@ -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"
|
@ -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 %}
|
@ -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:"—" }}</dd>
|
||||
<dt>{% trans "CPUs" %}</dt>
|
||||
<dd>{{ flavor.vcpus|default:"—" }}</dd>
|
||||
<dt>{% trans "Memory" %}</dt>
|
||||
<dd>{{ flavor.ram_bytes|filesizeformat|default:"—" }}</dd>
|
||||
<dt>{% trans "Disk" %}</dt>
|
||||
<dd>{{ flavor.disk_bytes|filesizeformat|default:"—" }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
{{ table.render }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
@ -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 }}× 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 %}
|
@ -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'),
|
||||
]))
|
@ -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'),
|
||||
)
|
@ -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,
|
||||
)
|
@ -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]
|
@ -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
|
@ -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)
|
@ -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"
|
@ -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 %}
|
@ -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')
|
@ -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'),
|
||||
)
|
@ -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 []
|
@ -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
|
@ -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)
|
@ -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"
|
@ -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 %}
|
@ -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 %}
|
@ -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 %}
|
@ -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 %}
|
@ -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 %}
|
@ -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)
|
@ -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'),
|
||||
)
|
@ -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)
|
@ -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)
|
@ -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)
|
@ -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"
|
@ -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,)
|
@ -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)
|
@ -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'),
|
||||
)
|
@ -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')
|
@ -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
|
@ -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)
|
@ -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',
|
||||
},
|
||||
])
|
@ -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'),
|
||||
)
|
@ -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())
|
@ -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
|
@ -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)
|
@ -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 %}
|
@ -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 %}
|
@ -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 %}
|
@ -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 %}
|
@ -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'{}'})
|
@ -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'),
|
||||
)
|
@ -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
|
@ -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)
|
@ -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
Loading…
Reference in New Issue
Block a user