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