summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--README.rst141
-rw-r--r--docs/index.rst36
-rw-r--r--horizon/forms/__init__.py3
-rw-r--r--horizon/forms/fields.py6
-rw-r--r--horizon/static/horizon/js/horizon.capacity.js163
-rw-r--r--horizon/static/horizon/js/horizon.d3circleschart.js279
-rw-r--r--horizon/static/horizon/js/horizon.d3linechart.js200
-rw-r--r--horizon/static/horizon/js/horizon.d3singlebarchart.js474
-rw-r--r--horizon/static/horizon/js/horizon.templates.js2
-rw-r--r--horizon/tables/base.py48
-rw-r--r--horizon/templates/horizon/client_side/_modal_chart.html19
-rw-r--r--horizon/templates/horizon/client_side/templates.html1
-rw-r--r--horizon/workflows/__init__.py4
-rw-r--r--horizon/workflows/base.py88
-rw-r--r--openstack_dashboard/api/__init__.py2
-rw-r--r--openstack_dashboard/api/nova.py2
-rw-r--r--openstack_dashboard/api/tuskar.py1001
-rw-r--r--openstack_dashboard/dashboards/infrastructure/__init__.py0
-rw-r--r--openstack_dashboard/dashboards/infrastructure/dashboard.py30
-rw-r--r--openstack_dashboard/dashboards/infrastructure/fixtures/initial_data.json94
-rw-r--r--openstack_dashboard/dashboards/infrastructure/models.py97
-rw-r--r--openstack_dashboard/dashboards/infrastructure/overview/__init__.py0
-rw-r--r--openstack_dashboard/dashboards/infrastructure/overview/models.py19
-rw-r--r--openstack_dashboard/dashboards/infrastructure/overview/panel.py29
-rw-r--r--openstack_dashboard/dashboards/infrastructure/overview/templates/overview/index.html12
-rw-r--r--openstack_dashboard/dashboards/infrastructure/overview/tests.py23
-rw-r--r--openstack_dashboard/dashboards/infrastructure/overview/urls.py24
-rw-r--r--openstack_dashboard/dashboards/infrastructure/overview/views.py26
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/__init__.py0
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/flavors/__init__.py0
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/flavors/forms.py108
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/flavors/tables.py94
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/flavors/tabs.py34
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/flavors/tests.py118
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/flavors/urls.py34
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/flavors/views.py129
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/nodes/__init__.py0
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/nodes/tables.py62
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/nodes/tabs.py34
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/nodes/tests.py28
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/nodes/urls.py29
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/nodes/views.py70
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/panel.py29
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/racks/__init__.py0
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/racks/forms.py154
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/racks/tables.py120
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/racks/tabs.py53
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/racks/tests.py204
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/racks/urls.py39
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/racks/views.py253
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/racks/workflows.py224
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/__init__.py0
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/forms.py52
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/tables.py169
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/tabs.py78
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/tests.py335
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/urls.py40
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/views.py202
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/workflows.py320
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/tabs.py93
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/flavors/_create.html26
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/flavors/_detail_overview.html128
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/flavors/_edit.html27
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/flavors/create.html11
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/flavors/detail.html24
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/flavors/edit.html12
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/index.html25
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/nodes/_detail_overview.html258
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/nodes/detail.html48
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/nodes/unracked.html29
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/_create.html22
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/_detail_overview.html316
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/_edit.html22
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/_edit_status.html23
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/_index_table.html3
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/_upload.html37
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/create.html11
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/detail.html59
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/edit.html11
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/edit_status.html11
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/upload.html11
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/_action.html22
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/_detail_flavors.html1
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/_detail_overview.html239
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/_detail_racks.html1
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/_racks_step.html3
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/_resource_class_info_and_flavors_step.html56
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/action.html9
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/detail.html62
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/tests.py84
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/urls.py32
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/views.py29
-rw-r--r--openstack_dashboard/dashboards/infrastructure/service_management/__init__.py0
-rw-r--r--openstack_dashboard/dashboards/infrastructure/service_management/panel.py29
-rw-r--r--openstack_dashboard/dashboards/infrastructure/service_management/templates/service_management/index.html12
-rw-r--r--openstack_dashboard/dashboards/infrastructure/service_management/tests.py23
-rw-r--r--openstack_dashboard/dashboards/infrastructure/service_management/urls.py24
-rw-r--r--openstack_dashboard/dashboards/infrastructure/service_management/views.py26
-rw-r--r--openstack_dashboard/dashboards/infrastructure/static/infrastructure/less/infrastructure.less497
-rw-r--r--openstack_dashboard/dashboards/infrastructure/templates/infrastructure/base.html18
-rw-r--r--openstack_dashboard/dashboards/infrastructure/templates/infrastructure/base_detail.html27
-rw-r--r--openstack_dashboard/dashboards/infrastructure/templatetags/__init__.py0
-rw-r--r--openstack_dashboard/dashboards/infrastructure/templatetags/chart_helpers.py40
-rw-r--r--openstack_dashboard/exceptions.py7
-rw-r--r--openstack_dashboard/local/local_settings.py.example13
-rw-r--r--openstack_dashboard/settings.py16
-rw-r--r--openstack_dashboard/static/dashboard/img/communication_flow.pngbin0 -> 1662 bytes
-rw-r--r--openstack_dashboard/static/dashboard/img/horizontal_loader.gifbin0 -> 22580 bytes
-rw-r--r--openstack_dashboard/test/api_tests/tuskar_tests.py109
-rw-r--r--openstack_dashboard/test/helpers.py10
-rw-r--r--openstack_dashboard/test/settings.py17
-rw-r--r--openstack_dashboard/test/test_data/tuskar_data.py167
-rw-r--r--openstack_dashboard/test/test_data/utils.py4
-rw-r--r--requirements.txt2
115 files changed, 8499 insertions, 123 deletions
diff --git a/.gitignore b/.gitignore
index 0f6793ef..7cdaea1e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,3 +25,4 @@ dist
AUTHORS
ChangeLog
tags
+openstack_dashboard/dummydb.sqlite
diff --git a/README.rst b/README.rst
index df895d28..5295b725 100644
--- a/README.rst
+++ b/README.rst
@@ -1,125 +1,36 @@
-=============================
-Horizon (OpenStack Dashboard)
-=============================
+tuskar-ui
+=========
-Horizon is a Django-based project aimed at providing a complete OpenStack
-Dashboard along with an extensible framework for building new dashboards
-from reusable components. The ``openstack_dashboard`` module is a reference
-implementation of a Django site that uses the ``horizon`` app to provide
-web-based interactions with the various OpenStack projects.
+**tuskar-ui** is a user interface for
+`Tuskar <https://github.com/tuskar/tuskar>`__, a management API for
+OpenStack deployments. It is based on (and forked from) `OpenStack
+Horizon <https://wiki.openstack.org/wiki/Horizon>`__.
-For release management:
+High-Level Overview
+-------------------
- * https://launchpad.net/horizon
+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.
-For blueprints and feature specifications:
+License
+-------
- * https://blueprints.launchpad.net/horizon
+This project is licensed under the Apache License, version 2. More
+information can be found in the LICENSE file.
-For issue tracking:
+Further Documentation
+---------------------
- * https://bugs.launchpad.net/horizon
+Check out our `docs directory
+<https://github.com/tuskar/tuskar-ui/blob/master/docs/index.rst>`_
+for expanded documentation.
-Dependencies
-============
+Contact Us
+----------
-To get started you will need to install Node.js (http://nodejs.org/) on your
-machine. Node.js is used with Horizon in order to use LESS
-(http://lesscss.org/) for our CSS needs. Horizon is currently using Node.js
-v0.6.12.
+Join us on IRC (Internet Relay Chat)::
-For Ubuntu use apt to install Node.js::
-
- $ sudo apt-get install nodejs
-
-For other versions of Linux, please see here:: http://nodejs.org/#download for
-how to install Node.js on your system.
-
-
-Getting Started
-===============
-
-For local development, first create a virtualenv for the project.
-In the ``tools`` directory there is a script to create one for you:
-
- $ python tools/install_venv.py
-
-Alternatively, the ``run_tests.sh`` script will also install the environment
-for you and then run the full test suite to verify everything is installed
-and functioning correctly.
-
-Now that the virtualenv is created, you need to configure your local
-environment. To do this, create a ``local_settings.py`` file in the
-``openstack_dashboard/local/`` directory. There is a
-``local_settings.py.example`` file there that may be used as a template.
-
-If all is well you should able to run the development server locally:
-
- $ tools/with_venv.sh manage.py runserver
-
-or, as a shortcut::
-
- $ ./run_tests.sh --runserver
-
-
-Settings Up OpenStack
-=====================
-
-The recommended tool for installing and configuring the core OpenStack
-components is `Devstack`_. Refer to their documentation for getting
-Nova, Keystone, Glance, etc. up and running.
-
-.. _Devstack: http://devstack.org/
-
-.. note::
-
- The minimum required set of OpenStack services running includes the
- following:
-
- * Nova (compute, api, scheduler, network, *and* volume services)
- * Glance
- * Keystone
-
- Optional support is provided for Swift.
-
-
-Development
-===========
-
-For development, start with the getting started instructions above.
-Once you have a working virtualenv and all the necessary packages, read on.
-
-If dependencies are added to either ``horizon`` or ``openstack-dashboard``,
-they should be added to ``requirements.txt``.
-
-The ``run_tests.sh`` script invokes tests and analyses on both of these
-components in its process, and it is what Jenkins uses to verify the
-stability of the project. If run before an environment is set up, it will
-ask if you wish to install one.
-
-To run the unit tests::
-
- $ ./run_tests.sh
-
-Building Contributor Documentation
-==================================
-
-This documentation is written by contributors, for contributors.
-
-The source is maintained in the ``doc/source`` folder using
-`reStructuredText`_ and built by `Sphinx`_
-
-.. _reStructuredText: http://docutils.sourceforge.net/rst.html
-.. _Sphinx: http://sphinx.pocoo.org/
-
-* Building Automatically::
-
- $ ./run_tests.sh --docs
-
-* Building Manually::
-
- $ export DJANGO_SETTINGS_MODULE=local.local_settings
- $ python doc/generate_autodoc_index.py
- $ sphinx-build -b html doc/source build/sphinx/html
-
-Results are in the `build/sphinx/html` directory
+ Network: Freenode (irc.freenode.net/tuskar)
+ Channel: #tuskar
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 00000000..a34f44ea
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,36 @@
+=========
+Tuskar-UI
+=========
+
+Tuskar-UI is a user interface for `Tuskar <https://github.com/tuskar/tuskar>`_, a management API for OpenStack deployments. It is based on (and forked from) `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.
+
+Developer Information
+---------------------
+
+Install and Contribute
+~~~~~~~~~~~~~~~~~~~~~~
+
+Follow the `Installation Guide <https://github.com/tuskar/tuskar-ui/blob/master/docs/install.md>`_ to install Tuskar-UI.
+
+If you're interested in the code, here are some key places to start:
+
+* `openstack_dashboard/api/tuskar.py <https://github.com/tuskar/tuskar-ui/blob/master/openstack_dashboard/api/tuskar.py>`_ - This file contains all the API calls made to the Tuskar API (through python-tuskarclient).
+* `openstack_dashboard/dashboards/infrastructure <https://github.com/tuskar/tuskar-ui/tree/master/openstack_dashboard/dashboards/infrastructure>`_ - The Tuskar UI code is contained within this directory. Up to this point, UI development has been focused within the resource_management/ subdirectory.
+
+*TODO* Contribution Guide
+
+Future Work
+-----------
+
+Contact Us
+----------
+
+Join us on IRC (Internet Relay Chat)::
+
+ Network: Freenode (irc.freenode.net/tuskar)
+ Channel: #tuskar
diff --git a/horizon/forms/__init__.py b/horizon/forms/__init__.py
index 4df93321..27b233f7 100644
--- a/horizon/forms/__init__.py
+++ b/horizon/forms/__init__.py
@@ -35,3 +35,6 @@ assert ModalFormView
assert ModalFormMixin
assert DynamicTypedChoiceField
assert DynamicChoiceField
+
+# FIXME: TableStep hack adding NumberInput
+from horizon.forms.fields import NumberInput
diff --git a/horizon/forms/fields.py b/horizon/forms/fields.py
index 38dd2250..9bfcc465 100644
--- a/horizon/forms/fields.py
+++ b/horizon/forms/fields.py
@@ -69,3 +69,9 @@ class DynamicChoiceField(fields.ChoiceField):
class DynamicTypedChoiceField(DynamicChoiceField, fields.TypedChoiceField):
""" Simple mix of ``DynamicChoiceField`` and ``TypedChoiceField``. """
pass
+
+
+# FIXME: TableStep
+# Should be in django 1.5.1 forms.widgets
+class NumberInput(widgets.TextInput):
+ input_type = 'number'
diff --git a/horizon/static/horizon/js/horizon.capacity.js b/horizon/static/horizon/js/horizon.capacity.js
new file mode 100644
index 00000000..c9cb4c0c
--- /dev/null
+++ b/horizon/static/horizon/js/horizon.capacity.js
@@ -0,0 +1,163 @@
+/*
+ Used for animating and displaying capacity information using
+ D3js progress bars.
+
+ Usage:
+ In order to have capacity bars that work with this, you need to have a
+ DOM structure like this in your Django template:
+
+ <div id="your_capacity_bar_id" class="capacity_bar">
+ </div>
+
+ With this capacity bar, you then need to add some data- HTML attributes
+ to the div #your_capacity_bar_id. The available data- attributes are:
+
+ data-chart-type="capacity_bar_chart" REQUIRED
+ Must be "capacity_bar_chart".
+
+ data-capacity-used="integer" OPTIONAL
+ Integer representing the total number used by the user.
+
+ data-capacity-limit="integer" OPTIONAL
+ Integer representing the total quota limit the user has. Note this IS
+ NOT the amount remaining they can use, but the total original capacity.
+
+ data-average-capacity-used="integer" OPTIONAL
+ Integer representing the average usage of given capacity.
+*/
+
+horizon.Capacity = {
+ capacity_bars: [],
+
+ // Determines the capacity bars to be used for capacity display.
+ init: function() {
+ this.capacity_bars = $('div[data-chart-type="capacity_bar_chart"]');
+
+ // Draw the capacity bars
+ this._initialCreation(this.capacity_bars);
+ },
+
+
+ /*
+ Create a new d3 bar and populate it with the current amount used,
+ average used, and percentage label
+ */
+ drawUsed: function(element, used_perc, used_px, average_perc) {
+ var w= "100%";
+ var h= 15;
+ var lvl_curve= 3;
+ var bkgrnd= "#F2F2F2";
+ var frgrnd= "grey";
+ var usage_color = d3.scale.linear()
+ .domain([0, 50, 75, 90, 100])
+ .range(["#669900", "#669900", "#FF9900", "#FF3300", "#CC0000"]);
+
+ // Horizontal Bars
+ var bar = d3.select("#"+element).append("svg:svg")
+ .attr("class", "chart")
+ .attr("width", w)
+ .attr("height", h)
+ .style("background-color", "white")
+ .append("g");
+
+ // background - unused resources
+ bar.append("rect")
+ .attr("y", 0)
+ .attr("width", w)
+ .attr("height", h)
+ .attr("rx", lvl_curve)
+ .attr("ry", lvl_curve)
+ .style("fill", bkgrnd)
+ .style("stroke", "#bebebe")
+ .style("stroke-width", 1);
+
+ // used resources
+ if (used_perc) {
+ bar.append("rect")
+ .attr("class", "usedbar")
+ .attr("y", 0)
+ .attr("id", "test")
+ .attr("width", 0)
+ .attr("height", h)
+ .attr("rx", lvl_curve)
+ .attr("ry", lvl_curve)
+ .style("fill", usage_color(used_perc))
+ .style("stroke", "#a0a0a0")
+ .style("stroke-width", 1)
+ .attr("d", used_perc)
+ .transition()
+ .duration(500)
+ .attr("width", used_perc + "%");
+ }
+
+ // average
+ if (average_perc) {
+ bar.append("rect")
+ .attr("y",1)
+ .attr("x", 0)
+ .attr("class", "average")
+ .attr("height", h-2)
+ .attr("width", 1)
+ .style("fill", "black")
+ .transition()
+ .duration(500)
+ .attr("x", average_perc + "%");
+ }
+
+ // used text
+ if (used_perc) {
+ bar.append("text")
+ .text(used_perc + "%")
+ .attr("y", 8)
+ .attr("x", 3)
+ .attr("dominant-baseline", "middle")
+ .attr("font-size", 10)
+ .transition()
+ .duration(500)
+ .attr("x", function() {
+ // position the percentage label
+ if (used_perc > 99 && used_px > 25){
+ return used_px - 30;
+ } else if (used_px > 25) {
+ return used_px - 25;
+ } else {
+ return used_px + 3;
+ }
+ });
+ }
+ },
+
+
+ // Draw the initial d3 bars
+ _initialCreation: function(bars) {
+ var scope = this;
+ $(bars).each(function(index, element) {
+ var progress_element = $(element);
+
+ var capacity_limit = parseInt(progress_element.attr('data-capacity-limit'), 10);
+ var capacity_used = parseInt(progress_element.attr('data-capacity-used'), 10);
+ var average_used = parseInt(progress_element.attr('data-average-capacity-used'), 10);
+
+ if (!isNaN(capacity_limit) && !isNaN(average_used)) {
+ var average_percentage = ((average_used / capacity_limit) * 100);
+ } else {
+ var average_percentage = 0;
+ }
+
+ if (!isNaN(capacity_limit) && !isNaN(capacity_used)) {
+ var percentage_used = Math.round((capacity_used / capacity_limit) * 100);
+ var used_px = progress_element.width() / 100 * percentage_used;
+
+ } else { // If NaN percentage_used is 0
+ var percentage_used = 0;
+ var used_px = 0;
+ }
+
+ scope.drawUsed($(element).attr('id'), percentage_used, used_px, average_percentage);
+ });
+ }
+};
+
+horizon.addInitFunction(function () {
+ horizon.Capacity.init();
+});
diff --git a/horizon/static/horizon/js/horizon.d3circleschart.js b/horizon/static/horizon/js/horizon.d3circleschart.js
new file mode 100644
index 00000000..305b2182
--- /dev/null
+++ b/horizon/static/horizon/js/horizon.d3circleschart.js
@@ -0,0 +1,279 @@
+/*
+ Draw circles chart in d3.
+
+ To use, a div is required with the data attributes
+ data-chart-type="circles_chart", data-url and data-size in the
+ div.
+
+ data-chart-type - must be "circles_chart" so chart gets initialized
+ data-url - (string) url for the json data for the chart
+ data-time - (string) time parameter, gets appended to url as time=...
+ data-size - (integer) size of the circles in pixels
+
+ If used in popup, initialization must be made manually e.g.:
+ addHorizonLoadEvent(function() {
+ horizon.d3_circles_chart.init('.html_element');
+ horizon.d3_circles_chart.init('.health_chart');
+ });
+
+
+ Example:
+ <div class="html_element"
+ data-chart-type="circles_chart"
+ data-url="/infrastructure/racks/1/top_communicating.json?cond=to"
+ data-time="now"
+ data-size="22">
+ </div>
+
+ There are controll elements for the cirles chart, implementing some commands
+ that will be executed over the chart.
+
+ 1. The selectbox for time change, implements ChangeTime command. It has to
+ have data attribute data-circles-chart-command="change_time", with defined
+ receiver as jquery selector (selector can point to more elements and it will
+ execute the command on all of them) e.g. data-receiver="#rack_health_chart"
+ Option value is then appended to url and chart is refreshed.
+
+ Example
+ <div class="span3 circles_chart_time_picker">
+ <select data-circles-chart-command="change_time"
+ data-receiver="#rack_health_chart">
+ <option value="now">Now</option>
+ <option value="yesterday">Yesterday</option>
+ <option value="last_week">Last Week</option>
+ <option value="last_month">Last Month</option>
+ </select>
+ </div>
+ <div class="clear"></div>
+
+ 2. Bootstrap tabs for switching different circle_charts implement command
+ ChangeUrl. It has to have data attribute data-circles-chart-command="change_url",
+ with defined receiver as jquery selector (selector can point to more elements and
+ it will execute the command on all of them) e.g. data-receiver="#rack_health_chart"
+
+ Inner li a element has to have attribute data-url, e.g.
+ data-url="/infrastructure/racks/1/top_communicating.json?type=alerts"
+
+ The url of the chart is then switched and chart is refreshed.
+
+ <ul class="nav nav-tabs"
+ data-circles-chart-command="change_url"
+ data-receiver="#rack_health_chart">
+ <li class="active">
+ <a data-url="/infrastructure/racks/1/top_communicating.json?type=overall_health" href="#">
+ Overall Health</a>
+ </li>
+ <li>
+ <a data-url="/infrastructure/racks/1/top_communicating.json?type=alerts" href="#">
+ Alerts</a>
+ </li>
+ <li>
+ <a data-url="/infrastructure/racks/1/top_communicating.json?type=capacities" href="#">
+ Capacities</a>
+ </li>
+ <li>
+ <a data-url="/infrastructure/racks/1/top_communicating.json?type=status" href="#">
+ Status</a>
+ </li>
+ </ul>
+*/
+
+
+horizon.d3_circles_chart = {
+ CirclesChart: function(chart_class, html_element){
+ this.chart_class = chart_class;
+ this.html_element = html_element;
+
+ var jquery_element = $(html_element);
+ this.size = jquery_element.data('size');
+ this.time = jquery_element.data('time');
+ this.url = jquery_element.data('url');
+
+ this.final_url = this.url;
+ if (this.final_url.indexOf('?') > -1){
+ this.final_url += '&time=' + this.time;
+ }else{
+ this.final_url += '?time=' + this.time;
+ }
+
+ this.time = jquery_element.data('time');
+ this.data = []
+
+ this.refresh = refresh;
+ function refresh(){
+ var self = this;
+ this.jqxhr = $.getJSON( this.final_url, function() {
+ //FIXME add loader in the target element
+ })
+ .done(function(data) {
+ // FIXME find a way how to only update graph with new data
+ // not delete and create
+ $(self.html_element).html("");
+
+ self.data = data.data;
+ self.settings = data.settings;
+ self.chart_class.render(self.html_element, self.size, self.data, self.settings);
+ })
+ .fail(function() {
+ // FIXME add proper fail message
+ console.log( "error" ); })
+ .always(function() {
+ // FIXME add behaviour that should be always done
+ });
+ }
+ },
+ init: function(selector, settings) {
+ var self = this;
+ $(selector).each(function() {
+ self.refresh(this);
+ });
+ self.bind_commands();
+ },
+ refresh: function(html_element){
+ var chart = new this.CirclesChart(this, html_element)
+ // FIXME save chart objects somewhere so I can use them again when
+ // e.g. I am swithing tabs, or if I want to update them
+ // via web sockets
+ // this.charts.add_or_update(chart)
+ chart.refresh();
+ },
+ render: function(html_element, size, data, settings){
+ var self = this;
+ // FIXME rewrite to scatter plot once we have some cool D3 chart
+ // library
+ var width = size + 4,
+ height = size + 4,
+ round = size / 2;
+ center_x = width / 2,
+ center_y = height / 2;
+
+ var svg = d3.select(html_element).selectAll("svg")
+ .data(data)
+ .enter().append("svg")
+ .attr("width", width)
+ .attr("height", height);
+
+ // FIXME use some pretty tooltip from some library we will use
+ // this one is just temporary
+ var tooltip = d3.select(html_element).append("div")
+ .style("position", "absolute")
+ .style("z-index", "10")
+ .style("visibility", "hidden")
+ .style("min-width", "100px")
+ .style("max-width", "110px")
+ .style("min-height", "30px")
+ .style("border", "1px ridge grey")
+ .style("background-color", "white")
+ .text(function(d) { "a simple tooltip"; });
+
+ var circle = svg.append("circle")
+ .attr("r", round)//function(d) { return d.r; })// can be sent form server
+ .attr("cx", center_x)
+ .attr("cy", center_y)
+ .attr("stroke", "#cecece")
+ .attr("stroke-width", function(d) {
+ return 1;
+ })
+ .style("fill", function(d) {
+ if (d.color){
+ return d.color;
+ } else if (settings.scale == "linear_color_scale"){
+ return self.linear_color_scale(d.percentage, settings.domain, settings.range)
+
+ }
+ })
+ .on("mouseover", function(d){
+ if (d.tooltip) {
+ tooltip.html(d.tooltip);
+ } else {
+ tooltip.html(d.name + "<br/>" + d.status);
+ }
+ tooltip.style("visibility", "visible");})
+ .on("mousemove", function(d){tooltip.style("top", (event.pageY-10)+"px").style("left",(event.pageX+10)+"px");})
+ .on("mouseout", function(d){tooltip.style("visibility", "hidden");});
+ ;
+
+ /*
+ // or just d3 title element
+ circle.append("svg:title")
+ .text(function(d) { return d.x; });
+
+ */
+ },
+ linear_color_scale: function(percentage, domain, range){
+ usage_color = d3.scale.linear()
+ .domain(domain)
+ .range(range);
+ return usage_color(percentage);
+ },
+ bind_commands: function (){
+ var change_time_command_selector = 'select[data-circles-chart-command="change_time"]';
+ var change_url_command_selector = '[data-circles-chart-command="change_url"]';
+ var self = this;
+ bind_change_time = function(){
+ $(change_time_command_selector).each(function() {
+ $(this).change(function(){
+ var invoker = $(this);
+ var command = new self.Command.ChangeTime(self, invoker);
+ command.execute();
+ });
+ });
+ }
+ bind_change_url = function(){
+ $(change_url_command_selector + ' a').click(function (e) {
+ // Bootstrap tabs functionality
+ e.preventDefault();
+ $(this).tab('show');
+
+ // Command for url change and refresh
+ var invoker = $(this);
+ var command = new self.Command.ChangeUrl(self, invoker);
+ command.execute();
+ })
+ }
+ bind_change_time();
+ bind_change_url();
+ },
+ Command: {
+ ChangeTime: function (chart_class, invoker){
+ // Invoker of the command should know about it's receiver.
+ // Also invoker brings all parameters of the command.
+ this.receiver_selector = invoker.data('receiver');
+ this.new_time = invoker.find("option:selected").val();
+
+ this.execute = execute;
+ function execute(){
+ var self = this;
+ $(this.receiver_selector).each(function(){
+ // change time of the chart
+ $(this).data('time', self.new_time);
+ // refresh the chart
+ chart_class.refresh(this)
+ });
+ }
+ },
+ ChangeUrl: function (chart_class, invoker, new_url){
+ // Invoker of the command should know about it's receiver.
+ // Also invoker brings all parameters of the command.
+ this.receiver_selector = invoker.parents('ul').first().data('receiver');
+ this.new_url = invoker.data('url');
+
+ this.execute = execute;
+ function execute(){
+ var self = this;
+ $(this.receiver_selector).each(function(){
+ // change time of the chart
+ $(this).data('url', self.new_url);
+ // refresh the chart
+ chart_class.refresh(this)
+ });
+ }
+ }
+ }
+}
+
+/* init the graphs */
+horizon.addInitFunction(function () {
+ horizon.d3_circles_chart.init('div[data-chart-type="circles_chart"]');
+});
+
diff --git a/horizon/static/horizon/js/horizon.d3linechart.js b/horizon/static/horizon/js/horizon.d3linechart.js
new file mode 100644
index 00000000..f84cee82
--- /dev/null
+++ b/horizon/static/horizon/js/horizon.d3linechart.js
@@ -0,0 +1,200 @@
+/*
+ Draw line chart in d3.
+
+ It support 2 types of usage:
+ * as a standard line chart
+ * as a line chart in a modal window
+
+ To use as a standard line chart the following data attributes need to be
+ provided for a div:
+ data-chart-type - must be "line_chart"
+ data-url - (string) url for the json data for the chart
+ data-series - (string) the list of series separated by comma
+
+ Example:
+ <div data-chart-type="line_chart"
+ data-url="/data_url"
+ data-series="cpu,ram,storage,network">
+ </div>
+
+ To use as a line chart in a modal windows the following data attributes
+ need to be provided for a link:
+ data-chart-type - must be "modal_line_chart"
+ data-url - (string) url for the json data for the chart
+ data-series - (string) the list of series separated by comma
+
+ Example:
+ <a data-chart-type="modal_line_chart"
+ data-url="/data_url"
+ data-series="network">
+ Click me!
+ </a>
+*/
+
+horizon.d3_line_chart = {
+
+ init: function() {
+ var self = this;
+
+ self.init_line_charts();
+ self.init_modal_chart_links();
+ },
+
+ init_line_charts: function() {
+ var self = this;
+
+ line_charts = $("div[data-chart-type='line_chart']");
+ $.each(line_charts, function(index, line_chart) {
+ if($(line_chart).data('modal') != true) {
+ var template = horizon.templates.compiled_templates["#modal_chart_template"];
+ self.init_svg(line_chart);
+ self.draw(self.data(line_chart));
+ }
+ });
+ },
+
+ init_modal_chart_links: function() {
+ var self = this;
+
+ $(document).on('click', "a[data-chart-type='modal_line_chart']", function(event) {
+ event.preventDefault();
+
+ var template = horizon.templates.compiled_templates["#modal_chart_template"];
+ $('#modal_wrapper').append(template.render({classes: "modal"}));
+
+ var modal = $('.modal:last');
+ modal.modal();
+
+ $(modal).on('click', 'ul#interval_selector li a', function(event){
+ event.preventDefault();
+
+ self.url_options.interval = $(event.target).data('interval');
+ self.draw(self.url_options);
+ $("ul#interval_selector li").removeClass("active");
+ $(event.target).parent().addClass("active");
+ })
+
+ self.init_svg("#modal_chart");
+ self.url_options = self.data($(event.target))
+ self.draw(self.url_options)
+ });
+ },
+
+ init_svg: function(chart_div) {
+ var self = this;
+
+ self.margin = {top: 20, right: 80, bottom: 30, left: 50};
+ self.width = 550 - self.margin.left - self.margin.right;
+ self.height = 300 - self.margin.top - self.margin.bottom;
+
+ self.svg = d3.select(chart_div)
+ .append("svg")
+ .attr("width", self.width + self.margin.left + self.margin.right)
+ .attr("height", self.height + self.margin.top + self.margin.bottom)
+ .append("g")
+ .attr("transform", "translate(" + self.margin.left + "," + self.margin.top + ")");
+
+ self.x = d3.time.scale().range([0, self.width]);
+ self.y = d3.scale.linear().range([self.height, 0]);
+
+ self.xAxis = d3.svg.axis()
+ .scale(self.x)
+ .orient("bottom").ticks(6);
+ self.yAxis = d3.svg.axis()
+ .scale(self.y)
+ .orient("left");
+
+ self.parse_date = d3.time.format("%Y-%m-%dT%H:%M:%S.%L").parse;
+
+ self.line = d3.svg.line().interpolate("linear")
+ .x(function(d) { return self.x(d.date); })
+ .y(function(d) { return self.y(d.value); });
+
+ self.color = d3.scale.category10();
+ },
+
+ draw: function(url_options) {
+ var self = this;
+ var url_options = url_options || {};
+ url_options.interval = url_options.interval || "1w";
+
+ d3.json(self.json_url(url_options), function(error, data) {
+ data.forEach(function(d) { d.date = self.parse_date(d.date); });
+
+ self.color.domain(d3.keys(data[0]).filter(function(key) { return key !== 'date'; }));
+
+ var usage_values = self.color.domain().map(function(name) {
+ return {
+ name: name,
+ values: data.map(function(d) {
+ return {date: d.date, value: d[name]};
+ })
+ };
+ });
+
+ self.svg.selectAll(".axis").remove();
+ self.x.domain(d3.extent(data, function(d) { return d.date; }));
+ self.y.domain([0, 15]);
+
+ self.svg.append("g")
+ .attr("transform", "translate(0," + self.height + ")")
+ .attr("class", "axis")
+ .call(self.xAxis);
+ self.svg.append("g")
+ .attr("class", "axis")
+ .call(self.yAxis)
+
+ var usages = self.svg.selectAll(".usage")
+ .data(usage_values, function(d) { return self.key(d) });
+
+ var usage = usages.enter().append("g")
+ .attr("class", "usage");
+
+ usage.append("path")
+ .attr("d", function(d) { return self.line(d.values); })
+ .style("stroke", function(d) { return self.color(d.name); })
+ .style("fill", "none")
+ .style("stroke-width", 3);
+
+ var legend = usage.append('g')
+ .attr('class', 'legend');
+
+ legend.append('rect')
+ .attr('x', self.width - 60)
+ .attr('y', function(d, i){ return (i * 20) + 30 ;})
+ .attr('width', 10)
+ .attr('height', 10)
+ .style('fill', function(d) { return self.color(d.name); });
+
+ legend.append('text')
+ .attr('x', self.width - 40)
+ .attr('y', function(d, i){ return (i * 20) + 39;})
+ .text(function(d){ return d.name.replace(/_/g, " "); });
+
+ usages.exit().remove();
+ });
+ },
+
+ data: function(element) {
+ return {
+ url: $(element).data("url"),
+ series: $(element).data("series")
+ };
+ },
+
+ json_url: function(url_options) {
+ var options = $.extend({}, url_options);
+ var url = options.url
+ delete options.url
+ return url + '?' + $.param(options);
+ },
+
+ key: function(data){
+ return data.name + data.values.length + data.values[0].date + data.values[data.values.length-1].date
+ },
+
+};
+
+horizon.addInitFunction(function () {
+ horizon.d3_line_chart.init();
+});
diff --git a/horizon/static/horizon/js/horizon.d3singlebarchart.js b/horizon/static/horizon/js/horizon.d3singlebarchart.js
new file mode 100644
index 00000000..5e89a0e7
--- /dev/null
+++ b/horizon/static/horizon/js/horizon.d3singlebarchart.js
@@ -0,0 +1,474 @@
+/*
+ Used for animating and displaying single-bar information using
+ D3js rect.
+
+ Usage:
+ In order to have single bars that work with this, you need to have a
+ DOM structure like this in your Django template:
+
+ Example:
+ <div class="flavor_usage_bar"
+ data-popup-free='<p>Capacity remaining by flavors: </p>
+ {{resource_class.all_instances_flavors_info}}'
+ data-single-bar-orientation="horizontal"
+ data-single-bar-height="50"
+ data-single-bar-width="100%"
+ data-single-bar-used="{{ resource_class.all_used_instances_info }}"
+ data-single-bar-auto-scale-selector=".flavors_scale_selector"
+ data-single-bar-color-scale-range='["#000060", "#99FFFF"]'>
+ </div>
+
+ The available data- attributes are:
+ data-popup-free, data-popup-used, data-popup-average OPTIONAL
+ Html content of popups that will be displayed over this areas.
+
+ data-single-bar-orientation REQUIRED
+ String representing orientation of the bar.Can be "horizontal"
+ or "vertical"
+
+ data-single-bar-height REQUIRED
+ Integer or string with percent mark format e.g. "50%". Determines
+ the total height of the bar.
+
+ data-single-bar-width="100%" REQUIRED
+ Integer or string with percent mark format e.g. "50%". Determines
+ the total width of the bar.
+
+ data-single-bar-used="integer" REQUIRED
+ 1. Integer
+ Integer representing the percent used.
+ 2. Array
+ Array of following structure:
+ [{"popup_used": "Popup html 1", "used_instances": "5"},
+ {"popup_used": "Popup html 2", "used_instances": "15"},....]
+
+ used_instances: Integer representing the percent used.
+ popup_used: Html that will be displayed in popup window over
+ this area.
+
+ data-single-bar-used-average="integer" OPTIONAL
+ Integer representing the average usage in percent of given
+ single-bar.
+
+ data-single-bar-auto-scale-selector OPTIONAL
+ Jquery selector of bar elements that have Integer
+ data-single-bar-used attribute. It then takes maximum of these
+ values as 100% of the liner scale of the colors.
+ So the array representing linear scale interval is set
+ automatically.This then maps to data-single-bar-color-scale-range.
+ (arrays must have the same structure)
+
+ single-bar-color-scale-range OPTIONAL
+ Array representing linear scale interval that is set manually.
+ E.g "[0,10]". This then maps to data-single-bar-color-scale-range.
+ (arrays must have the same structure)
+
+ data-single-bar-color-scale-range OPTIONAL
+ Array representing linear scale of colors.
+ E.g '["#000060", "#99FFFF"]'
+
+*/
+
+horizon.d3_single_bar_chart = {
+ SingleBarChart: function(chart_class, html_element){
+ var self = this;
+ self.chart_class = chart_class;
+
+ self.html_element = html_element;
+ self.jquery_element = $(self.html_element);
+ // Using only percent, so limit is 100%
+ self.single_bar_limit = 100;
+
+ self.single_bar_used = $.parseJSON(self.jquery_element.attr('data-single-bar-used'));
+ self.average_used = parseInt(self.jquery_element.attr('data-single-bar-average-used'), 10);
+
+ self.data = {};
+
+ // Percentage and used_px count
+ if ($.isArray(self.single_bar_used)){
+ self.data.used_px = 0;
+ self.data.percentage_used = Array();
+ self.data.tooltip_used_contents = Array();
+ for (var i = 0; i < self.single_bar_used.length; ++i) {
+ if (!isNaN(self.single_bar_limit) && !isNaN(self.single_bar_used[i].used_instances)) {
+ used = Math.round((self.single_bar_used[i].used_instances / self.single_bar_limit) * 100);
+
+ self.data.percentage_used.push(used);
+ // for multi-bar chart, tooltip is in the data
+ self.data.tooltip_used_contents.push(self.single_bar_used[i].popup_used);
+
+ self.data.used_px += self.jquery_element.width() / 100 * used;
+ } else { // If NaN self.data.percentage_used is 0
+
+ }
+ }
+
+ }
+ else {
+ if (!isNaN(self.single_bar_limit) && !isNaN(self.single_bar_used)) {
+ self.data.percentage_used = Math.round((self.single_bar_used / self.single_bar_limit) * 100);
+ self.data.used_px = self.jquery_element.width() / 100 * self.data.percentage_used;
+
+ } else { // If NaN self.data.percentage_used is 0
+ self.data.percentage_used = 0;
+ self.data.used_px = 0;
+ }
+
+ if (!isNaN(self.single_bar_limit) && !isNaN(self.average_used)) {
+ self.data.percentage_average = ((self.average_used / self.single_bar_limit) * 100);
+ } else {
+ self.data.percentage_average = 0;
+ }
+ }
+
+ // Width and height of bar
+ self.data.width = self.jquery_element.data('single-bar-width');
+ self.data.height = self.jquery_element.data('single-bar-height');
+
+ // Color scales
+ self.auto_scale_selector = function () {
+ return self.jquery_element.data('single-bar-auto-scale-selector');
+ };
+ self.is_auto_scaling = function () {
+ return self.auto_scale_selector();
+ };
+ self.auto_scale = function () {
+ var max_scale = 0;
+ $(self.auto_scale_selector()).each(function() {
+ var scale = parseInt($(this).data('single-bar-used'));
+ if (scale > max_scale)
+ max_scale = scale;
+ });
+ return [0, max_scale];
+ };
+
+ if (self.jquery_element.data('single-bar-color-scale-domain'))
+ self.data.color_scale_domain =
+ self.jquery_element.data('single-bar-color-scale-domain');
+ else if (self.is_auto_scaling())
+ // Dynamically set scale based on biggest value
+ self.data.color_scale_domain = self.auto_scale();
+ else
+ self.data.color_scale_domain = [0,100];
+
+ if (self.jquery_element.data('single-bar-color-scale-range'))
+ self.data.color_scale_range =
+ self.jquery_element.data('single-bar-color-scale-range');
+ else
+ self.data.color_scale_range = ["#000000", "#0000FF"];
+
+ // Tooltips data
+ self.data.popup_average = self.jquery_element.data('popup-average');
+ self.data.popup_free = self.jquery_element.data('popup-free');
+ self.data.popup_used = self.jquery_element.data('popup-used');
+
+ // Orientation of the Bar chart
+ self.data.orientation = self.jquery_element.data('single-bar-orientation');
+
+ // Refresh method
+ self.refresh = function (){
+ self.chart_class.render(self.html_element, self.data);
+ };
+ },
+ BaseComponent: function(data){
+ var self = this;
+
+ self.data = data;
+
+ self.w = data.width;
+ self.h = data.height;
+ self.lvl_curve = 3;
+ self.bkgrnd = "#F2F2F2";
+ self.frgrnd = "grey";
+ self.color_scale_max = 25;
+
+ self.percentage_used = data.percentage_used;
+ self.total_used_perc = 0;
+ self.used_px = data.used_px;
+ self.percentage_average = data.percentage_average;
+ self.tooltip_used_contents = data.tooltip_used_contents;
+
+ // set scales
+ self.usage_color = d3.scale.linear()
+ .domain(data.color_scale_domain)
+ .range(data.color_scale_range);
+
+ // return true if it renders used percentage multiple in one chart
+ self.used_multi = function (){
+ return ($.isArray(self.percentage_used));
+ };
+
+ // deals with percentage if there should be multiple in one chart
+ self.used_multi_iterator = 0;
+ self.percentage_used_value = function(){
+ if (self.used_multi()){
+ return self.percentage_used[self.used_multi_iterator];
+ } else {
+ return self.percentage_used;
+ }
+ };
+ // deals with html tooltips if there should be multiple in one chart
+ self.tooltip_used_value = function (){
+ if (self.used_multi()){
+ return self.tooltip_used_contents[self.used_multi_iterator];
+ } else {
+ return "";
+ }
+ };
+
+ // return true if it chart is oriented horizontally
+ self.horizontal_orientation = function (){
+ return (self.data.orientation == "horizontal");
+ };
+
+ },
+ UsedComponent: function(base_component){
+ var self = this;
+ self.base_component = base_component;
+
+ // FIXME woud be good to abstract all atributes and resolve orientation inside
+ if (base_component.horizontal_orientation()){
+ // Horizontal Bars
+ self.y = 0;
+ self.x = base_component.total_used_perc + "%";
+ self.width = 0;
+ self.height = base_component.h;
+ self.trasition_attr = "width";
+ self.trasition_value = base_component.percentage_used_value() + "%";
+ } else { // Vertical Bars
+ self.y = base_component.h;
+ self.x = 0;
+ self.width = base_component.w;
+ self.height = base_component.percentage_used_value() + "%";
+ self.trasition_attr = "y";
+ self.trasition_value = 100 - base_component.percentage_used_value() + "%";
+ }
+
+ self.append = function(bar, tooltip){
+ var used_component = self;
+ var base_component = self.base_component;
+
+ bar.append("rect")
+ .attr("class", "usedbar")
+ .attr("y", used_component.y)
+ .attr("x", used_component.x)
+ .attr("width", used_component.width)
+ .attr("height", used_component.height)
+ //.attr("rx", base_component.lvl_curve)
+ //.attr("ry", base_component.lvl_curve)
+ .style("fill", base_component.usage_color(base_component.percentage_used_value()))
+ .style("stroke", "#bebebe")
+ .style("stroke-width", 0)
+ .attr("d", base_component.percentage_used_value())
+ .attr("popup-used", base_component.tooltip_used_value())
+ .on("mouseover", function(d){
+ if ($(this).attr('popup-used')){
+ tooltip.html($(this).attr('popup-used'));
+ }
+ tooltip.style("visibility", "visible");})
+ .on("mousemove", function(d){tooltip.style("top",
+ (event.pageY-10)+"px").style("left",(event.pageX+10)+"px");})
+ .on("mouseout", function(d){tooltip.style("visibility", "hidden");})
+ .transition()
+ .duration(500)
+ .attr(used_component.trasition_attr, used_component.trasition_value);
+ };
+ },
+ AverageComponent: function(base_component){
+ var self = this;
+ self.base_component = base_component;
+
+ // FIXME woud be good to abstract all atributes and resolve orientation inside
+ if (base_component.horizontal_orientation()){
+ // Horizontal Bars
+ self.y = 1;
+ self.x = 0;
+ self.width = 1;
+ self.height = base_component.h;
+ self.trasition_attr = "x";
+ self.trasition_value = base_component.percentage_average + "%";
+ } else { // Vertical Bars
+ self.y = 0;
+ self.x = 0;
+ self.width = base_component.w;
+ self.height = 1;
+ self.trasition_attr = "y";
+ self.trasition_value = 100 - base_component.percentage_average + "%";
+ }
+
+ self.append = function(bar, tooltip){
+ var average_component = self;
+ var base_component = self.base_component;
+
+ bar.append("rect")
+ .attr("y", average_component.y)
+ .attr("x", average_component.x)
+ .attr("class", "average")
+ .attr("height", average_component.height)
+ .attr("width", average_component.width)
+ .style("fill", "black")
+ .on("mouseover", function(){tooltip.style("visibility", "visible");})
+ .on("mousemove", function(){tooltip.style("top",
+ (event.pageY-10)+"px").style("left",(event.pageX+10)+"px");})
+ .on("mouseout", function(){tooltip.style("visibility", "hidden");})
+ .transition()
+ .duration(500)
+ .attr(average_component.trasition_attr, average_component.trasition_value);
+ };
+ },
+ /* TODO rewrite below as components */
+ /* FIXME use some pretty tooltip from some library we will use
+ * this one is just temporary */
+ append_tooltip: function(tooltip, html_content){
+ return tooltip
+ .style("position", "absolute")
+ .style("z-index", "10")
+ .style("visibility", "hidden")
+ .style("min-width", "100px")
+ .style("max-width", "200px")
+ .style("min-height", "30px")
+ .style("max-height", "150px")
+ .style("border", "1px ridge grey")
+ .style("padding", "8px")
+ .style("padding-top", "5px")
+ .style("background-color", "white")
+ .html(html_content);
+ },
+ append_unused: function(bar, base_component, tooltip_free){
+ bar.append("rect")
+ .attr("y", 0)
+ .attr("width", base_component.w)
+ .attr("height", base_component.h)
+ .attr("rx", base_component.lvl_curve)
+ .attr("ry", base_component.lvl_curve)
+ .style("fill", base_component.bkgrnd)
+ .style("stroke", "#e0e0e0")
+ .style("stroke-width", 1)
+ .on("mouseover", function(d){tooltip_free.style("visibility", "visible");})
+ .on("mousemove", function(d){tooltip_free.style("top",
+ (event.pageY-10)+"px").style("left",(event.pageX+10)+"px");})
+ .on("mouseout", function(d){tooltip_free.style("visibility", "hidden");});
+ },
+ // TODO This have to be enhanced, so this library can replace jtomasek capacity charts
+ append_text: function(bar, base_component, tooltip){
+ bar.append("text")
+ .text("FREE")
+ .attr("y", base_component.h/2)
+ .attr("x", 3)
+ .attr("dominant-baseline", "middle")
+ .attr("font-size", 13)
+ .on("mouseover", function(d){tooltip.style("visibility", "visible");})
+ .on("mousemove", function(d){tooltip.style("top",
+ (event.pageY-10)+"px").style("left",(event.pageX+10)+"px");})
+ .on("mouseout", function(d){tooltip.style("visibility", "hidden");})
+ .transition()
+ .duration(500)
+ .attr("x", function() {
+ // FIXME when another panel is active, this page is hidden and used_px return 0
+ // text is then badly positioned, quick fix will be to refresh charts when panel
+ // is switched. Need to find better solution.
+ if (base_component.total_used_perc > 90 && base_component.used_px > 25)
+ return base_component.used_px - 20;
+ else
+ return base_component.used_px + 20;
+ });
+ },
+ append_border: function(bar){
+ bar.append("rect")
+ .attr("x", 0)
+ .attr("y", 0)
+ .attr("height", '100%')
+ .attr("width", '100%')
+ .style("stroke", "#bebebe")
+ .style("fill", "none")
+ .style("stroke-width", 1);
+ },
+ // INIT
+ init: function() {
+ var self = this;
+ this.single_bars = $('div[data-single-bar-used]');
+
+ this.single_bars.each(function() {
+ self.refresh(this);
+ });
+ },
+ refresh: function(html_element){
+ var chart = new this.SingleBarChart(this, html_element);
+ // FIXME save chart objects somewhere so I can use them again when
+ // e.g. I am swithing tabs, or if I want to update them
+ // via web sockets
+ // this.charts.add_or_update(chart)
+ chart.refresh();
+ },
+ render: function(html_element, data) {
+ var jquery_element = $(html_element);
+
+ // Initialize base_component
+ var base_component = new this.BaseComponent(data);
+
+ // Bar
+ var bar_html = d3.select(html_element);
+
+ // Tooltips
+ var tooltip_average = bar_html.append("div");
+ if (data.popup_average)
+ tooltip_average = this.append_tooltip(tooltip_average, data.popup_average);
+
+ var tooltip_free = bar_html.append("div");
+ if (data.popup_free)
+ tooltip_free = this.append_tooltip(tooltip_free, data.popup_free);
+
+ var tooltip_used = bar_html.append("div");
+ if (data.popup_used)
+ tooltip_used = this.append_tooltip(tooltip_used, data.popup_used);
+
+ // append layout for bar chart
+ var bar = bar_html.append("svg:svg")
+ .attr("class", "chart")
+ .attr("width", base_component.w)
+ .attr("height", base_component.h)
+ .style("background-color", "white")
+ .append("g");
+
+ // append Unused resources Bar
+ this.append_unused(bar, base_component, tooltip_free);
+
+ if (base_component.used_multi()){
+ // If Used is shown as multiple values in one chart
+ for (var i = 0; i < base_component.percentage_used.length; ++i) {
+ // FIXME write proper iterator
+ base_component.used_multi_iterator = i;
+
+ // Use general tooltip, content of tooltip will be changed
+ // by inner used compoentnts on their hover
+ tooltip_used = this.append_tooltip(tooltip_used, "");
+
+ // append used so it will be shown as multiple values in one chart
+ var used_component = new this.UsedComponent(base_component);
+ used_component.append(bar, tooltip_used);
+
+ // append Used resources to Bar
+ base_component.total_used_perc += base_component.percentage_used_value();
+ };
+
+ // append Text to Bar
+ this.append_text(bar, base_component, tooltip_free);
+
+ } else {
+ // used is show as one value it the chart
+ var used_component = new this.UsedComponent(base_component);
+ used_component.append(bar, tooltip_used);
+
+ // append average value to Bar
+ var average_component = new this.AverageComponent(base_component);
+ average_component.append(bar, tooltip_average);
+ }
+ // append border of whole Bar
+ this.append_border(bar);
+ },
+};
+
+
+horizon.addInitFunction(function () {
+ horizon.d3_single_bar_chart.init();
+});
diff --git a/horizon/static/horizon/js/horizon.templates.js b/horizon/static/horizon/js/horizon.templates.js
index f228496c..4c393461 100644
--- a/horizon/static/horizon/js/horizon.templates.js
+++ b/horizon/static/horizon/js/horizon.templates.js
@@ -1,6 +1,6 @@
/* Namespace for core functionality related to client-side templating. */
horizon.templates = {
- template_ids: ["#modal_template", "#empty_row_template", "#alert_message_template", "#spinner-modal", "#project_user_template"],
+ template_ids: ["#modal_template", "#modal_chart_template", "#empty_row_template", "#alert_message_template", "#spinner-modal", "#project_user_template"],
compiled_templates: {}
};
diff --git a/horizon/tables/base.py b/horizon/tables/base.py
index 71b39e43..5e9ee06c 100644
--- a/horizon/tables/base.py
+++ b/horizon/tables/base.py
@@ -199,7 +199,11 @@ class Column(html.HTMLElement):
link=None, allowed_data_types=[], hidden=False, attrs=None,
status=False, status_choices=None, display_choices=None,
empty_value=None, filters=None, classes=None, summation=None,
- auto=None, truncate=None, link_classes=None):
+ auto=None, truncate=None, link_classes=None,
+ # FIXME: Added for TableStep:
+ form_widget=None, form_widget_attributes=None
+ ):
+
self.classes = list(classes or getattr(self, "classes", []))
super(Column, self).__init__()
self.attrs.update(attrs or {})
@@ -228,6 +232,8 @@ class Column(html.HTMLElement):
self.filters = filters or []
self.truncate = truncate
self.link_classes = link_classes or []
+ self.form_widget = form_widget # FIXME: TableStep
+ self.form_widget_attributes = form_widget_attributes or {} # TableStep
if status_choices:
self.status_choices = status_choices
@@ -452,10 +458,42 @@ class Row(html.HTMLElement):
cells = []
for column in table.columns.values():
if column.auto == "multi_select":
- widget = forms.CheckboxInput(check_test=lambda value: False)
+
+ # FIXME: TableStep code modified
+ # multi_select fields in the table must be checked after
+ # a server action
+ # TODO: remove this ugly code and create proper TableFormWidget
+ multi_select_values = []
+ if (getattr(table, 'request', False) and
+ getattr(table.request, 'POST', False)):
+ multi_select_values = table.request.POST.getlist(
+ self.table._meta.multi_select_name)
+
+ multi_select_values += getattr(table,
+ 'active_multi_select_values', [])
+
+ if unicode(table.get_object_id(datum)) in multi_select_values:
+ multi_select_value = lambda value: True
+ else:
+ multi_select_value = lambda value: False
+ widget = forms.CheckboxInput(check_test=multi_select_value)
+
# Convert value to string to avoid accidental type conversion
- data = widget.render('object_ids',
+ data = widget.render(self.table._meta.multi_select_name,
unicode(table.get_object_id(datum)))
+ # FIXME: end of added TableStep code
+
+ table._data_cache[column][table.get_object_id(datum)] = data
+ elif column.auto == "form_widget": # FIXME: Added for TableStep:
+ widget = column.form_widget
+ widget_name = "%s__%s__%s" % \
+ (self.table._meta.multi_select_name,
+ column.name,
+ unicode(table.get_object_id(datum)))
+
+ data = widget.render(widget_name,
+ column.get_data(datum),
+ column.form_widget_attributes)
table._data_cache[column][table.get_object_id(datum)] = data
elif column.auto == "actions":
data = table.render_row_actions(datum)
@@ -777,6 +815,10 @@ class DataTableOptions(object):
self.actions_column = getattr(options,
'actions_column',
len(self.row_actions) > 0)
+ # FIXME: TableStep
+ self.multi_select_name = getattr(options,
+ 'multi_select_name',
+ 'object_ids')
self.multi_select = getattr(options,
'multi_select',
len(self.table_actions) > 0)
diff --git a/horizon/templates/horizon/client_side/_modal_chart.html b/horizon/templates/horizon/client_side/_modal_chart.html
new file mode 100644
index 00000000..e26a7d4d
--- /dev/null
+++ b/horizon/templates/horizon/client_side/_modal_chart.html
@@ -0,0 +1,19 @@
+{% extends "horizon/client_side/template.html" %}
+{% load horizon %}
+
+{% block id %}modal_chart_template{% endblock %}
+
+{% block template %}
+{% jstemplate %}
+<div class="[[classes]]" style="top: 80px; display: block;">
+ <ul id="interval_selector">
+ <li><a href="#" data-interval="12h">12h</a></li>
+ <li><a href="#" data-interval="24h">24h</a></li>
+ <li class="active"><a href="#" data-interval="1w">1w</a></li>
+ <li><a href="#" data-interval="1m">1m</a></li>
+ <li><a href="#" data-interval="1y">1y</a></li>
+ </ul>
+ <div id="modal_chart"></div>
+</div>
+{% endjstemplate %}
+{% endblock %}
diff --git a/horizon/templates/horizon/client_side/templates.html b/horizon/templates/horizon/client_side/templates.html
index 81fe0852..eeafe83d 100644
--- a/horizon/templates/horizon/client_side/templates.html
+++ b/horizon/templates/horizon/client_side/templates.html
@@ -1,4 +1,5 @@
{% include "horizon/client_side/_modal.html" %}
+{% include "horizon/client_side/_modal_chart.html" %}
{% include "horizon/client_side/_table_row.html" %}
{% include "horizon/client_side/_alert_message.html" %}
{% include "horizon/client_side/_loading.html" %}
diff --git a/horizon/workflows/__init__.py b/horizon/workflows/__init__.py
index b4ec576f..145a3bd3 100644
--- a/horizon/workflows/__init__.py
+++ b/horizon/workflows/__init__.py
@@ -9,3 +9,7 @@ assert Step
assert UpdateMembersStep
assert Workflow
assert WorkflowView
+
+# FIXME: TableStep adding UpdateMembersStep
+from horizon.workflows.base import UpdateMembersStep
+from horizon.workflows.base import TableStep
diff --git a/horizon/workflows/base.py b/horizon/workflows/base.py
index e1356513..44a7a4a8 100644
--- a/horizon/workflows/base.py
+++ b/horizon/workflows/base.py
@@ -29,6 +29,11 @@ from django.utils.encoding import force_unicode
from django.utils.importlib import import_module
from django.utils.translation import ugettext_lazy as _
+# FIXME: TableStep
+from django.utils.datastructures import SortedDict
+from django.template.defaultfilters import linebreaks, safe
+from django.forms.forms import NON_FIELD_ERRORS
+
from horizon import base
from horizon import exceptions
from horizon.templatetags.horizon import has_permissions
@@ -416,6 +421,11 @@ class Step(object):
step_template = template.loader.get_template(self.template_name)
extra_context = {"form": self.action,
"step": self}
+
+ # FIXME: TableStep:
+ if issubclass(self.__class__, TableStep):
+ extra_context.update(self.get_context_data(self.workflow.request))
+
context = template.RequestContext(self.workflow.request, extra_context)
return step_template.render(context)
@@ -431,6 +441,84 @@ class Step(object):
"""
self.action.add_error(message)
+# FIXME: TableStep
+class TableStep(Step):
+ """
+ A :class:`~horizon.workflows.Step` class which knows how to deal with
+ :class:`~horizon.tables.DataTable` classes rendered inside of it.
+
+ This distinct class is required due to the complexity involved in handling
+ both dynamic tab loading, dynamic table updating and table actions all
+ within one view.
+
+ .. attribute:: table_classes
+
+ An iterable containing the :class:`~horizon.tables.DataTable` classes
+ which this tab will contain. Equivalent to the
+ :attr:`~horizon.tables.MultiTableView.table_classes` attribute on
+ :class:`~horizon.tables.MultiTableView`. For each table class you
+ need to define a corresponding ``get_{{ table_name }}_data`` method
+ as with :class:`~horizon.tables.MultiTableView`.
+ """
+
+ table_classes = None
+
+ def __init__(self, workflow):
+ super(TableStep, self).__init__(workflow)
+ if not self.table_classes:
+ class_name = self.__class__.__name__
+ raise NotImplementedError("You must define a table_class "
+ "attribute on %s" % class_name)
+ # Instantiate our table classes but don't assign data yet
+ table_instances = [(table._meta.name,
+ table(workflow.request, needs_form_wrapper=False))
+ for table in self.table_classes]
+ self._tables = SortedDict(table_instances)
+ self._table_data_loaded = False
+
+ def load_table_data(self):
+ """
+ Calls the ``get_{{ table_name }}_data`` methods for each table class
+ and sets the data on the tables.
+ """
+ # We only want the data to be loaded once, so we track if we have...
+ if not self._table_data_loaded:
+ for table_name, table in self._tables.items():
+ # Fetch the data function.
+ func_name = "get_%s_data" % table_name
+ data_func = getattr(self, func_name, None)
+ if data_func is None:
+ cls_name = self.__class__.__name__
+ raise NotImplementedError("You must define a %s method "
+ "on %s." % (func_name, cls_name))
+ # Load the data.
+ table.data = data_func()
+ table._meta.has_more_data = self.has_more_data(table)
+ # Mark our data as loaded so we don't run the loaders again.
+ self._table_data_loaded = True
+
+ def get_context_data(self, request):
+ """
+ Adds a ``{{ table_name }}_table`` item to the context for each table
+ in the :attr:`~horizon.tabs.TableTab.table_classes` attribute.
+
+ If only one table class is provided, a shortcut ``table`` context
+ variable is also added containing the single table.
+ """
+ context = {}
+ # If the data hasn't been manually loaded before now,
+ # make certain it's loaded before setting the context.
+ self.load_table_data()
+ for table_name, table in self._tables.items():
+ # If there's only one table class, add a shortcut name as well.
+ if len(self.table_classes) == 1:
+ context["table"] = table
+ context["%s_table" % table_name] = table
+ return context
+
+ def has_more_data(self, table):
+ return False
+
class WorkflowMetaclass(type):
def __new__(mcs, name, bases, attrs):
diff --git a/openstack_dashboard/api/__init__.py b/openstack_dashboard/api/__init__.py
index dd3bdedc..50e764f2 100644
--- a/openstack_dashboard/api/__init__.py
+++ b/openstack_dashboard/api/__init__.py
@@ -43,6 +43,7 @@ from openstack_dashboard.api import network
from openstack_dashboard.api import neutron
from openstack_dashboard.api import nova
from openstack_dashboard.api import swift
+from openstack_dashboard.api import tuskar
assert base
assert cinder
@@ -54,3 +55,4 @@ assert nova
assert neutron
assert lbaas
assert swift
+assert tuskar
diff --git a/openstack_dashboard/api/nova.py b/openstack_dashboard/api/nova.py
index ba42c9b0..35535183 100644
--- a/openstack_dashboard/api/nova.py
+++ b/openstack_dashboard/api/nova.py
@@ -30,8 +30,10 @@ from django.utils.translation import ugettext_lazy as _
from novaclient.v1_1 import client as nova_client
from novaclient.v1_1.contrib.list_extensions import ListExtManager
from novaclient.v1_1 import security_group_rules as nova_rules
+from novaclient.v1_1.contrib import baremetal
from novaclient.v1_1.security_groups import SecurityGroup as NovaSecurityGroup
from novaclient.v1_1.servers import REBOOT_HARD
+from novaclient.v1_1.servers import REBOOT_SOFT
from horizon.conf import HORIZON_CONFIG
from horizon.utils.memoized import memoized
diff --git a/openstack_dashboard/api/tuskar.py b/openstack_dashboard/api/tuskar.py
new file mode 100644
index 00000000..bebccc70
--- /dev/null
+++ b/openstack_dashboard/api/tuskar.py
@@ -0,0 +1,1001 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# 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 copy
+import logging
+import re
+from collections import namedtuple
+import itertools
+from datetime import timedelta
+from random import randint
+
+from django.conf import settings
+from django.db.models import Sum, Max
+from django.utils.translation import ugettext_lazy as _
+from horizon import exceptions
+
+from tuskarclient.v1 import client as tuskar_client
+
+from openstack_dashboard.api import base, nova
+import openstack_dashboard.dashboards.infrastructure.models as dummymodels
+
+
+LOG = logging.getLogger(__name__)
+TUSKAR_ENDPOINT_URL = getattr(settings, 'TUSKAR_ENDPOINT_URL')
+NOVA_BAREMETAL_CREDS = getattr(settings, 'NOVA_BAREMETAL_CREDS')
+OVERCLOUD_AUTH_URL = getattr(settings, 'OVERCLOUD_AUTH_URL')
+OVERCLOUD_USERNAME = getattr(settings, 'OVERCLOUD_USERNAME')
+OVERCLOUD_PASSWORD = getattr(settings, 'OVERCLOUD_PASSWORD')
+
+
+# FIXME: request isn't used right in the tuskar client right now, but looking
+# at other clients, it seems like it will be in the future
+def tuskarclient(request):
+ c = tuskar_client.Client(TUSKAR_ENDPOINT_URL)
+ return c
+
+
+class StringIdAPIResourceWrapper(base.APIResourceWrapper):
+ # horizon DataTable class expects ids to be string,
+ # if it's not string, then comparison in
+ # horizon/tables/base.py:get_object_by_id fails.
+ # Because of this, ids returned from dummy api are converted to string
+ # (luckily django autoconverts strings to integers when passing string to
+ # django model id)
+
+ def __init__(self, apiresource, request=None):
+ self.request = request
+ self._apiresource = apiresource
+
+ # FIXME
+ # this is redefined from base.APIResourceWrapper,
+ # remove this when tuskarclient returns object instead of dict
+ def __getattr__(self, attr):
+ if attr in self._attrs:
+ if issubclass(self._apiresource.__class__, dict):
+ return self._apiresource.get(attr)
+ else:
+ return self._apiresource.__getattribute__(attr)
+ else:
+ msg = ('Attempted to access unknown attribute "%s" on '
+ 'APIResource object of type "%s" wrapping resource of '
+ 'type "%s".') % (attr, self.__class__,
+ self._apiresource.__class__)
+ LOG.debug(exceptions.error_color(msg))
+ raise AttributeError(attr)
+
+ @property
+ def id(self):
+ return str(self._apiresource.id)
+
+ # FIXME: self.request is required when calling some instance
+ # methods (e.g. list_flavors), once we really start using this request
+ # param (if ever), a proper request value should be set
+ @property
+ def request(self):
+ return getattr(self, '_request', None)
+
+ @request.setter
+ def request(self, value):
+ setattr(self, '_request', value)
+
+
+class Alert(StringIdAPIResourceWrapper):
+ """Wrapper for the Alert object returned by the
+ dummy model.
+ """
+ _attrs = ['message', 'time']
+
+
+class Capacity(StringIdAPIResourceWrapper):
+ """Wrapper for the Capacity object returned by the
+ dummy model.
+ """
+ _attrs = ['name', 'value', 'unit']
+
+ @classmethod
+ def create(cls, request, content_object, name, value, unit):
+ c = dummymodels.Capacity(
+ content_object=content_object,
+ name=name,
+ value=value,
+ unit=unit)
+ c.save()
+ return Capacity(c)
+
+ @classmethod
+ def update(cls, request, capacity_id, content_object, name, value, unit):
+ c = dummymodels.Capacity.objects.get(id=capacity_id)
+ c.content_object = content_object
+ c.name = name
+ c.value = value
+ c.unit = unit
+ c.save()
+ return cls(c)
+
+ # defines a random usage of capacity - API should probably be able to
+ # determine usage of capacity based on capacity value and obejct_id
+ @property
+ def usage(self):
+ if not hasattr(self, '_usage'):
+ self._usage = randint(0, int(self.value))
+ return self._usage
+
+ # defines a random average of capacity - API should probably be able to
+ # determine average of capacity based on capacity value and obejct_id
+ @property
+ def average(self):
+ if not hasattr(self, '_average'):
+ self._average = randint(0, int(self.value))
+ return self._average
+
+
+class Node(StringIdAPIResourceWrapper):
+ """Wrapper for the Node object returned by the
+ dummy model.
+ """
+ _attrs = ['id', 'pm_address', 'cpus', 'memory_mb', 'service_host',
+ 'local_gb', 'pm_user']
+
+ @classmethod
+ def manager(cls):
+ nc = nova.nova_client.Client(
+ NOVA_BAREMETAL_CREDS['user'],
+ NOVA_BAREMETAL_CREDS['password'],
+ NOVA_BAREMETAL_CREDS['tenant'],
+ auth_url=NOVA_BAREMETAL_CREDS['auth_url'],
+ bypass_url=NOVA_BAREMETAL_CREDS['bypass_url'])
+ return nova.baremetal.BareMetalNodeManager(nc)
+
+ @classmethod
+ def get(cls, request, node_id):
+ node = cls(cls.manager().get(node_id))
+ node.request = request
+
+ # FIXME ugly, fix after demo, make abstraction of instance details
+ # this is realy not optimal, but i dont hve time do fix it now
+ instances, more = nova.server_list(
+ request,
+ search_opts={'paginate': True},
+ all_tenants=True)
+
+ instance_details = {}
+ for instance in instances:
+ id = (instance.
+ _apiresource._info['OS-EXT-SRV-ATTR:hypervisor_hostname'])
+ instance_details[id] = instance
+
+ detail = instance_details.get(node_id)
+ if detail:
+ addresses = detail._apiresource.addresses.get('ctlplane')
+ if addresses:
+ node.ip_address_other = (", "
+ .join([addr['addr'] for addr in addresses]))
+
+ node.status = detail._apiresource._info['OS-EXT-STS:vm_state']
+ node.power_mamanegemt = ""
+ if node.pm_user:
+ node.power_mamanegemt = node.pm_user + "/********"
+ else:
+ node.status = 'unprovisioned'
+
+ return node
+
+ @classmethod
+ def list(cls, request):
+ return cls.manager().list()
+
+ @classmethod
+ def list_unracked(cls, request):
+ return [cls(h) for h in dummymodels.Node.objects.all() if (
+ h.rack is None)]
+
+ @classmethod
+ def create(cls, request, name, mac_address, ip_address, status,
+ usage, rack):
+ node = dummymodels.Node(name=name, mac_address=mac_address,
+ ip_address=ip_address, status=status,
+ usage=usage, rack=rack)
+ node.save()
+
+ @property
+ def list_flavors(self):
+ if not hasattr(self, '_flavors'):
+ # FIXME: just a mock of used instances, add real values
+ used_instances = 0
+
+ if not self.rack or not self.rack.resource_class:
+ return []
+ resource_class = self.rack.resource_class
+
+ added_flavors = tuskarclient(self.request).flavors\
+ .list(resource_class.id)
+ self._flavors = []
+ if added_flavors:
+ for f in added_flavors:
+ flavor_obj = Flavor(f)
+ #flavor_obj.max_vms = f.max_vms
+
+ # FIXME just a mock of used instances, add real values
+ used_instances += 5
+ flavor_obj.used_instances = used_instances
+ self._flavors.append(flavor_obj)
+
+ return self._flavors
+
+ @property
+ def capacities(self):
+ if not hasattr(self, '_capacities'):
+ self._capacities = [Capacity(c) for c in
+ self._apiresource.capacities.all()]
+ return self._capacities
+
+ @property
+ def rack(self):
+ try:
+ if not hasattr(self, '_rack'):
+ # FIXME the node.rack association should be stored somewhere
+ self._rack = None
+ for rack in Rack.list(self.request):
+ for node_obj in rack.list_nodes:
+ if node_obj.id == self.id:
+ self._rack = rack
+
+ return self._rack
+ except:
+ msg = "Could not obtain Nodes's rack"
+ LOG.debug(exceptions.error_color(msg))
+ return None
+
+ @property
+ def cpu(self):
+ if not hasattr(self, '_cpu'):
+ try:
+ cpu = dummymodels.Capacity.objects\
+ .filter(node=self._apiresource)\
+ .filter(name='cpu')[0]
+ except:
+ cpu = dummymodels.Capacity(
+ name='cpu',
+ value=_('Unable to retrieve '
+ '(Is the node configured properly?)'),
+ unit='')
+ self._cpu = Capacity(cpu)
+ return self._cpu
+
+ @property
+ def ram(self):
+ if not hasattr(self, '_ram'):
+ try:
+ ram = dummymodels.Capacity.objects\
+ .filter(node=self._apiresource)\
+ .filter(name='ram')[0]
+ except:
+ ram = dummymodels.Capacity(
+ name='ram',
+ value=_('Unable to retrieve '
+ '(Is the node configured properly?)'),
+ unit='')
+ self._ram = Capacity(ram)
+ return self._ram
+
+ @property
+ def storage(self):
+ if not hasattr(self, '_storage'):
+ try:
+ storage = dummymodels.Capacity.objects\
+ .filter(node=self._apiresource)\
+ .filter(name='storage')[0]
+ except:
+ storage = dummymodels.Capacity(
+ name='storage',
+ value=_('Unable to retrieve '
+ '(Is the node configured properly?)'),
+ unit='')
+ self._storage = Capacity(storage)
+ return self._storage
+
+ @property
+ def network(self):
+ if not hasattr(self, '_network'):
+ try:
+ network = dummymodels.Capacity.objects\
+ .filter(node=self._apiresource)\
+ .filter(name='network')[0]
+ except:
+ network = dummymodels.Capacity(
+ name='network',
+ value=_('Unable to retrieve '
+ '(Is the node configured properly?)'),
+ unit='')
+ self._network = Capacity(network)
+ return self._network
+
+ @property
+ def vm_capacity(self):
+ if not hasattr(self, '_vm_capacity'):
+ try:
+ value = dummymodels.ResourceClassFlavor.objects\
+ .filter(
+ resource_class__rack__node=self._apiresource)\
+ .aggregate(Max("max_vms"))['max_vms__max']
+ except:
+ value = _("Unable to retrieve vm capacity")
+
+ vm_capacity = dummymodels.Capacity(name=_("Max VMs"),
+ value=value,
+ unit=_("VMs"))
+ self._vm_capacity = Capacity(vm_capacity)
+ return self._vm_capacity
+
+ @property
+ # FIXME: just mock implementation, add proper one
+ def running_instances(self):
+ return 4
+
+ @property
+ # FIXME: just mock implementation, add proper one
+ def remaining_capacity(self):
+ return 100 - self.running_instances
+
+ @property
+ # FIXME: just mock implementation, add proper one
+ def is_provisioned(self):
+ return self.status != "unprovisioned" and self.rack
+
+ @property
+ def alerts(self):
+ if not hasattr(self, '_alerts'):
+ self._alerts = [Alert(a) for a in
+ dummymodels.Alert.objects
+ .filter(object_type='node')
+ .filter(object_id=str(self.id))]
+ return self._alerts
+
+ @property
+ def mac_address(self):
+ try:
+ return self._apiresource.interfaces[0]['address']
+ except:
+ return None
+
+ @property
+ def running_virtual_machines(self):
+ if not hasattr(self, '_running_virtual_machines'):
+ search_opts = {}
+ search_opts['all_tenants'] = True
+ nova_client = nova.nova_client.Client(OVERCLOUD_USERNAME,
+ OVERCLOUD_PASSWORD,
+ 'admin',
+ auth_url=OVERCLOUD_AUTH_URL)
+ self._running_virtual_machines = [s for s in
+ nova_client.servers.list(True, search_opts)
+ if s.hostId == self.id]
+ return self._running_virtual_machines
+
+
+class Rack(StringIdAPIResourceWrapper):
+ """Wrapper for the Rack object returned by the
+ dummy model.
+ """
+ _attrs = ['id', 'name', 'location', 'subnet', 'nodes', 'state']
+
+ @classmethod
+ def create(cls, request, name, resource_class_id, location, subnet,
+ nodes=[]):
+ ## FIXME: set nodes here
+ rack = tuskarclient(request).racks.create(
+ name=name,
+ location=location,
+ subnet=subnet,
+ nodes=nodes,
+ resource_class={'id': resource_class_id},
+ slots=0)
+ return cls(rack)
+
+ @classmethod
+ def update(cls, request, rack_id, kwargs):
+ ## FIXME: set nodes here
+ correct_kwargs = copy.copy(kwargs)
+ # remove rack_id from kwargs (othervise it is duplicated)
+ correct_kwargs.pop('rack_id', None)
+ # correct data mapping for resource_class
+ if 'resource_class_id' in correct_kwargs:
+ correct_kwargs['resource_class'] = {
+ 'id': correct_kwargs.pop('resource_class_id', None)}
+
+ rack = tuskarclient(request).racks.update(rack_id, **correct_kwargs)
+ return cls(rack)
+
+ @classmethod
+ def list(cls, request, only_free_racks=False):
+ if only_free_racks:
+ return [Rack(r, request) for r in
+ tuskarclient(request).racks.list() if (
+ r.resource_class is None)]
+ else:
+ return [Rack(r, request) for r in
+ tuskarclient(request).racks.list()]
+
+ @classmethod
+ def get(cls, request, rack_id):
+ rack = cls(tuskarclient(request).racks.get(rack_id))
+ rack.request = request
+ return rack
+
+ @property
+ def resource_class_id(self):
+ rclass = getattr(self._apiresource, 'resource_class', None)
+ return rclass['id'] if rclass else None
+
+ @classmethod
+ def delete(cls, request, rack_id):
+ tuskarclient(request).racks.delete(rack_id)
+
+ @property
+ def node_ids(self):
+ """ List of unicode ids of nodes added to rack"""
+ return [
+ unicode(node['id']) for node in (
+ self._apiresource.nodes)]
+
+ ## FIXME: this will have to be rewritten to ultimately
+ ## fetch nodes from nova baremetal
+ @property
+ def list_nodes(self):
+ if not hasattr(self, '_nodes'):
+ self._nodes = [Node.get(self.request, node['id']) for node in (
+ self._apiresource.nodes)]
+ return self._nodes
+
+ def nodes_count(self):
+ return len(self._apiresource.nodes)
+
+ # The idea here is to take a list of MAC addresses and assign them to
+ # our rack. I'm attaching this here so that we can take one list, versus
+ # potentially making a long series of API calls.
+ # The present implementation makes no attempt at optimization since this
+ # is likely short-lived until a real API is implemented.
+ @classmethod
+ def register_nodes(cls, rack_id, nodes_list):
+ for mac in nodes_list:
+ # search for MAC
+ try:
+ node = dummymodels.Node.objects.get(mac_address=mac)
+ if node is not None:
+ node.rack_id = rack_id
+ node.save()
+ except:
+ # FIXME: It is unclear what we're supposed to do in this case.
+ # I create a new Node, but it's possible we should not
+ # allow new entries here.
+ # FIXME: If this stays, we should probably add Capabilities
+ # here so that graphs work as expected.
+ Node.create(None, mac, mac, None, None, None, rack_id)
+
+ @property
+ def resource_class(self):
+ if not hasattr(self, '_resource_class'):
+ rclass = getattr(self._apiresource, 'resource_class', None)
+ if rclass:
+ self._resource_class = ResourceClass.get(self.request,
+ rclass['id'])
+ else:
+ self._resource_class = None
+ return self._resource_class
+
+ @property
+ def capacities(self):
+ if not hasattr(self, '_capacities'):
+ self._capacities = [Capacity(c) for c in
+ self._apiresource.capacities]
+ return self._capacities
+
+ @property
+ def vm_capacity(self):
+ """ Rack VM Capacity is maximum value from It's Resource Class's
+ Flavors max_vms (considering flavor sizes are multiples).
+ """
+ if not hasattr(self, '_vm_capacity'):
+ try:
+ value = max([flavor.max_vms for flavor in
+ self.resource_class.list_flavors])
+ except:
+ value = None
+ self._vm_capacity = Capacity({'name': "VM Capacity",
+ 'value': value,
+ 'unit': 'VMs'})
+ return self._vm_capacity
+
+ @property
+ def alerts(self):
+ if not hasattr(self, '_alerts'):
+ self._alerts = [Alert(a) for a in
+ dummymodels.Alert.objects
+ .filter(object_type='rack')
+ .filter(object_id=int(self.id))]
+ return self._alerts
+
+ @property
+ def aggregated_alerts(self):
+ # FIXME: for now return only list of nodes (particular alerts are not
+ # used)
+ return [node for node in self.list_nodes if node.alerts]
+
+ @property
+ def list_flavors(self):
+
+ if not hasattr(self, '_flavors'):
+ # FIXME just a mock of used instances, add real values
+ used_instances = 0
+
+ if not self.resource_class:
+ return []
+ added_flavors = tuskarclient(self.request).flavors\
+ .list(self.resource_class.id)
+ self._flavors = []
+ if added_flavors:
+ for f in added_flavors:
+ flavor_obj = Flavor(f)
+ #flavor_obj.max_vms = f.max_vms
+
+ # FIXME just a mock of used instances, add real values
+ used_instances += 2
+ flavor_obj.used_instances = used_instances
+ self._flavors.append(flavor_obj)
+
+ return self._flavors
+
+ @property
+ def all_used_instances(self):
+ return [flavor.used_instances for flavor in self.list_flavors]
+
+ @property
+ def total_instances(self):
+ # FIXME just mock implementation, add proper one
+ return sum(self.all_used_instances)
+
+ @property
+ def remaining_capacity(self):
+ # FIXME just mock implementation, add proper one
+ return 100 - self.total_instances
+
+ @property
+ def is_provisioned(self):
+ return (self.state == 'active') or (self.state == 'error')
+
+ @property
+ def is_provisioning(self):
+ return (self.state == 'provisioning')
+
+ @classmethod
+ def provision(cls, request, rack_id):
+ tuskarclient(request).data_centers.provision_all()
+
+
+class ResourceClass(StringIdAPIResourceWrapper):
+ """Wrapper for the ResourceClass object returned by the
+ dummy model.
+ """
+ _attrs = ['id', 'name', 'service_type', 'racks']
+
+ @classmethod
+ def get(cls, request, resource_class_id):
+ rc = cls(tuskarclient(request).resource_classes.get(resource_class_id))
+ return rc
+
+ @classmethod
+ def create(self, request, name, service_type, flavors):
+ return ResourceClass(
+ tuskarclient(request).resource_classes.create(
+ name=name,
+ service_type=service_type,
+ flavors=flavors))
+
+ @classmethod
+ def list(cls, request):
+ return [cls(rc) for rc in (
+ tuskarclient(request).resource_classes.list())]
+
+ @classmethod
+ def update(cls, request, resource_class_id, **kwargs):
+ resource_class = cls(tuskarclient(request).resource_classes.update(
+ resource_class_id, **kwargs))
+
+ ## FIXME: flavors have to be updated separately, seems less than ideal
+ for flavor_id in resource_class.flavors_ids:
+ Flavor.delete(request, resource_class.id, flavor_id)
+ for flavor in kwargs['flavors']:
+ Flavor.create(request, resource_class.id, **flavor)
+
+ return resource_class
+
+ @property
+ def deletable(self):
+ return (len(self.list_racks) <= 0)
+
+ @classmethod
+ def delete(cls, request, resource_class_id):
+ tuskarclient(request).resource_classes.delete(resource_class_id)
+
+ @property
+ def racks_ids(self):
+ """ List of unicode ids of racks added to resource class """
+ return [
+ unicode(rack['id']) for rack in (
+ self._apiresource.racks)]
+
+ @property
+ def list_racks(self):
+ """ List of racks added to ResourceClass """
+ if not hasattr(self, '_racks'):
+ self._racks = [Rack.get(self.request, rid) for rid in (
+ self.racks_ids)]
+ return self._racks
+
+ @property
+ def capacities(self):
+ """Aggregates Rack capacities values
+ """
+ if not hasattr(self, '_capacities'):
+ capacities = [rack.capacities for rack in self.list_racks]
+
+ def add_capacities(c1, c2):
+ return [Capacity({'name': a.name,
+ 'value': int(a.value) + int(b.value),
+ 'unit': a.unit}) for a, b in zip(c1, c2)]
+
+ self._capacities = reduce(add_capacities, capacities)
+ return self._capacities
+
+ @property
+ def all_racks(self):
+ """ List of racks added to ResourceClass + list of free racks,
+ meaning racks that don't belong to any ResourceClass"""
+ if not hasattr(self, '_all_racks'):
+ self._all_racks =\
+ [r for r in (
+ Rack.list(self.request)) if (
+ r.resource_class_id is None or
+ str(r.resource_class_id) == self.id)]
+ return self._all_racks
+
+ @property
+ def flavors_ids(self):
+ """ List of unicode ids of flavors added to resource class """
+ return [unicode(flavor.id) for flavor in self.list_flavors]
+
+ # FIXME: for now, we display list of flavor templates when
+ # editing a resource class - we have to set id of flavor template, not
+ # flavor
+ @property
+ def flavortemplates_ids(self):
+ """ List of unicode ids of flavor templates added to resource class """
+ return [unicode(ft.flavor_template.id) for ft in self.list_flavors]
+
+ @property
+ def list_flavors(self):
+ if not hasattr(self, '_flavors'):
+ # FIXME just a mock of used instances, add real values
+ used_instances = 0
+
+ added_flavors = tuskarclient(self.request).flavors.list(self.id)
+ self._flavors = []
+ for f in added_flavors:
+ flavor_obj = Flavor(f)
+ #flavor_obj.max_vms = f.max_vms
+
+ # FIXME just a mock of used instances, add real values
+ used_instances += 5
+ flavor_obj.used_instances = used_instances
+ self._flavors.append(flavor_obj)
+
+ return self._flavors
+
+ @property
+ def all_used_instances(self):
+ return [flavor.used_instances for flavor in self.list_flavors]
+
+ @property
+ def total_instances(self):
+ # FIXME just mock implementation, add proper one
+ return sum(self.all_used_instances)
+
+ @property
+ def remaining_capacity(self):
+ # FIXME just mock implementation, add proper one
+ return 100 - self.total_instances
+
+ @property
+ def all_flavors(self):
+ """ Joined relation table resourceclassflavor with all global flavors
+ """
+ if not hasattr(self, '_all_flavors'):
+ my_flavors = self.list_flavors
+ self._all_flavors = []
+ for flavor in FlavorTemplate.list(self.request):
+ fname = "%s.%s" % (self.name, flavor.name)
+ f = next((f for f in my_flavors if f.name == fname), None)
+ if f:
+ flavor.max_vms = f.max_vms
+ self._all_flavors.append(flavor)
+ return self._all_flavors
+
+ @property
+ def nodes(self):
+ if not hasattr(self, '_nodes'):
+ self._nodes = [rack.list_nodes for rack in self.list_racks]
+ return self._nodes
+
+ @property
+ def nodes_count(self):
+ return len(self.nodes)
+
+ @property
+ def racks_count(self):
+ return len(self.racks)
+
+ @property
+ def vm_capacity(self):
+ """ Resource Class VM Capacity is maximum value from It's Flavors
+ max_vms (considering flavor sizes are multiples), multipled by
+ number of Racks in Resource Class.
+ """
+ if not hasattr(self, '_vm_capacity'):
+ try:
+ value = self.racks_count * max([flavor.max_vms for flavor in
+ self.list_flavors])
+ except:
+ value = _("Unable to retrieve vm capacity")
+ self._vm_capacity = Capacity({'name': _("VM Capacity"),
+ 'value': value,
+ 'unit': _('VMs')})
+ return self._vm_capacity
+
+ def set_racks(self, request, racks_ids):
+ # FIXME: there is a bug now in tuskar, we have to remove all racks at
+ # first and then add new ones:
+ # https://github.com/tuskar/tuskar/issues/37
+ tuskarclient(request).resource_classes.update(self.id, racks=[])
+ racks = [{'id': rid} for rid in racks_ids]
+ tuskarclient(request).resource_classes.update(self.id, racks=racks)
+
+ @property
+ def aggregated_alerts(self):
+ # FIXME: for now return only list of racks (particular alerts are not
+ # used)
+ return [rack for rack in self.list_racks if (rack.alerts +
+ rack.aggregated_alerts)]
+
+ @property
+ def has_provisioned_rack(self):
+ return any([rack.is_provisioned for rack in self.list_racks])
+
+
+class FlavorTemplate(StringIdAPIResourceWrapper):
+ """Wrapper for the Flavor object returned by the
+ dummy model.
+ """
+ _attrs = ['name']
+
+ @property
+ def max_vms(self):
+ return getattr(self, '_max_vms', '0')
+
+ @max_vms.setter
+ def max_vms(self, value='0'):
+ self._max_vms = value
+
+ @property
+ def used_instances(self):
+ return getattr(self, '_used_instances', 0)
+
+ @used_instances.setter
+ def used_instances(self, value=0):
+ self._used_instances = value
+
+ @classmethod
+ def list(cls, request, only_free_racks=False):
+ return [cls(f) for f in dummymodels.FlavorTemplate.objects.all()]
+
+ @classmethod
+ def get(cls, request, flavor_id):
+ return cls(dummymodels.FlavorTemplate.objects.get(id=flavor_id))
+
+ @classmethod
+ def create(cls, request,
+ name, cpu, memory, storage, ephemeral_disk, swap_disk):
+ template = dummymodels.FlavorTemplate(name=name)
+ template.save()
+ Capacity.create(request, template, 'cpu', cpu, '')
+ Capacity.create(request, template, 'memory', memory, 'MB')
+ Capacity.create(request, template, 'storage', storage, 'GB')
+ Capacity.create(request,
+ template, 'ephemeral_disk', ephemeral_disk, 'GB')
+ Capacity.create(request, template, 'swap_disk', swap_disk, 'MB')
+
+ @property
+ def capacities(self):
+ if not hasattr(self, '_capacities'):
+ self._capacities = [Capacity(c) for c in
+ self._apiresource.capacities.all()]
+ return self._capacities
+
+ def capacity(self, capacity_name):
+ key = "_%s" % capacity_name
+ if not hasattr(self, key):
+ try:
+ capacity = [c for c in self.capacities if (
+ c.name == capacity_name)][0]
+ except:
+ capacity = dummymodels.Capacity(
+ name=capacity_name,
+ value=_('Unable to retrieve '
+ '(Is the flavor configured properly?)'),
+ unit='')
+ setattr(self, key, capacity)
+ return getattr(self, key)
+
+ @property
+ def cpu(self):
+ return self.capacity('cpu')
+
+ @property
+ def memory(self):
+ return self.capacity('memory')
+
+ @property
+ def storage(self):
+ return self.capacity('storage')
+
+ @property
+ def ephemeral_disk(self):
+ return self.capacity('ephemeral_disk')
+
+ @property
+ def swap_disk(self):
+ return self.capacity('swap_disk')
+
+ @property
+ def running_virtual_machines(self):
+ # FIXME: arbitrary number
+ return randint(0, int(self.cpu.value))
+
+ # defines a random average of capacity - API should probably be able to
+ # determine average of capacity based on capacity value and obejct_id
+ def vms_over_time(self, start_time, end_time):
+ values = []
+ current_time = start_time
+ while current_time <= end_time:
+ values.append(
+ {'date': current_time,
+ 'active_vms': randint(0, self.running_virtual_machines)})
+ current_time += timedelta(hours=1)
+
+ return values
+
+ @classmethod
+ def update(cls, request, template_id, name, cpu, memory, storage,
+ ephemeral_disk, swap_disk):
+ t = dummymodels.FlavorTemplate.objects.get(id=template_id)
+ t.name = name
+ t.save()
+ template = cls(t)
+ Capacity.update(request, template.cpu.id, template._apiresource,
+ 'cpu', cpu, '')
+ Capacity.update(request, template.memory.id, template._apiresource,
+ 'memory', memory, 'MB')
+ Capacity.update(request, template.storage.id, template._apiresource,
+ 'storage', storage, 'GB')
+ Capacity.update(request, template.ephemeral_disk.id,
+ template._apiresource, 'ephemeral_disk',
+ ephemeral_disk, 'GB')
+ Capacity.update(request, template.swap_disk.id, template._apiresource,
+ 'swap_disk', swap_disk, 'MB')
+ return template
+
+ @classmethod
+ def delete(cls, request, template_id):
+ dummymodels.FlavorTemplate.objects.get(id=template_id).delete()
+
+
+class Flavor(StringIdAPIResourceWrapper):
+ """Wrapper for the Flavor object returned by Tuskar.
+ """
+ _attrs = ['name', 'max_vms']
+
+ @classmethod
+ def create(cls, request, resource_class_id, name, max_vms, capacities):
+ return cls(tuskarclient(request).flavors.create(
+ resource_class_id,
+ name=name,
+ max_vms=max_vms,
+ capacities=capacities))
+
+ @classmethod
+ def delete(cls, request, resource_class_id, flavor_id):
+ tuskarclient(request).flavors.delete(resource_class_id, flavor_id)
+
+ # FIXME: returns flavor template for this flavor
+ @property
+ def flavor_template(self):
+ # strip resource class prefix from flavor name before comparing:
+ fname = re.sub(r'^.*\.', '', self.name)
+ return next(f for f in FlavorTemplate.list(None) if (
+ f.name == fname))
+
+ @property
+ def capacities(self):
+ if not hasattr(self, '_capacities'):
+ ## FIXME: should we distinguish between tuskar
+ ## capacities and our internal capacities?
+ CapacityStruct = namedtuple('CapacityStruct', 'name value unit')
+ self._capacities = [Capacity(CapacityStruct(
+ name=c['name'],
+ value=c['value'],
+ unit=c['unit'])) for c in self._apiresource.capacities]
+ return self._capacities
+
+ def capacity(self, capacity_name):
+ key = "_%s" % capacity_name
+ if not hasattr(self, key):
+ try:
+ capacity = [c for c in self.capacities if (
+ c.name == capacity_name)][0]
+ except:
+ # FIXME: test this
+ capacity = Capacity(
+ name=capacity_name,
+ value=_('Unable to retrieve '
+ '(Is the flavor configured properly?)'),
+ unit='')
+ setattr(self, key, capacity)
+ return getattr(self, key)
+
+ @property
+ def cpu(self):
+ return self.capacity('cpu')
+
+ @property
+ def memory(self):
+ return self.capacity('memory')
+
+ @property
+ def storage(self):
+ return self.capacity('storage')
+
+ @property
+ def ephemeral_disk(self):
+ return self.capacity('ephemeral_disk')
+
+ @property
+ def swap_disk(self):
+ return self.capacity('swap_disk')
+
+ @property
+ def running_virtual_machines(self):
+ # FIXME: arbitrary number
+ return randint(0, int(self.cpu.value))
+
+ # defines a random average of capacity - API should probably be able to
+ # determine average of capacity based on capacity value and obejct_id
+ def vms_over_time(self, start_time, end_time):
+ values = []
+ current_time = start_time
+ while current_time <= end_time:
+ values.append({'date': current_time,
+ 'value': randint(0, self.running_virtual_machines)})
+ current_time += timedelta(hours=1)
+
+ return values
diff --git a/openstack_dashboard/dashboards/infrastructure/__init__.py b/openstack_dashboard/dashboards/infrastructure/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/__init__.py
diff --git a/openstack_dashboard/dashboards/infrastructure/dashboard.py b/openstack_dashboard/dashboards/infrastructure/dashboard.py
new file mode 100644
index 00000000..287aaba1
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/dashboard.py
@@ -0,0 +1,30 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from django.utils.translation import ugettext_lazy as _
+
+import horizon
+
+
+class Infrastructure(horizon.Dashboard):
+ name = _("Infrastructure")
+ slug = "infrastructure"
+ panels = ('resource_management',)
+ default_panel = 'resource_management'
+ permissions = ('openstack.roles.admin',)
+
+
+horizon.register(Infrastructure)
diff --git a/openstack_dashboard/dashboards/infrastructure/fixtures/initial_data.json b/openstack_dashboard/dashboards/infrastructure/fixtures/initial_data.json
new file mode 100644
index 00000000..281af6e4
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/fixtures/initial_data.json
@@ -0,0 +1,94 @@
+[
+ {"pk": 1, "model": "infrastructure.flavortemplate", "fields": {"name": "nano"}},
+ {"pk": 2, "model": "infrastructure.flavortemplate", "fields": {"name": "micro"}},
+ {"pk": 3, "model": "infrastructure.flavortemplate", "fields": {"name": "tiny"}},
+ {"pk": 4, "model": "infrastructure.flavortemplate", "fields": {"name": "small"}},
+ {"pk": 5, "model": "infrastructure.flavortemplate", "fields": {"name": "medium"}},
+ {"pk": 6, "model": "infrastructure.flavortemplate", "fields": {"name": "large"}},
+ {"pk": 7, "model": "infrastructure.flavortemplate", "fields": {"name": "xlarge"}},
+
+ {"pk": 1, "model": "infrastructure.node", "fields": {"name": "node1", "rack": 1, "mac_address": "00-B0-D0-86-AB-F7", "ip_address": "192.168.191.11", "status": "active", "usage": "20"}},
+ {"pk": 2, "model": "infrastructure.node", "fields": {"name": "node2", "rack": 1, "mac_address": "00-B0-D0-86-AB-F8", "ip_address": "192.168.191.12", "status": "active", "usage": "30"}},
+ {"pk": 3, "model": "infrastructure.node", "fields": {"name": "node3", "rack": 2, "mac_address": "00-B0-D0-86-AB-F9", "ip_address": "192.168.191.13", "status": "active", "usage": "40"}},
+ {"pk": 4, "model": "infrastructure.node", "fields": {"name": "node4", "rack": 2, "mac_address": "00-B0-D0-86-AB-F0", "ip_address": "192.168.191.14", "status": "active", "usage": "50"}},
+ {"pk": 5, "model": "infrastructure.node", "fields": {"name": "Unracked Node", "mac_address": "00-B0-D0-86-AB-F1"}},
+
+ {"pk": 1, "model": "infrastructure.rack", "fields": {"name": "rack1", "resource_class": 1, "location": "Boston DC 1", "subnet": "10.16.25.0/24", "status": "active"}},
+ {"pk": 2, "model": "infrastructure.rack", "fields": {"name": "rack2", "resource_class": 1, "location": "Toronto - 151 Front St.", "subnet": "24.50.60.0/22", "status": "active"}},
+
+ {"pk": 1, "model": "infrastructure.resourceclass", "fields": {"service_type": "compute", "name": "m1"}},
+
+ {"pk": 1, "model": "infrastructure.resourceclassflavor", "fields": {"flavortemplate": 1, "resource_class": 1, "max_vms": 128}},
+ {"pk": 2, "model": "infrastructure.resourceclassflavor", "fields": {"flavortemplate": 2, "resource_class": 1, "max_vms": 64}},
+ {"pk": 3, "model": "infrastructure.resourceclassflavor", "fields": {"flavortemplate": 3, "resource_class": 1, "max_vms": 32}},
+ {"pk": 4, "model": "infrastructure.resourceclassflavor", "fields": {"flavortemplate": 4, "resource_class": 1, "max_vms": 16}},
+ {"pk": 5, "model": "infrastructure.resourceclassflavor", "fields": {"flavortemplate": 5, "resource_class": 1, "max_vms": 8}},
+ {"pk": 6, "model": "infrastructure.resourceclassflavor", "fields": {"flavortemplate": 6, "resource_class": 1, "max_vms": 4}},
+ {"pk": 7, "model": "infrastructure.resourceclassflavor", "fields": {"flavortemplate": 7, "resource_class": 1, "max_vms": 2}},
+
+ {"pk": 1, "model": "infrastructure.capacity", "fields": {"value": 4, "unit": "GHz", "object_id": 1, "content_type": ["infrastructure", "node"], "name": "cpu"}},
+ {"pk": 2, "model": "infrastructure.capacity", "fields": {"value": 4, "unit": "GHz", "object_id": 2, "content_type": ["infrastructure", "node"], "name": "cpu"}},
+ {"pk": 3, "model": "infrastructure.capacity", "fields": {"value": 4, "unit": "GHz", "object_id": 3, "content_type": ["infrastructure", "node"], "name": "cpu"}},
+ {"pk": 4, "model": "infrastructure.capacity", "fields": {"value": 4, "unit": "GHz", "object_id": 4, "content_type": ["infrastructure", "node"], "name": "cpu"}},
+ {"pk": 5, "model": "infrastructure.capacity", "fields": {"value": 4, "unit": "GHz", "object_id": 5, "content_type": ["infrastructure", "node"], "name": "cpu"}},
+ {"pk": 6, "model": "infrastructure.capacity", "fields": {"value": 32, "unit": "GB", "object_id": 1, "content_type": ["infrastructure", "node"], "name": "ram"}},
+ {"pk": 7, "model": "infrastructure.capacity", "fields": {"value": 32, "unit": "GB", "object_id": 2, "content_type": ["infrastructure", "node"], "name": "ram"}},
+ {"pk": 8, "model": "infrastructure.capacity", "fields": {"value": 32, "unit": "GB", "object_id": 3, "content_type": ["infrastructure", "node"], "name": "ram"}},
+ {"pk": 9, "model": "infrastructure.capacity", "fields": {"value": 32, "unit": "GB", "object_id": 4, "content_type": ["infrastructure", "node"], "name": "ram"}},
+ {"pk": 10, "model": "infrastructure.capacity", "fields": {"value": 32, "unit": "GB", "object_id": 5, "content_type": ["infrastructure", "node"], "name": "ram"}},
+ {"pk": 11, "model": "infrastructure.capacity", "fields": {"value": 12, "unit": "TB", "object_id": 1, "content_type": ["infrastructure", "node"], "name": "storage"}},
+ {"pk": 12, "model": "infrastructure.capacity", "fields": {"value": 12, "unit": "TB", "object_id": 2, "content_type": ["infrastructure", "node"], "name": "storage"}},
+ {"pk": 13, "model": "infrastructure.capacity", "fields": {"value": 12, "unit": "TB", "object_id": 3, "content_type": ["infrastructure", "node"], "name": "storage"}},
+ {"pk": 14, "model": "infrastructure.capacity", "fields": {"value": 12, "unit": "TB", "object_id": 4, "content_type": ["infrastructure", "node"], "name": "storage"}},
+ {"pk": 15, "model": "infrastructure.capacity", "fields": {"value": 12, "unit": "TB", "object_id": 5, "content_type": ["infrastructure", "node"], "name": "storage"}},
+ {"pk": 16, "model": "infrastructure.capacity", "fields": {"value": 90, "unit": "Gbps", "object_id": 1, "content_type": ["infrastructure", "node"], "name": "network"}},
+ {"pk": 17, "model": "infrastructure.capacity", "fields": {"value": 90, "unit": "Gbps", "object_id": 2, "content_type": ["infrastructure", "node"], "name": "network"}},
+ {"pk": 18, "model": "infrastructure.capacity", "fields": {"value": 90, "unit": "Gbps", "object_id": 3, "content_type": ["infrastructure", "node"], "name": "network"}},
+ {"pk": 19, "model": "infrastructure.capacity", "fields": {"value": 90, "unit": "Gbps", "object_id": 4, "content_type": ["infrastructure", "node"], "name": "network"}},
+ {"pk": 20, "model": "infrastructure.capacity", "fields": {"value": 90, "unit": "Gbps", "object_id": 5, "content_type": ["infrastructure", "node"], "name": "network"}},
+
+ {"pk": 21, "model": "infrastructure.capacity", "fields": {"value": 1, "unit": "", "object_id": 1, "content_type": ["infrastructure", "flavortemplate"], "name": "cpu"}},
+ {"pk": 22, "model": "infrastructure.capacity", "fields": {"value": 64, "unit": "MB", "object_id": 1, "content_type": ["infrastructure", "flavortemplate"], "name": "memory"}},
+ {"pk": 23, "model": "infrastructure.capacity", "fields": {"value": 0, "unit": "GB", "object_id": 1, "content_type": ["infrastructure", "flavortemplate"], "name": "storage"}},
+ {"pk": 24, "model": "infrastructure.capacity", "fields": {"value": 0, "unit": "GB", "object_id": 1, "content_type": ["infrastructure", "flavortemplate"], "name": "ephemeral_disk"}},
+ {"pk": 25, "model": "infrastructure.capacity", "fields": {"value": 0, "unit": "MB", "object_id": 1, "content_type": ["infrastructure", "flavortemplate"], "name": "swap_disk"}},
+
+ {"pk": 26, "model": "infrastructure.capacity", "fields": {"value": 1, "unit": "", "object_id": 2, "content_type": ["infrastructure", "flavortemplate"], "name": "cpu"}},
+ {"pk": 27, "model": "infrastructure.capacity", "fields": {"value": 128, "unit": "MB", "object_id": 2, "content_type": ["infrastructure", "flavortemplate"], "name": "memory"}},
+ {"pk": 28, "model": "infrastructure.capacity", "fields": {"value": 0, "unit": "GB", "object_id": 2, "content_type": ["infrastructure", "flavortemplate"], "name": "storage"}},
+ {"pk": 29, "model": "infrastructure.capacity", "fields": {"value": 0, "unit": "GB", "object_id": 2, "content_type": ["infrastructure", "flavortemplate"], "name": "ephemeral_disk"}},
+ {"pk": 30, "model": "infrastructure.capacity", "fields": {"value": 0, "unit": "MB", "object_id": 2, "content_type": ["infrastructure", "flavortemplate"], "name": "swap_disk"}},
+
+ {"pk": 31, "model": "infrastructure.capacity", "fields": {"value": 1, "unit": "", "object_id": 3, "content_type": ["infrastructure", "flavortemplate"], "name": "cpu"}},
+ {"pk": 32, "model": "infrastructure.capacity", "fields": {"value": 512, "unit": "MB", "object_id": 3, "content_type": ["infrastructure", "flavortemplate"], "name": "memory"}},
+ {"pk": 33, "model": "infrastructure.capacity", "fields": {"value": 1, "unit": "GB", "object_id": 3, "content_type": ["infrastructure", "flavortemplate"], "name": "storage"}},
+ {"pk": 34, "model": "infrastructure.capacity", "fields": {"value": 0, "unit": "GB", "object_id": 3, "content_type": ["infrastructure", "flavortemplate"], "name": "ephemeral_disk"}},
+ {"pk": 35, "model": "infrastructure.capacity", "fields": {"value": 0, "unit": "MB", "object_id": 3, "content_type": ["infrastructure", "flavortemplate"], "name": "swap_disk"}},
+
+ {"pk": 36, "model": "infrastructure.capacity", "fields": {"value": 1, "unit": "", "object_id": 4, "content_type": ["infrastructure", "flavortemplate"], "name": "cpu"}},
+ {"pk": 37, "model": "infrastructure.capacity", "fields": {"value": 2048, "unit": "MB", "object_id": 4, "content_type": ["infrastructure", "flavortemplate"], "name": "memory"}},
+ {"pk": 38, "model": "infrastructure.capacity", "fields": {"value": 20, "unit": "GB", "object_id": 4, "content_type": ["infrastructure", "flavortemplate"], "name": "storage"}},
+ {"pk": 39, "model": "infrastructure.capacity", "fields": {"value": 0, "unit": "GB", "object_id": 4, "content_type": ["infrastructure", "flavortemplate"], "name": "ephemeral_disk"}},
+ {"pk": 40, "model": "infrastructure.capacity", "fields": {"value": 0, "unit": "MB", "object_id": 4, "content_type": ["infrastructure", "flavortemplate"], "name": "swap_disk"}},
+
+ {"pk": 41, "model": "infrastructure.capacity", "fields": {"value": 2, "unit": "", "object_id": 5, "content_type": ["infrastructure", "flavortemplate"], "name": "cpu"}},
+ {"pk": 42, "model": "infrastructure.capacity", "fields": {"value": 4096, "unit": "MB", "object_id": 5, "content_type": ["infrastructure", "flavortemplate"], "name": "memory"}},
+ {"pk": 43, "model": "infrastructure.capacity", "fields": {"value": 40, "unit": "GB", "object_id": 5, "content_type": ["infrastructure", "flavortemplate"], "name": "storage"}},
+ {"pk": 44, "model": "infrastructure.capacity", "fields": {"value": 0, "unit": "GB", "object_id": 5, "content_type": ["infrastructure", "flavortemplate"], "name": "ephemeral_disk"}},
+ {"pk": 45, "model": "infrastructure.capacity", "fields": {"value": 0, "unit": "MB", "object_id": 5, "content_type": ["infrastructure", "flavortemplate"], "name": "swap_disk"}},
+
+ {"pk": 46, "model": "infrastructure.capacity", "fields": {"value": 4, "unit": "", "object_id": 6, "content_type": ["infrastructure", "flavortemplate"], "name": "cpu"}},
+ {"pk": 47, "model": "infrastructure.capacity", "fields": {"value": 8192, "unit": "MB", "object_id": 6, "content_type": ["infrastructure", "flavortemplate"], "name": "memory"}},
+ {"pk": 48, "model": "infrastructure.capacity", "fields": {"value": 80, "unit": "GB", "object_id": 6, "content_type": ["infrastructure", "flavortemplate"], "name": "storage"}},
+ {"pk": 49, "model": "infrastructure.capacity", "fields": {"value": 0, "unit": "GB", "object_id": 6, "content_type": ["infrastructure", "flavortemplate"], "name": "ephemeral_disk"}},
+ {"pk": 50, "model": "infrastructure.capacity", "fields": {"value": 0, "unit": "MB", "object_id": 6, "content_type": ["infrastructure", "flavortemplate"], "name": "swap_disk"}},
+
+ {"pk": 51, "model": "infrastructure.capacity", "fields": {"value": 8, "unit": "", "object_id": 7, "content_type": ["infrastructure", "flavortemplate"], "name": "cpu"}},
+ {"pk": 52, "model": "infrastructure.capacity", "fields": {"value": 16384, "unit": "MB", "object_id": 7, "content_type": ["infrastructure", "flavortemplate"], "name": "memory"}},
+ {"pk": 53, "model": "infrastructure.capacity", "fields": {"value": 160, "unit": "GB", "object_id": 7, "content_type": ["infrastructure", "flavortemplate"], "name": "storage"}},
+ {"pk": 54, "model": "infrastructure.capacity", "fields": {"value": 0, "unit": "GB", "object_id": 7, "content_type": ["infrastructure", "flavortemplate"], "name": "ephemeral_disk"}},
+ {"pk": 55, "model": "infrastructure.capacity", "fields": {"value": 0, "unit": "MB", "object_id": 7, "content_type": ["infrastructure", "flavortemplate"], "name": "swap_disk"}},
+ {"pk": 1, "model": "infrastructure.alert", "fields": {"message": "Switch is not accessible.", "object_id": "1", "object_type": "rack", "time": "2011-09-01T13:20:30+03:00"}},
+ {"pk": 2, "model": "infrastructure.alert", "fields": {"message": "Nova service is not running.", "object_id": "1", "object_type": "node", "time": "2011-09-01T13:20:30+03:00"}},
+ {"pk": 3, "model": "infrastructure.alert", "fields": {"message": "Disk usage is over 90%.", "object_id": "1", "object_type": "node", "time": "2011-09-01T13:20:30+03:00"}}
+]
diff --git a/openstack_dashboard/dashboards/infrastructure/models.py b/openstack_dashboard/dashboards/infrastructure/models.py
new file mode 100644
index 00000000..8097e5b1
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/models.py
@@ -0,0 +1,97 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Red Hat, 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.
+
+# FIXME: configuration for dummy data
+from django.db import models
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.contenttypes import generic
+
+
+class Capacity(models.Model):
+ content_type = models.ForeignKey(ContentType)
+ object_id = models.PositiveIntegerField()
+ content_object = generic.GenericForeignKey('content_type', 'object_id')
+
+ name = models.CharField(max_length=50)
+ value = models.PositiveIntegerField()
+ unit = models.CharField(max_length=10)
+
+
+class Alert(models.Model):
+ class Meta:
+ db_table = 'infrastructure_alerts'
+
+ object_id = models.CharField(max_length=50)
+ object_type = models.CharField(max_length=20)
+ message = models.CharField(max_length=250)
+ time = models.DateTimeField()
+
+
+class FlavorTemplate(models.Model):
+ class Meta:
+ db_table = 'infrastructure_flavortemplate'
+
+ name = models.CharField(max_length=50, unique=True)
+ capacities = generic.GenericRelation(Capacity)
+
+
+class Node(models.Model):
+ class Meta:
+ db_table = 'infrastructure_node'
+
+ name = models.CharField(max_length=50, unique=True)
+ mac_address = models.CharField(max_length=50, unique=True)
+ ip_address = models.CharField(max_length=50, unique=True, null=True)
+ status = models.CharField(max_length=50, null=True)
+ usage = models.IntegerField(max_length=50, null=True)
+ rack = models.ForeignKey('Rack', null=True)
+ capacities = generic.GenericRelation(Capacity)
+
+
+class Rack(models.Model):
+ class Meta:
+ db_table = 'infrastructure_rack'
+
+ name = models.CharField(max_length=50, unique=True)
+ resource_class = models.ForeignKey('ResourceClass', blank=True, null=True)
+ capacities = generic.GenericRelation(Capacity)
+ location = models.CharField(max_length=50)
+ subnet = models.CharField(max_length=50, unique=True)
+ status = models.CharField(max_length=50)
+
+
+class ResourceClass(models.Model):
+ class Meta:
+ # syncdb by default creates 'openstack_dashboard_resourceclass' table,
+ # but it's better to keep models under
+ # openstack_dashboard/dashboards/infrastructure/models.py instead of
+ # openstack_dashboard/models.py since the models.py stub file is
+ # required here anyway
+ db_table = 'infrastructure_resourceclass'
+
+ name = models.CharField(max_length=50, unique=True)
+ service_type = models.CharField(max_length=50)
+ status = models.CharField(max_length=10, null=True, blank=True)
+
+
+class ResourceClassFlavor(models.Model):
+ class Meta:
+ db_table = 'infrastructure_resourceclass_flavors'
+
+ # ResourceClass db model is not used anymore
+ flavortemplate = models.ForeignKey('FlavorTemplate')
+ resource_class = models.ForeignKey('ResourceClass')
+ max_vms = models.PositiveIntegerField(max_length=50, null=True)
diff --git a/openstack_dashboard/dashboards/infrastructure/overview/__init__.py b/openstack_dashboard/dashboards/infrastructure/overview/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/overview/__init__.py
diff --git a/openstack_dashboard/dashboards/infrastructure/overview/models.py b/openstack_dashboard/dashboards/infrastructure/overview/models.py
new file mode 100644
index 00000000..14d87bfb
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/overview/models.py
@@ -0,0 +1,19 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Red Hat, 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.
+
+"""
+Stub file to work around django bug: https://code.djangoproject.com/ticket/7198
+"""
diff --git a/openstack_dashboard/dashboards/infrastructure/overview/panel.py b/openstack_dashboard/dashboards/infrastructure/overview/panel.py
new file mode 100644
index 00000000..e8aee3fe
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/overview/panel.py
@@ -0,0 +1,29 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from django.utils.translation import ugettext_lazy as _
+
+import horizon
+
+from openstack_dashboard.dashboards.infrastructure import dashboard
+
+
+class Overview(horizon.Panel):
+ name = _("Overview")
+ slug = "overview"
+
+
+#dashboard.Infrastructure.register(Overview)
diff --git a/openstack_dashboard/dashboards/infrastructure/overview/templates/overview/index.html b/openstack_dashboard/dashboards/infrastructure/overview/templates/overview/index.html
new file mode 100644
index 00000000..0c616106
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/overview/templates/overview/index.html
@@ -0,0 +1,12 @@
+{% extends 'infrastructure/base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Overview" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Overview") %}
+{% endblock page_header %}
+
+{% block infrastructure_main %}
+{% endblock %}
+
+
diff --git a/openstack_dashboard/dashboards/infrastructure/overview/tests.py b/openstack_dashboard/dashboards/infrastructure/overview/tests.py
new file mode 100644
index 00000000..0a6778a5
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/overview/tests.py
@@ -0,0 +1,23 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from openstack_dashboard.test import helpers as test
+
+
+class OverviewTests(test.TestCase):
+ # Unit tests for overview.
+ def test_me(self):
+ self.assertTrue(1 + 1 == 2)
diff --git a/openstack_dashboard/dashboards/infrastructure/overview/urls.py b/openstack_dashboard/dashboards/infrastructure/overview/urls.py
new file mode 100644
index 00000000..d6e4d2d5
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/overview/urls.py
@@ -0,0 +1,24 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from django.conf.urls.defaults import patterns, url
+
+from .views import IndexView
+
+
+urlpatterns = patterns('',
+ url(r'^$', IndexView.as_view(), name='index'),
+)
diff --git a/openstack_dashboard/dashboards/infrastructure/overview/views.py b/openstack_dashboard/dashboards/infrastructure/overview/views.py
new file mode 100644
index 00000000..9e85aa0c
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/overview/views.py
@@ -0,0 +1,26 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from horizon import views
+
+
+class IndexView(views.APIView):
+ # A very simple class-based view...
+ template_name = 'infrastructure/overview/index.html'
+
+ def get_data(self, request, context, *args, **kwargs):
+ # Add data to the context here...
+ return context
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/__init__.py b/openstack_dashboard/dashboards/infrastructure/resource_management/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/__init__.py
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/flavors/__init__.py b/openstack_dashboard/dashboards/infrastructure/resource_management/flavors/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/flavors/__init__.py
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/flavors/forms.py b/openstack_dashboard/dashboards/infrastructure/resource_management/flavors/forms.py
new file mode 100644
index 00000000..ef84a261
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/flavors/forms.py
@@ -0,0 +1,108 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+#
+# 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.core import validators
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import exceptions
+from horizon import forms
+from horizon import messages
+
+from openstack_dashboard import api
+
+
+class CreateFlavor(forms.SelfHandlingForm):
+ name = forms.RegexField(label=_("Name"),
+ max_length=25,
+ regex=r'^[\w\.\- ]+$',
+ error_messages={'invalid': _('Name may only '
+ 'contain letters, numbers, underscores, '
+ 'periods and hyphens.')})
+ cpu = forms.IntegerField(label=_("VCPU"),
+ min_value=0,
+ initial=0)
+ memory = forms.IntegerField(label=_("RAM (MB)"),
+ min_value=0,
+ initial=0)
+ storage = forms.IntegerField(label=_("Root Disk (GB)"),
+ min_value=0,
+ initial=0)
+ ephemeral_disk = forms.IntegerField(label=_("Ephemeral Disk (GB)"),
+ min_value=0,
+ initial=0)
+ swap_disk = forms.IntegerField(label=_("Swap Disk (MB)"),
+ min_value=0,
+ initial=0)
+
+ def clean(self):
+ cleaned_data = super(CreateFlavor, self).clean()
+ name = cleaned_data.get('name')
+ flavor_id = self.initial.get('flavor_id', None)
+ try:
+ flavors = api.tuskar.FlavorTemplate.list(self.request)
+ except:
+ flavors = []
+ msg = _('Unable to get flavor list')
+ exceptions.check_message(["Connection", "refused"], msg)
+ raise
+ # Check if there is no flavor with the same name
+ for flavor in flavors:
+ if flavor.name == name and flavor.id != flavor_id:
+ raise forms.ValidationError(
+ _('The name "%s" is already used by another flavor.')
+ % name
+ )
+ return cleaned_data
+
+ def handle(self, request, data):
+ try:
+ flavor = api.tuskar.FlavorTemplate.create(
+ request,
+ data['name'],
+ data['cpu'],
+ data['memory'],
+ data['storage'],
+ data['ephemeral_disk'],
+ data['swap_disk']
+ )
+ msg = _('Created flavor "%s".') % data['name']
+ messages.success(request, msg)
+ return True
+ except:
+ exceptions.handle(request, _("Unable to create flavor."))
+
+
+class EditFlavor(CreateFlavor):
+
+ def handle(self, request, data):
+ try:
+ flavor = api.tuskar.FlavorTemplate.update(
+ self.request,
+ self.initial['flavor_id'],
+ data['name'],
+ data['cpu'],
+ data['memory'],
+ data['storage'],
+ data['ephemeral_disk'],
+ data['swap_disk']
+ )
+
+ msg = _('Updated flavor "%s".') % data['name']
+ messages.success(request, msg)
+ return True
+ except:
+ exceptions.handle(request, _("Unable to update flavor."))
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/flavors/tables.py b/openstack_dashboard/dashboards/infrastructure/resource_management/flavors/tables.py
new file mode 100644
index 00000000..bbf48c3c
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/flavors/tables.py
@@ -0,0 +1,94 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import logging
+
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import tables
+
+from openstack_dashboard import api
+
+
+LOG = logging.getLogger(__name__)
+
+
+class DeleteFlavors(tables.DeleteAction):
+ data_type_singular = _("Flavor")
+ data_type_plural = _("Flavors")
+
+ def delete(self, request, obj_id):
+ api.tuskar.FlavorTemplate.delete(request, obj_id)
+
+
+class CreateFlavor(tables.LinkAction):
+ name = "create"
+ verbose_name = _("Create Flavor")
+ url = "horizon:infrastructure:resource_management:flavors:create"
+ classes = ("ajax-modal", "btn-create")
+
+
+class EditFlavor(tables.LinkAction):
+ name = "edit"
+ verbose_name = _("Edit Flavor")
+ url = "horizon:infrastructure:resource_management:flavors:edit"
+ classes = ("ajax-modal", "btn-edit")
+
+
+class FlavorsFilterAction(tables.FilterAction):
+
+ def filter(self, table, flavors, filter_string):
+ """ Naive case-insensitive search. """
+ q = filter_string.lower()
+ return [flavor for flavor in instances
+ if q in flavor.name.lower()]
+
+
+class FlavorsTable(tables.DataTable):
+ name = tables.Column('name',
+ link=("horizon:infrastructure:"
+ "resource_management:flavors:detail"),
+ verbose_name=_('Flavor Name'))
+ cpu = tables.Column(
+ "cpu",
+ verbose_name=_('VCPU'),
+ filters=(lambda x: getattr(x, 'value', ''),)
+ )
+ memory = tables.Column(
+ "memory",
+ verbose_name=_('RAM (MB)'),
+ filters=(lambda x: getattr(x, 'value', ''),)
+ )
+ storage = tables.Column(
+ "storage",
+ verbose_name=_('Root Disk (GB)'),
+ filters=(lambda x: getattr(x, 'value', ''),)
+ )
+ ephemeral_disk = tables.Column(
+ "ephemeral_disk",
+ verbose_name=_('Ephemeral Disk (GB)'),
+ filters=(lambda x: getattr(x, 'value', ''),)
+ )
+ swap_disk = tables.Column(
+ "swap_disk",
+ verbose_name=_('Swap Disk (MB)'),
+ filters=(lambda x: getattr(x, 'value', ''),)
+ )
+
+ class Meta:
+ name = "flavors"
+ verbose_name = _("Flavors")
+ table_actions = (CreateFlavor, DeleteFlavors, FlavorsFilterAction)
+ row_actions = (EditFlavor, DeleteFlavors)
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/flavors/tabs.py b/openstack_dashboard/dashboards/infrastructure/resource_management/flavors/tabs.py
new file mode 100644
index 00000000..8010e3e1
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/flavors/tabs.py
@@ -0,0 +1,34 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# 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 tabs
+
+
+class OverviewTab(tabs.Tab):
+ name = _("Overview")
+ slug = "flavor_overview_tab"
+ template_name = ("infrastructure/resource_management/flavors/"
+ "_detail_overview.html")
+ preload = False
+
+ def get_context_data(self, request):
+ return {"flavor": self.tab_group.kwargs['flavor']}
+
+
+class FlavorDetailTabs(tabs.TabGroup):
+ slug = "flavor_detail_tabs"
+ tabs = (OverviewTab,)
+ sticky = True
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/flavors/tests.py b/openstack_dashboard/dashboards/infrastructure/resource_management/flavors/tests.py
new file mode 100644
index 00000000..fbedce8f
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/flavors/tests.py
@@ -0,0 +1,118 @@
+from django import http
+from django.core.urlresolvers import reverse
+from mox import IsA
+
+from openstack_dashboard import api
+from openstack_dashboard.test import helpers as test
+
+
+class FlavorTemplatesTests(test.BaseAdminViewTests):
+
+ @test.create_stubs({api.tuskar.FlavorTemplate: ('list', 'create')})
+ def test_create_flavor_template(self):
+ template = self.tuskar_flavor_templates.first()
+
+ api.tuskar.FlavorTemplate.list(
+ IsA(http.HttpRequest)).AndReturn([])
+ api.tuskar.FlavorTemplate.create(IsA(http.HttpRequest),
+ template.name,
+ 0, 0, 0, 0, 0).AndReturn(template)
+ self.mox.ReplayAll()
+
+ url = reverse(
+ 'horizon:infrastructure:resource_management:flavors:create')
+ resp = self.client.get(url)
+ self.assertEqual(resp.status_code, 200)
+ self.assertTemplateUsed(
+ resp, "infrastructure/resource_management/flavors/create.html")
+
+ data = {'name': template.name,
+ 'cpu': 0,
+ 'memory': 0,
+ 'storage': 0,
+ 'ephemeral_disk': 0,
+ 'swap_disk': 0}
+ resp = self.client.post(url, data)
+ self.assertRedirectsNoFollow(
+ resp, reverse('horizon:infrastructure:resource_management:index'))
+
+ @test.create_stubs({api.tuskar.FlavorTemplate: ('list', 'update', 'get')})
+ def test_edit_flavor_template_get(self):
+ template = self.tuskar_flavor_templates.first() # has no extra spec
+
+ api.tuskar.FlavorTemplate.get(IsA(http.HttpRequest),
+ template.id).AndReturn(template)
+ self.mox.ReplayAll()
+
+ url = reverse(
+ 'horizon:infrastructure:resource_management:flavors:edit',
+ args=[template.id])
+ resp = self.client.get(url)
+ self.assertEqual(resp.status_code, 200)
+ self.assertTemplateUsed(
+ resp, "infrastructure/resource_management/flavors/edit.html")
+
+ @test.create_stubs({api.tuskar.FlavorTemplate: ('list', 'update', 'get')})
+ def test_edit_flavor_template_post(self):
+ template = self.tuskar_flavor_templates.first() # has no extra spec
+
+ api.tuskar.FlavorTemplate.list(IsA(http.HttpRequest)).AndReturn(
+ self.tuskar_flavor_templates.list())
+ api.tuskar.FlavorTemplate.update(IsA(http.HttpRequest),
+ template.id,
+ template.name,
+ 0, 0, 0, 0, 0).AndReturn(template)
+ api.tuskar.FlavorTemplate.get(IsA(http.HttpRequest),
+ template.id).AndReturn(template)
+ self.mox.ReplayAll()
+
+ data = {'flavor_id': template.id,
+ 'name': template.name,
+ 'cpu': 0,
+ 'memory': 0,
+ 'storage': 0,
+ 'ephemeral_disk': 0,
+ 'swap_disk': 0}
+ url = reverse(
+ 'horizon:infrastructure:resource_management:flavors:edit',
+ args=[template.id])
+ resp = self.client.post(url, data)
+ self.assertNoFormErrors(resp)
+ self.assertMessageCount(success=1)
+ self.assertRedirectsNoFollow(
+ resp, reverse('horizon:infrastructure:resource_management:index'))
+
+ @test.create_stubs({api.tuskar.FlavorTemplate: ('list', 'delete')})
+ def test_delete_flavor_template(self):
+ template = self.tuskar_flavor_templates.first()
+
+ api.tuskar.FlavorTemplate.list(IsA(http.HttpRequest)).\
+ AndReturn(self.tuskar_flavor_templates.list())
+ api.tuskar.FlavorTemplate.delete(IsA(http.HttpRequest), template.id)
+ self.mox.ReplayAll()
+
+ form_data = {'action': 'flavors__delete__%s' % template.id}
+ res = self.client.post(
+ reverse('horizon:infrastructure:resource_management:index'),
+ form_data)
+
+ self.assertRedirectsNoFollow(
+ res, reverse('horizon:infrastructure:resource_management:index'))
+
+ @test.create_stubs({api.tuskar.FlavorTemplate: ('get',)})
+ def test_detail_flavor_template(self):
+ template = self.tuskar_flavor_templates.first()
+
+ api.tuskar.FlavorTemplate.get(IsA(http.HttpRequest),
+ template.id).AndReturn(template)
+ api.tuskar.FlavorTemplate.resource_classes = self. \
+ tuskar_resource_classes
+
+ self.mox.ReplayAll()
+
+ url = reverse(
+ 'horizon:infrastructure:resource_management:flavors:detail',
+ args=[template.id])
+ res = self.client.get(url)
+ self.assertTemplateUsed(
+ res, "infrastructure/resource_management/flavors/detail.html")
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/flavors/urls.py b/openstack_dashboard/dashboards/infrastructure/resource_management/flavors/urls.py
new file mode 100644
index 00000000..aca72419
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/flavors/urls.py
@@ -0,0 +1,34 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from django.conf.urls.defaults import patterns, url
+
+from .views import (CreateView, DetailView, EditView,
+ DetailEditView, ActiveInstancesDataView)
+
+
+FLAVORS = r'^(?P<flavor_id>[^/]+)/%s$'
+VIEW_MOD = 'openstack_dashboard.dashboards.infrastructure.' \
+ 'resource_management.flavors.views'
+
+urlpatterns = patterns(VIEW_MOD,
+ url(r'^create/$', CreateView.as_view(), name='create'),
+ url(FLAVORS % 'edit/$', EditView.as_view(), name='edit'),
+ url(FLAVORS % 'detail_edit/$',
+ DetailEditView.as_view(), name='detail_edit'),
+ url(FLAVORS % 'detail', DetailView.as_view(), name='detail'),
+ url(FLAVORS % 'active_instances_data',
+ ActiveInstancesDataView.as_view(), name='active_instances_data')
+)
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/flavors/views.py b/openstack_dashboard/dashboards/infrastructure/resource_management/flavors/views.py
new file mode 100644
index 00000000..bd82666a
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/flavors/views.py
@@ -0,0 +1,129 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+#
+# 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 datetime import datetime, timedelta
+import json
+import logging
+
+from django.core.serializers.json import DjangoJSONEncoder
+from django.core.urlresolvers import reverse, reverse_lazy
+from django.http import HttpResponse
+from django.utils.translation import ugettext_lazy as _
+from django.views.generic import View
+
+from horizon import exceptions
+from horizon import forms
+from horizon import tables
+from horizon import tabs
+
+from openstack_dashboard import api
+from .forms import CreateFlavor, EditFlavor
+from .tables import FlavorsTable
+from .tabs import FlavorDetailTabs
+
+
+LOG = logging.getLogger(__name__)
+
+
+class CreateView(forms.ModalFormView):
+ form_class = CreateFlavor
+ template_name = 'infrastructure/resource_management/flavors/create.html'
+ success_url = reverse_lazy(
+ 'horizon:infrastructure:resource_management:index')
+
+
+class EditView(forms.ModalFormView):
+ form_class = EditFlavor
+ template_name = 'infrastructure/resource_management/flavors/edit.html'
+ form_url = 'horizon:infrastructure:resource_management:flavors:edit'
+ success_url = reverse_lazy(
+ 'horizon:infrastructure:resource_management:index')
+
+ def get_context_data(self, **kwargs):
+ context = super(EditView, self).get_context_data(**kwargs)
+ context['flavor_id'] = self.kwargs['flavor_id']
+ context['form_url'] = self.form_url
+ context['success_url'] = self.get_success_url()
+ return context
+
+ def get_initial(self):
+ try:
+ flavor = api.tuskar.FlavorTemplate.get(
+ self.request, self.kwargs['flavor_id'])
+ except:
+ exceptions.handle(self.request,
+ _("Unable to retrieve flavor data."))
+ return {'flavor_id': flavor.id,
+ 'name': flavor.name,
+ 'cpu': flavor.cpu.value,
+ 'memory': flavor.memory.value,
+ 'storage': flavor.storage.value,
+ 'ephemeral_disk': flavor.ephemeral_disk.value,
+ 'swap_disk': flavor.swap_disk.value}
+
+
+class DetailEditView(EditView):
+ form_url = 'horizon:infrastructure:resource_management:flavors:detail_edit'
+ success_url = 'horizon:infrastructure:resource_management:flavors:detail'
+
+ def get_success_url(self):
+ return reverse(self.success_url,
+ args=(self.kwargs['flavor_id'],))
+
+
+class DetailView(tabs.TabView):
+ tab_group_class = FlavorDetailTabs
+ template_name = 'infrastructure/resource_management/flavors/detail.html'
+
+ def get_context_data(self, **kwargs):
+ context = super(DetailView, self).get_context_data(**kwargs)
+ context["flavor"] = self.get_data()
+ return context
+
+ def get_data(self):
+ if not hasattr(self, "_flavor"):
+ try:
+ flavor_id = self.kwargs['flavor_id']
+ flavor = api.tuskar.FlavorTemplate.get(self.request, flavor_id)
+ except:
+ redirect = reverse('horizon:infrastructure:'
+ 'resource_management:index')
+ exceptions.handle(self.request,
+ _('Unable to retrieve details for '
+ 'flavor "%s".')
+ % flavor_id,
+ redirect=redirect)
+ self._flavor = flavor
+ return self._flavor
+
+ def get_tabs(self, request, *args, **kwargs):
+ flavor = self.get_data()
+ return self.tab_group_class(request, flavor=flavor,
+ **kwargs)
+
+
+class ActiveInstancesDataView(View):
+
+ def get(self, request, *args, **kwargs):
+ try:
+ flavor = api.tuskar.FlavorTemplate.get(
+ self.request, self.kwargs['flavor_id'])
+ values = flavor.vms_over_time(
+ datetime.now() - timedelta(days=7), datetime.now())
+ return HttpResponse(json.dumps(values, cls=DjangoJSONEncoder),
+ mimetype='application/json')
+ except:
+ exceptions.handle(self.request,
+ _("Unable to retrieve flavor data."))
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/nodes/__init__.py b/openstack_dashboard/dashboards/infrastructure/resource_management/nodes/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/nodes/__init__.py
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/nodes/tables.py b/openstack_dashboard/dashboards/infrastructure/resource_management/nodes/tables.py
new file mode 100644
index 00000000..2bcf1cdc
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/nodes/tables.py
@@ -0,0 +1,62 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# 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
+
+
+class DeleteNodes(tables.DeleteAction):
+ data_type_singular = _("Node")
+ data_type_plural = _("Nodes")
+
+ def delete(self, request, obj_id):
+ api.tuskar.node_delete(request, obj_id)
+
+
+class NodesFilterAction(tables.FilterAction):
+ def filter(self, table, nodes, filter_string):
+ """ Naive case-insensitive search. """
+ q = filter_string.lower()
+ return [node for node in nodes if q in node.name.lower()]
+
+
+class NodesTable(tables.DataTable):
+ service_host = tables.Column("service_host",
+ link=("horizon:infrastructure:"
+ "resource_management:nodes:detail"),
+ verbose_name=_("Name"))
+ mac_address = tables.Column("mac_address", verbose_name=_("MAC Address"))
+ pm_address = tables.Column("pm_address", verbose_name=_("IP Address"))
+ status = tables.Column("status",
+ verbose_name=_("Status"))
+ usage = tables.Column("usage",
+ verbose_name=_("Usage"))
+
+ class Meta:
+ name = "nodes"
+ verbose_name = _("Nodes")
+ table_actions = (DeleteNodes, NodesFilterAction)
+ row_actions = (DeleteNodes,)
+
+
+class UnrackedNodesTable(NodesTable):
+
+ class Meta:
+ name = "unracked_nodes"
+ verbose_name = _("Unracked Nodes")
+ table_actions = ()
+ row_actions = ()
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/nodes/tabs.py b/openstack_dashboard/dashboards/infrastructure/resource_management/nodes/tabs.py
new file mode 100644
index 00000000..4c63ae50
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/nodes/tabs.py
@@ -0,0 +1,34 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# 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 tabs
+
+
+class OverviewTab(tabs.Tab):
+ name = _("Overview")
+ slug = "node_overview_tab"
+ template_name = ("infrastructure/resource_management/nodes/"
+ "_detail_overview.html")
+ preload = False
+
+ def get_context_data(self, request):
+ return {"node": self.tab_group.kwargs['node']}
+
+
+class NodeDetailTabs(tabs.TabGroup):
+ slug = "node_detail_tabs"
+ tabs = (OverviewTab,)
+ sticky = True
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/nodes/tests.py b/openstack_dashboard/dashboards/infrastructure/resource_management/nodes/tests.py
new file mode 100644
index 00000000..16865b8b
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/nodes/tests.py
@@ -0,0 +1,28 @@
+from django.core.urlresolvers import reverse
+from openstack_dashboard.test import helpers as test
+from openstack_dashboard import api
+from mox import IsA
+from django import http
+
+
+class ResourceViewTests(test.BaseAdminViewTests):
+ unracked_page = reverse('horizon:infrastructure:'
+ 'resource_management:nodes:unracked')
+
+ @test.create_stubs({api.tuskar.Node: ('list_unracked',), })
+ def test_unracked(self):
+ unracked_nodes = self.tuskar_racks.list()
+
+ api.tuskar.Node.list_unracked(IsA(http.HttpRequest)) \
+ .AndReturn(unracked_nodes)
+
+ self.mox.ReplayAll()
+
+ res = self.client.get(self.unracked_page)
+
+ self.assertTemplateUsed(res,
+ 'infrastructure/resource_management/nodes/unracked.html')
+
+ unracked_nodes_table = res.context['unracked_nodes_table'].data
+
+ self.assertItemsEqual(unracked_nodes_table, unracked_nodes)
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/nodes/urls.py b/openstack_dashboard/dashboards/infrastructure/resource_management/nodes/urls.py
new file mode 100644
index 00000000..5752b44d
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/nodes/urls.py
@@ -0,0 +1,29 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from django.conf.urls.defaults import patterns, url
+
+from .views import DetailView
+from .views import UnrackedView
+
+
+NODES = r'^(?P<node_id>[^/]+)/%s$'
+VIEW_MOD = 'openstack_dashboard.dashboards.infrastructure.' \
+ 'resource_management.nodes.views'
+
+
+urlpatterns = patterns(VIEW_MOD,
+ url(NODES % 'detail', DetailView.as_view(), name='detail'),
+ url(r'^unracked/$', UnrackedView.as_view(), name='unracked'),
+)
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/nodes/views.py b/openstack_dashboard/dashboards/infrastructure/resource_management/nodes/views.py
new file mode 100644
index 00000000..ca73a49a
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/nodes/views.py
@@ -0,0 +1,70 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# 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 exceptions
+from horizon import tables
+from horizon import tabs
+
+from openstack_dashboard import api
+
+from .tabs import NodeDetailTabs
+from .tables import UnrackedNodesTable
+
+
+class UnrackedView(tables.DataTableView):
+ table_class = UnrackedNodesTable
+ template_name = 'infrastructure/resource_management/nodes/unracked.html'
+
+ def get_data(self):
+ try:
+ nodes = api.tuskar.Node.list_unracked(self.request)
+ except:
+ nodes = []
+ exceptions.handle(self.request,
+ _('Unable to retrieve nodes.'))
+ return nodes
+
+
+class DetailView(tabs.TabView):
+ tab_group_class = NodeDetailTabs
+ template_name = 'infrastructure/resource_management/nodes/detail.html'
+
+ def get_context_data(self, **kwargs):
+ context = super(DetailView, self).get_context_data(**kwargs)
+ context["node"] = self.get_data()
+ return context
+
+ def get_data(self):
+ if not hasattr(self, "_node"):
+ try:
+ node_id = self.kwargs['node_id']
+ node = api.tuskar.Node.get(self.request, node_id)
+ except:
+ redirect = reverse('horizon:infrastructure:'
+ 'resource_management:index')
+ exceptions.handle(self.request,
+ _('Unable to retrieve details for '
+ 'node "%s".')
+ % node_id,
+ redirect=redirect)
+ self._node = node
+ return self._node
+
+ def get_tabs(self, request, *args, **kwargs):
+ node = self.get_data()
+ return self.tab_group_class(request, node=node,
+ **kwargs)
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/panel.py b/openstack_dashboard/dashboards/infrastructure/resource_management/panel.py
new file mode 100644
index 00000000..97ec0e13
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/panel.py
@@ -0,0 +1,29 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from django.utils.translation import ugettext_lazy as _
+
+import horizon
+
+from openstack_dashboard.dashboards.infrastructure import dashboard
+
+
+class Resource_Management(horizon.Panel):
+ name = _("Resource Management")
+ slug = "resource_management"
+
+
+dashboard.Infrastructure.register(Resource_Management)
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/racks/__init__.py b/openstack_dashboard/dashboards/infrastructure/resource_management/racks/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/racks/__init__.py
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/racks/forms.py b/openstack_dashboard/dashboards/infrastructure/resource_management/racks/forms.py
new file mode 100644
index 00000000..d525091a
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/racks/forms.py
@@ -0,0 +1,154 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+import re
+
+from django.core import validators
+from django.utils.translation import ugettext_lazy as _
+from django.forms import ValidationError
+from django import template
+
+from horizon import exceptions
+from horizon import forms
+from horizon import messages
+
+from openstack_dashboard import api
+
+import csv
+import base64
+import StringIO
+import logging
+
+LOG = logging.getLogger(__name__)
+
+
+class UploadRack(forms.SelfHandlingForm):
+ csv_file = forms.FileField(label=_("Choose CSV File"),
+ help_text=("CSV file with rack definitions"),
+ required=False)
+ uploaded_data = forms.CharField(widget=forms.HiddenInput(),
+ required=False)
+
+ def clean_csv_file(self):
+ csv_file = self.cleaned_data['csv_file']
+ data = csv_file.read() if csv_file else None
+
+ if 'upload' in self.request.POST:
+ if not csv_file:
+ raise ValidationError(_('CSV file not set.'))
+ else:
+ try:
+ CSVRack.from_str(data)
+ except Exception as e:
+ LOG.exception("Failed to parse rack CSV file.")
+ raise ValidationError(_('Failed to parse CSV file.'))
+ return data
+
+ def clean_uploaded_data(self):
+ data = self.cleaned_data['uploaded_data']
+ if 'add_racks' in self.request.POST:
+ if not data:
+ raise ValidationError(_('Upload CSV file first'))
+ elif 'upload' in self.request.POST:
+ # reset obsolete uploaded data
+ self.data['uploaded_data'] = None
+ return data
+
+ def handle(self, request, data):
+ if 'upload' in self.request.POST:
+ # if upload button was pressed, stay on the same page
+ # but show content of the CSV file in table
+ racks_str = self.cleaned_data['csv_file']
+ self.initial['racks'] = CSVRack.from_str(racks_str)
+ self.data['uploaded_data'] = base64.b64encode(racks_str)
+ return False
+ else:
+ fails = []
+ successes = []
+ racks_str = self.cleaned_data['uploaded_data']
+ racks = CSVRack.from_str(base64.b64decode(racks_str))
+ # get the resource class ids by resource class names
+ rclass_ids = dict((rc.name, rc.id) for rc in
+ api.tuskar.ResourceClass.list(request))
+ for rack in racks:
+ try:
+ r = api.tuskar.Rack.create(request, rack.name,
+ rclass_ids[rack.resource_class], rack.region,
+ rack.subnet)
+ api.tuskar.Rack.register_nodes(r, rack.nodes)
+ successes.append(rack.name)
+ except:
+ LOG.exception("Exception in processing rack CSV file.")
+ fails.append(rack.name)
+ if successes:
+ messages.success(request,
+ _('Added %d racks.') % len(successes))
+ if fails:
+ messages.error(request,
+ _('Failed to add following racks: %s') %
+ (',').join(fails))
+ return True
+
+
+class CSVRack:
+ def __init__(self, **kwargs):
+ self.id = kwargs['id']
+ self.name = kwargs['name']
+ self.resource_class = kwargs['resource_class']
+ self.region = kwargs['region']
+ self.subnet = kwargs['subnet']
+ self.nodes = kwargs['nodes']
+
+ @classmethod
+ def from_str(cls, csv_str):
+ racks = []
+ csvreader = csv.reader(StringIO.StringIO(csv_str), delimiter=',')
+ for row in csvreader:
+ # ignore empty rows
+ if not row:
+ continue
+ racks.append(cls(id=row[0],
+ name=row[0],
+ resource_class=row[1],
+ subnet=row[2],
+ region=row[3],
+ nodes=row[4].split()))
+ return racks
+
+ def nodes_count(self):
+ return len(self.nodes)
+
+
+class UpdateRackStatus(forms.SelfHandlingForm):
+
+ def handle(self, request, data):
+ try:
+ rack = self.initial.get('rack', None)
+ action = request.GET.get('action')
+
+ if action == "provision":
+ api.tuskar.Rack.provision(
+ request,
+ rack.id)
+
+ msg = _('Rack "%s" is being provisioned.') % rack.name
+ else:
+ if action == "start":
+ rack.state = "active"
+ elif action == "unprovision":
+ rack.state = "unprovisioned"
+ elif action == "reboot":
+ rack.state = "active"
+ elif action == "shutdown":
+ rack.state = "off"
+
+ rack = api.tuskar.Rack.update(
+ request,
+ rack.id,
+ {'state': rack.state}
+ )
+
+ msg = _('Updated rack "%s" status.') % rack.name
+ messages.success(request, msg)
+ return True
+ except:
+ exceptions.handle(request, _("Unable to update Rack status."))
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/racks/tables.py b/openstack_dashboard/dashboards/infrastructure/resource_management/racks/tables.py
new file mode 100644
index 00000000..33fa31fb
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/racks/tables.py
@@ -0,0 +1,120 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import logging
+
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import tables
+
+from openstack_dashboard import api
+
+LOG = logging.getLogger(__name__)
+
+
+class DeleteRacks(tables.DeleteAction):
+ data_type_singular = _("Rack")
+ data_type_plural = _("Racks")
+
+ def delete(self, request, obj_id):
+ api.tuskar.Rack.delete(request, obj_id)
+
+
+class CreateRack(tables.LinkAction):
+ name = "create"
+ verbose_name = _("Create Rack")
+ url = "horizon:infrastructure:resource_management:racks:create"
+ classes = ("ajax-modal", "btn-create")
+
+
+class UploadRack(tables.LinkAction):
+ name = "upload"
+ verbose_name = _("Upload Rack")
+ url = "horizon:infrastructure:resource_management:racks:upload"
+ classes = ("ajax-modal", "btn-upload")
+
+
+class EditRack(tables.LinkAction):
+ name = "edit"
+ verbose_name = _("Edit Rack")
+ url = "horizon:infrastructure:resource_management:racks:edit"
+ classes = ("ajax-modal", "btn-edit")
+
+
+class RacksFilterAction(tables.FilterAction):
+
+ def filter(self, table, racks, filter_string):
+ """ Naive case-insensitive search. """
+ q = filter_string.lower()
+ return [rack for rack in racks
+ if q in rack.name.lower()]
+
+
+class UpdateRow(tables.Row):
+ ajax = True
+
+ def get_data(self, request, rack_id):
+ rack = api.tuskar.Rack.get(request, rack_id)
+ return rack
+
+
+class RacksTable(tables.DataTable):
+ STATUS_CHOICES = (
+ ("unprovisioned", False),
+ ("provisioning", None),
+ ("active", True),
+ ("error", False),
+ )
+ name = tables.Column('name',
+ link=("horizon:infrastructure:resource_management"
+ ":racks:detail"),
+ verbose_name=_("Rack Name"))
+ subnet = tables.Column('subnet', verbose_name=_("IP Subnet"))
+ resource_class = tables.Column('resource_class',
+ verbose_name=_("Class"),
+ filters=(lambda resource_class:
+ (resource_class and
+ resource_class.name) or None,))
+ node_count = tables.Column('nodes_count', verbose_name=_("Nodes"))
+ state = tables.Column('state',
+ verbose_name=_("State"),
+ status=True,
+ status_choices=STATUS_CHOICES)
+ usage = tables.Column(
+ 'vm_capacity',
+ verbose_name=_("Usage"),
+ filters=(lambda vm_capacity:
+ (vm_capacity.value and
+ "%s %%" % int(round((100 / float(vm_capacity.value)) *
+ vm_capacity.usage, 0))) or None,))
+
+ class Meta:
+ name = "racks"
+ row_class = UpdateRow
+ status_columns = ["state"]
+ verbose_name = _("Racks")
+ table_actions = (UploadRack, CreateRack, DeleteRacks,
+ RacksFilterAction)
+ row_actions = (EditRack, DeleteRacks)
+
+
+class UploadRacksTable(tables.DataTable):
+ name = tables.Column("name")
+ subnet = tables.Column("subnet")
+ nodes_count = tables.Column("nodes_count")
+ #region = tables.Column("region")
+
+ class Meta:
+ name = "uploaded_racks"
+ verbose_name = " "
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/racks/tabs.py b/openstack_dashboard/dashboards/infrastructure/resource_management/racks/tabs.py
new file mode 100644
index 00000000..c3314623
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/racks/tabs.py
@@ -0,0 +1,53 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import exceptions
+from horizon import tabs
+
+from ..nodes.tables import NodesTable
+
+
+class OverviewTab(tabs.Tab):
+ name = _("Overview")
+ slug = "rack_overview_tab"
+ template_name = ("infrastructure/resource_management/racks/"
+ "_detail_overview.html")
+
+ def get_context_data(self, request):
+ return {"rack": self.tab_group.kwargs['rack']}
+
+
+class NodesTab(tabs.TableTab):
+ table_classes = (NodesTable,)
+ name = _("Nodes")
+ slug = "nodes"
+ template_name = "horizon/common/_detail_table.html"
+
+ def get_nodes_data(self):
+ try:
+ rack = self.tab_group.kwargs['rack']
+ nodes = rack.list_nodes
+ except:
+ nodes = []
+ exceptions.handle(self.tab_group.request,
+ _('Unable to retrieve node list.'))
+ return nodes
+
+
+class RackDetailTabs(tabs.TabGroup):
+ slug = "rack_detail_tabs"
+ tabs = (OverviewTab, NodesTab)
+ sticky = True
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/racks/tests.py b/openstack_dashboard/dashboards/infrastructure/resource_management/racks/tests.py
new file mode 100644
index 00000000..63e1ed1f
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/racks/tests.py
@@ -0,0 +1,204 @@
+# 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 openstack_dashboard.test import helpers as test
+from openstack_dashboard import api
+from mox import IsA, IgnoreArg
+from django import http
+import tempfile
+import base64
+from django.core.files.uploadedfile import InMemoryUploadedFile
+
+
+class RackViewTests(test.BaseAdminViewTests):
+ index_page = reverse('horizon:infrastructure:resource_management:index')
+
+ @test.create_stubs({api.tuskar.ResourceClass: ('list',)})
+ def test_create_rack_get(self):
+ api.tuskar.ResourceClass.list(
+ IsA(http.request.HttpRequest)).AndReturn(
+ self.tuskar_resource_classes.list())
+
+ self.mox.ReplayAll()
+
+ url = reverse('horizon:infrastructure:resource_management:'
+ 'racks:create')
+ rack = self.client.get(url)
+
+ self.assertEqual(rack.status_code, 200)
+ self.assertTemplateUsed(rack,
+ 'horizon/common/_workflow_base.html')
+
+ # FIXME (mawagner) - After moving EditRack to use workflows, we need
+ # to circle back and fix these tests.
+ #
+ @test.create_stubs({api.tuskar.Rack: ('list', 'create',),
+ api.tuskar.ResourceClass: ('list',),
+ api.nova.baremetal.BareMetalNodeManager: ('create',)})
+ def test_create_rack_post(self):
+ api.tuskar.Rack.list(
+ IsA(http.request.HttpRequest)).AndReturn(
+ self.tuskar_racks.list())
+ api.nova.baremetal.BareMetalNodeManager.create(
+ 'New Node', u'1', u'1024', u'10', 'aa:bb:cc:dd:ee',
+ u'', u'', u'', u'').AndReturn(None)
+ api.tuskar.Rack.create(
+ IsA(http.request.HttpRequest), 'New Rack',
+ u'1', 'Tokyo', '1.2.3.4', [{'id': None}]).AndReturn(None)
+ api.tuskar.ResourceClass.list(
+ IsA(http.request.HttpRequest)).AndReturn(
+ self.tuskar_resource_classes.list())
+
+ self.mox.ReplayAll()
+
+ data = {'name': 'New Rack', 'resource_class_id': u'1',
+ 'location': 'Tokyo', 'subnet': '1.2.3.4',
+ 'node_name': 'New Node', 'prov_mac_address': 'aa:bb:cc:dd:ee',
+ 'cpus': u'1', 'memory_mb': u'1024', 'local_gb': u'10'}
+ url = reverse('horizon:infrastructure:resource_management:'
+ 'racks:create')
+ resp = self.client.post(url, data)
+ self.assertRedirectsNoFollow(resp, self.index_page)
+
+ @test.create_stubs({api.tuskar.Rack: ('get',),
+ api.tuskar.ResourceClass: ('list',)})
+ def test_edit_rack_get(self):
+ rack = self.tuskar_racks.first()
+
+ api.tuskar.Rack.\
+ get(IsA(http.HttpRequest), rack.id).\
+ MultipleTimes().AndReturn(rack)
+ api.tuskar.ResourceClass.list(
+ IsA(http.request.HttpRequest)).AndReturn(
+ self.tuskar_resource_classes.list())
+
+ self.mox.ReplayAll()
+
+ api.tuskar.Rack.list_nodes = []
+
+ url = reverse('horizon:infrastructure:resource_management:' +
+ 'racks:edit', args=[1])
+ res = self.client.get(url)
+ self.assertEqual(res.status_code, 200)
+ self.assertTemplateUsed(res,
+ 'horizon/common/_workflow_base.html')
+
+ @test.create_stubs({api.tuskar.Rack: ('get', 'list', 'update',),
+ api.tuskar.ResourceClass: ('list',)})
+ def test_edit_rack_post(self):
+ rack = self.tuskar_racks.first()
+
+ rack_data = {'name': 'Updated Rack', 'resource_class_id': u'1',
+ 'rack_id': u'1', 'location': 'New Location',
+ 'subnet': '127.10.10.0/24', 'node_macs': None}
+
+ data = {'name': 'Updated Rack', 'resource_class_id': u'1',
+ 'rack_id': u'1', 'location': 'New Location',
+ 'subnet': '127.10.10.0/24', 'node_macs': None,
+ 'node_name': 'New Node', 'prov_mac_address': 'aa:bb:cc:dd:ee',
+ 'cpus': u'1', 'memory_mb': u'1024', 'local_gb': u'10'}
+
+ api.tuskar.Rack.get(
+ IsA(http.HttpRequest), rack.id).MultipleTimes().\
+ AndReturn(rack)
+ api.tuskar.Rack.list(
+ IsA(http.request.HttpRequest)).AndReturn(
+ self.tuskar_racks.list())
+ api.tuskar.Rack.update(IsA(http.HttpRequest), rack.id, rack_data)
+ api.tuskar.ResourceClass.list(
+ IsA(http.request.HttpRequest)).AndReturn(
+ self.tuskar_resource_classes.list())
+
+ self.mox.ReplayAll()
+
+ url = reverse('horizon:infrastructure:resource_management:' +
+ 'racks:edit', args=[rack.id])
+ response = self.client.post(url, data)
+ self.assertNoFormErrors(response)
+ self.assertMessageCount(success=1)
+ self.assertRedirectsNoFollow(response, self.index_page)
+
+ @test.create_stubs({api.tuskar.Rack: ('delete', 'list')})
+ def test_delete_rack(self):
+ rack_id = u'1'
+ api.tuskar.Rack.delete(IsA(http.request.HttpRequest), rack_id) \
+ .AndReturn(None)
+ api.tuskar.Rack.list(
+ IsA(http.request.HttpRequest)).AndReturn(
+ self.tuskar_racks.list())
+
+ self.mox.ReplayAll()
+ data = {'action': 'racks__delete__%s' % rack_id}
+ url = reverse('horizon:infrastructure:resource_management:index')
+ result = self.client.post(url, data)
+ self.assertRedirectsNoFollow(result, self.index_page)
+
+ def test_upload_rack_get(self):
+ url = reverse('horizon:infrastructure:resource_management:'
+ 'racks:upload')
+ rack = self.client.get(url)
+
+ self.assertEqual(rack.status_code, 200)
+ self.assertTemplateUsed(rack,
+ 'infrastructure/resource_management/racks/upload.html')
+
+ def test_upload_rack_upload(self):
+ csv_data = 'Rack1,rclass1,192.168.111.0/24,regionX,f0:dd:f1:da:f9:b5 '\
+ 'f2:de:f1:da:f9:66 f2:de:ff:da:f9:67'
+ temp_file = tempfile.TemporaryFile()
+ temp_file.write(csv_data)
+ temp_file.flush()
+ temp_file.seek(0)
+
+ data = {'csv_file': temp_file, 'upload': '1'}
+ url = reverse('horizon:infrastructure:resource_management:'
+ 'racks:upload')
+ resp = self.client.post(url, data)
+ self.assertTemplateUsed(resp,
+ 'infrastructure/resource_management/racks/upload.html')
+ self.assertNoFormErrors(resp)
+ self.assertEqual(resp.context['form']['uploaded_data'].value(),
+ base64.b64encode(csv_data))
+
+ def test_upload_rack_upload_with_error(self):
+ data = {'upload': '1'}
+ url = reverse('horizon:infrastructure:resource_management:'
+ 'racks:upload')
+ resp = self.client.post(url, data)
+ self.assertTemplateUsed(resp,
+ 'infrastructure/resource_management/racks/upload.html')
+ self.assertFormErrors(resp, 1)
+ self.assertEqual(resp.context['form']['uploaded_data'].value(),
+ None)
+
+ @test.create_stubs({api.tuskar.Rack: ('create', 'register_nodes'),
+ api.tuskar.ResourceClass: ('list',)})
+ def test_upload_rack_create(self):
+ api.tuskar.Rack.create(IsA(http.request.HttpRequest), 'Rack1',
+ '1', 'regionX', '192.168.111.0/24').AndReturn(None)
+ api.tuskar.Rack.register_nodes(IgnoreArg(),
+ IgnoreArg()).AndReturn(None)
+ api.tuskar.ResourceClass.list(
+ IsA(http.request.HttpRequest)).AndReturn(
+ self.tuskar_resource_classes.list())
+ self.mox.ReplayAll()
+ csv_data = 'Rack1,rclass1,192.168.111.0/24,regionX,f0:dd:f1:da:f9:b5 '\
+ 'f2:de:f1:da:f9:66 f2:de:ff:da:f9:67'
+
+ data = {'uploaded_data': base64.b64encode(csv_data), 'add_racks': '1'}
+ url = reverse('horizon:infrastructure:resource_management:'
+ 'racks:upload')
+ resp = self.client.post(url, data)
+ self.assertRedirectsNoFollow(resp, self.index_page)
+ self.assertMessageCount(success=1)
+ self.assertMessageCount(error=0)
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/racks/urls.py b/openstack_dashboard/dashboards/infrastructure/resource_management/racks/urls.py
new file mode 100644
index 00000000..b41e6660
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/racks/urls.py
@@ -0,0 +1,39 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from django.conf.urls import patterns, url, include
+
+from .views import (CreateView, UploadView, EditView, DetailEditView,
+ EditRackStatusView, DetailView, UsageDataView)
+
+
+RACKS = r'^(?P<rack_id>[^/]+)/%s$'
+VIEW_MOD = 'openstack_dashboard.dashboards.infrastructure.' \
+ 'resource_management.racks.views'
+
+
+urlpatterns = patterns(VIEW_MOD,
+ url(r'^create/$', CreateView.as_view(), name='create'),
+ url(r'^upload/$', UploadView.as_view(), name='upload'),
+ url(r'^usage_data$', UsageDataView.as_view(), name='usage_data'),
+ url(RACKS % 'edit/', EditView.as_view(), name='edit'),
+ url(RACKS % 'detail_edit/', DetailEditView.as_view(), name='detail_edit'),
+ url(RACKS % 'edit_status/', EditRackStatusView.as_view(),
+ name='edit_status'),
+ url(RACKS % 'detail', DetailView.as_view(), name='detail'),
+ url(RACKS % 'top_communicating.json', 'top_communicating',
+ name='top_communicating'),
+ url(RACKS % 'node_health.json', 'node_health', name='node_health'),
+ url(RACKS % 'check_state.json', 'check_state', name='check_state'),
+)
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/racks/views.py b/openstack_dashboard/dashboards/infrastructure/resource_management/racks/views.py
new file mode 100644
index 00000000..8b66bc11
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/racks/views.py
@@ -0,0 +1,253 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# 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 datetime import datetime, timedelta
+import json
+import logging
+import random
+
+from django.core.serializers.json import DjangoJSONEncoder
+from django.core.urlresolvers import reverse
+from django.core.urlresolvers import reverse_lazy
+from django.http import HttpResponse
+
+from django.utils import simplejson
+from django.utils.translation import ugettext_lazy as _
+from django.views.generic import View
+
+from horizon import exceptions
+from horizon import tabs
+from horizon import forms
+from horizon import workflows
+from .workflows import (CreateRack, EditRack, DetailEditRack)
+
+from openstack_dashboard import api
+
+from .forms import UploadRack, UpdateRackStatus
+from .tabs import RackDetailTabs
+from .tables import UploadRacksTable
+
+
+LOG = logging.getLogger(__name__)
+
+
+class CreateView(workflows.WorkflowView):
+ workflow_class = CreateRack
+
+ def get_initial(self):
+ pass
+
+
+class UploadView(forms.ModalFormView):
+ form_class = UploadRack
+ template_name = 'infrastructure/resource_management/racks/upload.html'
+ success_url = reverse_lazy(
+ 'horizon:infrastructure:resource_management:index')
+
+ def get_context_data(self, **kwargs):
+ context = super(UploadView, self).get_context_data(**kwargs)
+ context['racks_table'] = UploadRacksTable(
+ self.request, kwargs['form'].initial.get('racks', []))
+ return context
+
+
+class EditView(workflows.WorkflowView):
+ workflow_class = EditRack
+
+ def get_initial(self):
+ obj = api.tuskar.Rack.get(self.request, self.kwargs['rack_id'])
+ # mac_str = "\n".join([x.mac_address for x in obj.list_nodes])
+ return {'name': obj.name, 'resource_class_id': obj.resource_class_id,
+ 'location': obj.location, 'subnet': obj.subnet,
+ 'state': obj.state, 'rack_id': self.kwargs['rack_id']}
+
+
+class DetailEditView(EditView):
+ workflow_class = DetailEditRack
+
+
+class EditRackStatusView(forms.ModalFormView):
+ form_class = UpdateRackStatus
+ template_name = 'infrastructure/resource_management/racks/edit_status.html'
+
+ def get_success_url(self):
+ # Redirect to previous url
+ return self.request.META.get('HTTP_REFERER', None)
+
+ def get_context_data(self, **kwargs):
+ context = super(EditRackStatusView, self).get_context_data(**kwargs)
+ context['rack_id'] = self.kwargs['rack_id']
+ context['action'] = context['form'].initial.get('action', None)
+ return context
+
+ def get_initial(self):
+ try:
+ rack = api.tuskar.Rack.get(
+ self.request, self.kwargs['rack_id'])
+ action = self.request.GET.get('action')
+ except:
+ exceptions.handle(self.request,
+ _("Unable to retrieve rack data."))
+ return {'rack': rack,
+ 'action': action}
+
+
+class DetailView(tabs.TabView):
+ tab_group_class = RackDetailTabs
+ template_name = 'infrastructure/resource_management/racks/detail.html'
+
+ def get_context_data(self, **kwargs):
+ context = super(DetailView, self).get_context_data(**kwargs)
+ context["rack"] = self.get_data()
+ return context
+
+ def get_data(self):
+ if not hasattr(self, "_rack"):
+ try:
+ rack_id = self.kwargs['rack_id']
+ rack = api.tuskar.Rack.get(self.request, rack_id)
+ except:
+ redirect = reverse('horizon:infrastructure:'
+ 'resource_management:index')
+ exceptions.handle(self.request,
+ _('Unable to retrieve details for '
+ 'rack "%s".')
+ % rack_id,
+ redirect=redirect)
+ self._rack = rack
+ return self._rack
+
+ def get_tabs(self, request, *args, **kwargs):
+ rack = self.get_data()
+ return self.tab_group_class(request, rack=rack,
+ **kwargs)
+
+
+class UsageDataView(View):
+
+ def get(self, request, *args, **kwargs):
+ interval = request.GET.get('interval', '1w')
+ series = request.GET.get('series', "")
+ series = series.split(',')
+
+ if interval == '12h':
+ data_count = 12
+ timedelta_param = 'hours'
+ elif interval == '24h':
+ data_count = 24
+ timedelta_param = 'hours'
+ elif interval == '1m':
+ data_count = 30
+ timedelta_param = 'days'
+ elif interval == '1y':
+ data_count = 52
+ timedelta_param = 'weeks'
+ else:
+ # default is 1 week
+ data_count = 7
+ timedelta_param = 'days'
+
+ values = []
+ for i in range(data_count):
+ timediff = timedelta(**{timedelta_param: i})
+ current_value = {'date': datetime.now() - timediff}
+
+ for usage_type in series:
+ current_value[usage_type] = random.randint(1, 9)
+
+ values.append(current_value)
+
+ return HttpResponse(json.dumps(values, cls=DjangoJSONEncoder),
+ mimetype='application/json')
+
+
+def top_communicating(request, rack_id=None):
+ # FIXME replace mock data
+ random.seed()
+ data = []
+ statuses = ["Insane level of communication",
+ "High level of communication",
+ "Normal level of communication",
+ "Low level of communication"]
+
+ rack = api.tuskar.Rack.get(request, rack_id)
+ for node in rack.nodes:
+ status = random.randint(0, 3)
+ percentage = random.randint(0, 100)
+
+ tooltip = ("<p>Node: <strong>{0}</strong></p><p>{1}</p>").format(
+ node['id'],
+ statuses[status])
+
+ data.append({'tooltip': tooltip,
+ 'status': statuses[status],
+ 'scale': 'linear_color_scale',
+ 'percentage': percentage,
+ 'id': "FIXME_RACK id",
+ 'name': "FIXME name",
+ 'url': "FIXME url"})
+
+ data.sort(key=lambda x: x['percentage'])
+
+ # FIXME dynamically set the max domain, based on data
+ settings = {'scale': 'linear_color_scale',
+ 'domain': [0, max([datum['percentage'] for datum in data])],
+ 'range': ["#000060", "#99FFFF"]}
+ res = {'data': data,
+ 'settings': settings}
+ return HttpResponse(simplejson.dumps(res),
+ mimetype="application/json")
+
+
+def node_health(request, rack_id=None):
+ # FIXME replace mock data
+ random.seed()
+ data = []
+ statuses = ["Good", "Warnings", "Disaster"]
+ colors = ["rgb(244,244,244)", "rgb(240,170,0)", "rgb(200,0,0)"]
+
+ rack = api.tuskar.Rack.get(request, rack_id)
+
+ for node in rack.nodes:
+ rand_index = random.randint(0, 2)
+ percentage = (2 - rand_index) * 50
+ color = colors[rand_index]
+
+ tooltip = ("<p>Node: <strong>{0}</strong></p><p>{1}</p>").format(
+ node['id'],
+ statuses[rand_index])
+
+ data.append({'tooltip': tooltip,
+ 'color': color,
+ 'status': statuses[rand_index],
+ 'percentage': percentage,
+ 'id': node['id'],
+ 'name': node['id'],
+ 'url': "FIXME url"})
+
+ data.sort(key=lambda x: x['percentage'])
+
+ res = {'data': data}
+ return HttpResponse(simplejson.dumps(res),
+ mimetype="application/json")
+
+
+def check_state(request, rack_id=None):
+ rack = api.tuskar.Rack.get(request, rack_id)
+
+ res = {'state': rack.state}
+
+ return HttpResponse(
+ simplejson.dumps(res),
+ mimetype="application/json")
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/racks/workflows.py b/openstack_dashboard/dashboards/infrastructure/resource_management/racks/workflows.py
new file mode 100644
index 00000000..8a133dd7
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/racks/workflows.py
@@ -0,0 +1,224 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# 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, reverse_lazy
+from django.utils.translation import ugettext_lazy as _
+from django.forms import widgets
+
+from horizon import exceptions
+from horizon import workflows
+from horizon import forms
+
+from openstack_dashboard import api
+
+import re
+
+
+class NodeCreateAction(workflows.Action):
+ # node_macs = forms.CharField(label=_("MAC Addresses"),
+ # widget=forms.Textarea(attrs={'rows': 12, 'cols': 20}),
+ # required=False)
+
+ node_name = forms.CharField(label="Name", required=True)
+ prov_mac_address = forms.CharField(label=("MAC Address"),
+ required=True)
+
+ # Hardware Specifications
+ cpus = forms.CharField(label="CPUs", required=True)
+ memory_mb = forms.CharField(label="Memory", required=True)
+ local_gb = forms.CharField(label="Local Disk (GB)", required=True)
+
+ # Power Management
+ pm_address = forms.CharField(label="Power Management IP", required=False)
+ pm_user = forms.CharField(label="Power Management User", required=False)
+ pm_password = forms.CharField(label="Power Management Password",
+ required=False,
+ widget=forms.PasswordInput(
+ render_value=False))
+
+ # Access
+ terminal_port = forms.CharField(label="Terminal Port", required=False)
+
+ class Meta:
+ name = _("Nodes")
+
+
+# mawagner FIXME - For the demo, all we can really do is edit the one
+# associated node. That's very much _not_ what this form is actually
+# about, though.
+class NodeEditAction(NodeCreateAction):
+
+ class Meta:
+ name = _("Nodes")
+
+ # FIXME: mawagner - This is all for debugging. The idea is to fetch
+ # the first node and display it in the form; the latter part needs
+ # implementation. This also needs error handling; right now for testing
+ # I want to let it fail, but don't commit like that! :)
+ def __init__(self, request, *args, **kwargs):
+ super(NodeEditAction, self).__init__(request, *args, **kwargs)
+ rack_id = self.initial['rack_id']
+ rack = api.tuskar.Rack.get(request, rack_id)
+ nodes = rack.list_nodes
+
+
+class RackCreateInfoAction(workflows.Action):
+ name = forms.RegexField(label=_("Name"),
+ max_length=25,
+ regex=r'^[\w\.\- ]+$',
+ error_messages={'invalid': _('Name may only '
+ 'contain letters, numbers, underscores, '
+ 'periods and hyphens.')})
+ location = forms.CharField(label=_("Location"))
+ # see GenericIPAddressField, but not for subnets:
+ subnet = forms.CharField(label=_("IP Subnet"))
+ resource_class_id = forms.ChoiceField(label=_("Resource Class"))
+
+ def clean(self):
+ cleaned_data = super(RackCreateInfoAction, self).clean()
+ name = cleaned_data.get('name')
+ rack_id = self.initial.get('rack_id', None)
+ resource_class_id = cleaned_data.get('resource_class_id')
+ location = cleaned_data.get('location')
+ subnet = cleaned_data.get('subnet')
+ try:
+ racks = api.tuskar.Rack.list(self.request)
+ except:
+ racks = []
+ exceptions.check_message(['Connection', 'refused'],
+ _("Unable to retrieve rack list."))
+ raise
+
+ # Validations: detect duplicates
+ for rack in racks:
+ other_record = rack_id != rack.id
+ if rack.name == name and other_record:
+ raise forms.ValidationError(
+ _('The name %s is already used by another rack.')
+ % name)
+ if rack.subnet == subnet and other_record:
+ raise forms.ValidationError(
+ _('The subnet %s is already assigned to rack %s.')
+ % (subnet, rack.name))
+
+ return cleaned_data
+
+ def __init__(self, request, *args, **kwargs):
+ super(RackCreateInfoAction, self).__init__(request, *args, **kwargs)
+ resource_class_id_choices = [('', _("Select a Resource Class"))]
+ for rc in api.tuskar.ResourceClass.list(request):
+ resource_class_id_choices.append((rc.id, rc.name))
+ self.fields['resource_class_id'].choices = resource_class_id_choices
+
+ class Meta:
+ name = _("Rack Settings")
+
+
+class RackEditInfoAction(RackCreateInfoAction):
+ def clean(self):
+ cleaned_data = super(RackCreateInfoAction, self).clean()
+
+
+class CreateRackInfo(workflows.Step):
+ action_class = RackCreateInfoAction
+
+ contributes = ('name', 'resource_class_id', 'subnet', 'location')
+
+ def get_racks_data():
+ pass
+
+
+class EditRackInfo(CreateRackInfo):
+ depends_on = ('rack_id',)
+
+
+class CreateNodes(workflows.Step):
+ action_class = NodeCreateAction
+ contributes = ('node_name', 'prov_mac_address', 'cpus', 'memory_mb',
+ 'local_gb', 'pm_address', 'pm_user', 'pm_password',
+ 'terminal_port')
+
+ def get_nodes_data():
+ pass
+
+
+class EditNodes(CreateNodes):
+ action_class = NodeEditAction
+ depends_on = ('rack_id',)
+ contributes = ('node_macs',)
+ # help_text = _("Editing nodes via textbox is not presently supported.")
+
+
+class CreateRack(workflows.Workflow):
+ default_steps = (CreateRackInfo, CreateNodes)
+ slug = "create_rack"
+ name = _("Add Rack")
+ success_url = 'horizon:infrastructure:resource_management:index'
+ success_message = _("Rack created.")
+ failure_message = _("Unable to create rack.")
+
+ def handle(self, request, data):
+ try:
+ if data['node_name'] is not None:
+ node = api
+ node = api.tuskar.Node.manager().create(data['node_name'],
+ data['cpus'], data['memory_mb'],
+ data['local_gb'], data['prov_mac_address'],
+ data['pm_address'], data['pm_user'],
+ data['pm_password'], data['terminal_port'])
+
+ if node:
+ node_id = node.id
+ else:
+ node_id = None
+
+ # Then, register the Rack, including the node if it exists
+ rack = api.tuskar.Rack.create(request, data['name'],
+ data['resource_class_id'],
+ data['location'],
+ data['subnet'],
+ [{'id': node_id}])
+
+ return True
+ except Exception as ex:
+ exceptions.handle(request, _("Unable to create rack."))
+
+
+class EditRack(CreateRack):
+ default_steps = (EditRackInfo, EditNodes)
+ slug = "edit_rack"
+ name = _("Edit Rack")
+ success_url = 'horizon:infrastructure:resource_management:index'
+ success_message = _("Rack updated.")
+ failure_message = _("Unable to update rack.")
+
+ def handle(self, request, data):
+ try:
+ rack_id = self.context['rack_id']
+ rack = api.tuskar.Rack.update(request, rack_id, data)
+ return True
+ except:
+ exceptions.handle(request, _("Unable to update rack."))
+
+
+class DetailEditRack(EditRack):
+ success_url = 'horizon:infrastructure:resource_management:racks:detail'
+
+ def get_success_url(self):
+ rack_id = self.context['rack_id']
+ return reverse(self.success_url, args=(rack_id,))
+
+ def get_failure_url(self):
+ rack_id = self.context['rack_id']
+ return reverse(self.success_url, args=(rack_id,))
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/__init__.py b/openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/__init__.py
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/forms.py b/openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/forms.py
new file mode 100644
index 00000000..00dbda82
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/forms.py
@@ -0,0 +1,52 @@
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import exceptions
+from horizon import forms
+from horizon import messages
+
+from openstack_dashboard import api
+
+import logging
+
+LOG = logging.getLogger(__name__)
+
+
+class DeleteForm(forms.SelfHandlingForm):
+ def __init__(self, request, *args, **kwargs):
+ super(DeleteForm, self).__init__(request, *args, **kwargs)
+
+ resource_class = self.initial.get('resource_class', None)
+ self.command = DeleteCommand(request, resource_class)
+
+ def handle(self, request, data):
+ try:
+ self.command.execute()
+
+ messages.success(request, self.command.msg)
+ return True
+ except:
+ exceptions.handle(request, _("Unable to delete Resource Class."))
+
+
+# TODO this command will be reused in table, so code is not duplicated
+class DeleteCommand(object):
+ def __init__(self, request, resource_class):
+ self.resource_class = resource_class
+ self.request = request
+ self.header = (_("Deleting resource class '%s.'")
+ % self.resource_class.name)
+ self.msg = ""
+
+ def execute(self):
+ try:
+ api.tuskar.ResourceClass.delete(self.request,
+ self.resource_class.id)
+ self.msg = (_('Successfully deleted Class "%s".')
+ % self.resource_class.name)
+ except:
+ self.msg = _('Failed to delete Class %s') % self.resource_class.id
+ LOG.info(self.msg)
+ redirect = reverse(
+ "horizon:infrastructure:resource_management:index")
+ exceptions.handle(self.request, self.msg, redirect=redirect)
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/tables.py b/openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/tables.py
new file mode 100644
index 00000000..3e0a7c5f
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/tables.py
@@ -0,0 +1,169 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# 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 import shortcuts
+from django.core import urlresolvers
+from django.utils.http import urlencode
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import exceptions
+from horizon import messages
+from horizon import tables
+from horizon import forms
+
+from openstack_dashboard import api
+
+import workflows
+
+from ..flavors import tables as flavors_tables
+from ..racks import tables as racks_tables
+
+
+LOG = logging.getLogger(__name__)
+
+
+class CreateResourceClass(tables.LinkAction):
+ name = "create_class"
+ verbose_name = _("Create Class")
+ url = "horizon:infrastructure:resource_management:resource_classes:create"
+ classes = ("ajax-modal", "btn-create")
+
+
+class UpdateResourceClass(tables.LinkAction):
+ name = "edit_class"
+ verbose_name = _("Edit Class")
+ url = "horizon:infrastructure:resource_management:resource_classes:update"
+ classes = ("ajax-modal", "btn-edit")
+
+
+class DeleteResourceClass(tables.DeleteAction):
+ data_type_singular = _("Resource Class")
+ data_type_plural = _("Resource Classes")
+
+ def delete(self, request, obj_id):
+ try:
+ api.tuskar.ResourceClass.delete(request, obj_id)
+ except:
+ msg = _('Failed to delete resource class %s') % obj_id
+ LOG.info(msg)
+ redirect = urlresolvers.reverse(
+ "horizon:infrastructure:resource_management:index")
+ exceptions.handle(request, msg, redirect=redirect)
+
+
+class ResourcesClassFilterAction(tables.FilterAction):
+ def filter(self, table, instances, filter_string):
+ pass
+
+
+class ResourceClassesTable(tables.DataTable):
+ name = tables.Column("name",
+ link=('horizon:infrastructure:'
+ 'resource_management:resource_classes:detail'),
+ verbose_name=_("Class Name"))
+ service_type = tables.Column("service_type",
+ verbose_name=_("Class Type"))
+ racks_count = tables.Column("racks_count",
+ verbose_name=_("Racks"),
+ empty_value="0")
+ nodes_count = tables.Column("nodes_count",
+ verbose_name=_("Nodes"),
+ empty_value="0")
+
+ class Meta:
+ name = "resource_classes"
+ verbose_name = ("Classes")
+ table_actions = (ResourcesClassFilterAction, CreateResourceClass,
+ DeleteResourceClass)
+ row_actions = (UpdateResourceClass, DeleteResourceClass)
+
+
+class FlavorsFilterAction(tables.FilterAction):
+ def filter(self, table, instances, filter_string):
+ pass
+
+
+class FlavorsTable(flavors_tables.FlavorsTable):
+ name = tables.Column('name',
+ verbose_name=_('Flavor Name'))
+ max_vms = tables.Column("max_vms",
+ auto='form_widget',
+ verbose_name=_("Max. VMs"),
+ form_widget=forms.NumberInput(),
+ form_widget_attributes={
+ 'class': "number_input_slim"})
+
+ class Meta:
+ name = "flavors"
+ verbose_name = _("Flavors")
+ multi_select = True
+ multi_select_name = "flavors_object_ids"
+
+
+class RacksFilterAction(tables.FilterAction):
+ def filter(self, table, instances, filter_string):
+ pass
+
+
+class RacksTable(racks_tables.RacksTable):
+
+ class Meta:
+ name = "racks"
+ verbose_name = _("Racks")
+ multi_select = True
+ multi_select_name = "racks_object_ids"
+ table_actions = (RacksFilterAction,)
+
+
+class UpdateRacksClass(tables.LinkAction):
+ name = "edit_flavors"
+ verbose_name = _("Edit Racks")
+
+ classes = ("ajax-modal", "btn-edit")
+
+ def get_link_url(self, datum=None):
+ url = "horizon:infrastructure:resource_management:resource_classes:"\
+ "update_racks"
+ return "%s?step=%s" % (
+ urlresolvers.reverse(
+ url,
+ args=(self.table.kwargs['resource_class_id'],)),
+ workflows.RacksAction.slug)
+
+
+class UpdateFlavorsClass(tables.LinkAction):
+ name = "edit_flavors"
+ verbose_name = _("Edit Flavors")
+ classes = ("ajax-modal", "btn-edit")
+
+ def get_link_url(self, datum=None):
+ url = "horizon:infrastructure:resource_management:resource_classes:"\
+ "update_flavors"
+ return "%s?step=%s" % (
+ urlresolvers.reverse(
+ url,
+ args=(self.table.kwargs['resource_class_id'],)),
+ workflows.ResourceClassInfoAndFlavorsAction.slug)
+
+
+class ResourceClassDetailFlavorsTable(flavors_tables.FlavorsTable):
+ max_vms = tables.Column("max_vms",
+ verbose_name=_("Max. VMs"))
+
+ class Meta:
+ name = "flavors"
+ verbose_name = _("Flavors")
+ table_actions = (FlavorsFilterAction, UpdateFlavorsClass)
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/tabs.py b/openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/tabs.py
new file mode 100644
index 00000000..0b667cad
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/tabs.py
@@ -0,0 +1,78 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import exceptions
+from horizon import tabs
+
+from tables import ResourceClassDetailFlavorsTable, RacksTable
+
+
+class OverviewTab(tabs.Tab):
+ name = _("Overview")
+ slug = "overview"
+ template_name = ("infrastructure/resource_management/resource_classes/"
+ "_detail_overview.html")
+ # FIXME charts doesnt work if uncommented
+ #preload = False
+
+ def get_context_data(self, request):
+ return {"resource_class": self.tab_group.kwargs['resource_class']}
+
+
+class RacksTab(tabs.TableTab):
+ table_classes = (RacksTable,)
+ name = _("Racks")
+ slug = "racks"
+ template_name = ("infrastructure/resource_management/resource_classes/"
+ "_detail_racks.html")
+
+ def get_racks_data(self):
+ try:
+ resource_class = self.tab_group.kwargs['resource_class']
+ racks = resource_class.list_racks
+ except:
+ racks = []
+ exceptions.handle(self.tab_group.request,
+ _('Unable to retrieve rack list.'))
+ return racks
+
+
+class FlavorsTab(tabs.TableTab):
+ table_classes = (ResourceClassDetailFlavorsTable,)
+ name = _("Flavors")
+ slug = "flavors"
+ template_name = ("infrastructure/resource_management/resource_classes/"
+ "_detail_flavors.html")
+
+ def get_flavors_data(self):
+ try:
+ resource_class = self.tab_group.kwargs['resource_class']
+ racks = resource_class.list_flavors
+ except:
+ racks = []
+ exceptions.handle(self.tab_group.request,
+ _('Unable to retrieve flavor list.'))
+ return racks
+
+ def allowed(self, request):
+ resource_class = self.tab_group.kwargs['resource_class']
+ return resource_class.service_type == "compute"
+
+
+class ResourceClassDetailTabs(tabs.TabGroup):
+ slug = "resource_class_details"
+ tabs = (OverviewTab, RacksTab, FlavorsTab)
+ sticky = True
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/tests.py b/openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/tests.py
new file mode 100644
index 00000000..67ec51f6
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/tests.py
@@ -0,0 +1,335 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# 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 import http
+from django.core.urlresolvers import reverse
+from mox import IsA
+from openstack_dashboard import api
+from openstack_dashboard.test import helpers as test
+
+
+class ResourceClassViewTests(test.BaseAdminViewTests):
+
+ @test.create_stubs({
+ api.tuskar.FlavorTemplate: ('list',),
+ api.tuskar.Rack: ('list',)
+ })
+ def test_create_resource_class_get(self):
+ all_templates = self.tuskar_flavor_templates.list()
+ all_racks = self.tuskar_racks.list()
+
+ api.tuskar.FlavorTemplate.\
+ list(IsA(http.HttpRequest)).AndReturn(all_templates)
+ api.tuskar.Rack.\
+ list(IsA(http.HttpRequest), True).AndReturn(all_racks)
+ self.mox.ReplayAll()
+
+ url = reverse(
+ 'horizon:infrastructure:resource_management:'
+ 'resource_classes:create')
+ res = self.client.get(url)
+ self.assertEqual(res.status_code, 200)
+
+ @test.create_stubs({
+ api.tuskar.ResourceClass: ('list', 'create', 'set_racks'),
+ })
+ def test_create_resource_class_post(self):
+ new_resource_class = self.tuskar_resource_classes.first()
+ new_unique_name = "unique_name_for_sure"
+ new_flavors = []
+
+ add_racks_ids = []
+
+ api.tuskar.ResourceClass.list(
+ IsA(http.request.HttpRequest)).AndReturn(
+ self.tuskar_resource_classes.list())
+ api.tuskar.ResourceClass.\
+ create(IsA(http.HttpRequest), name=new_unique_name,
+ service_type=new_resource_class.service_type,
+ flavors=new_flavors).\
+ AndReturn(new_resource_class)
+ api.tuskar.ResourceClass.\
+ set_racks(IsA(http.HttpRequest), add_racks_ids)
+ self.mox.ReplayAll()
+
+ url = reverse('horizon:infrastructure:resource_management:'
+ 'resource_classes:create')
+ form_data = {'name': new_unique_name,
+ 'service_type': new_resource_class.service_type,
+ 'image': 'compute-img'}
+ res = self.client.post(url, form_data)
+ self.assertNoFormErrors(res)
+ self.assertMessageCount(success=1)
+ self.assertRedirectsNoFollow(res,
+ ("%s?tab=resource_management_tabs__resource_classes_tab" %
+ reverse("horizon:infrastructure:resource_management:index")))
+
+ @test.create_stubs({api.tuskar.ResourceClass: ('get', 'list_flavors',
+ 'racks_ids')})
+ def test_edit_resource_class_get(self):
+ resource_class = self.tuskar_resource_classes.first()
+ all_flavors = []
+ all_racks = []
+
+ api.tuskar.ResourceClass.\
+ get(IsA(http.HttpRequest), resource_class.id).MultipleTimes().\
+ AndReturn(resource_class)
+
+ self.mox.ReplayAll()
+
+ # FIXME I should probably track the racks and flavors methods
+ # so maybe they shouldn't be a @property
+ # properties set
+ api.tuskar.ResourceClass.all_racks = all_racks
+ api.tuskar.ResourceClass.all_flavors = all_flavors
+ api.tuskar.ResourceClass.list_flavors = all_flavors
+
+ url = reverse(
+ 'horizon:infrastructure:resource_management:'
+ 'resource_classes:update',
+ args=[resource_class.id])
+ res = self.client.get(url)
+ self.assertEqual(res.status_code, 200)
+
+ @test.create_stubs({
+ api.tuskar.ResourceClass: ('get', 'list', 'update', 'set_racks')
+ })
+ def test_edit_resource_class_post(self):
+ resource_class = self.tuskar_resource_classes.first()
+
+ add_racks_ids = []
+
+ api.tuskar.ResourceClass.get(
+ IsA(http.HttpRequest),
+ resource_class.id).\
+ AndReturn(resource_class)
+ api.tuskar.ResourceClass.list(
+ IsA(http.request.HttpRequest)).AndReturn(
+ self.tuskar_resource_classes.list())
+ api.tuskar.ResourceClass.\
+ update(IsA(http.HttpRequest), resource_class.id,
+ name=resource_class.name,
+ service_type=resource_class.service_type,
+ flavors=[]).\
+ AndReturn(resource_class)
+ api.tuskar.ResourceClass.\
+ set_racks(IsA(http.HttpRequest), add_racks_ids)
+ self.mox.ReplayAll()
+
+ form_data = {'resource_class_id': resource_class.id,
+ 'name': resource_class.name,
+ 'service_type': resource_class.service_type,
+ 'image': 'compute-img'}
+ url = reverse('horizon:infrastructure:resource_management:'
+ 'resource_classes:update', args=[resource_class.id])
+ res = self.client.post(url, form_data)
+ self.assertNoFormErrors(res)
+ self.assertMessageCount(success=1)
+ self.assertRedirectsNoFollow(res,
+ ("%s?tab=resource_management_tabs__resource_classes_tab" %
+ reverse("horizon:infrastructure:resource_management:index")))
+
+ @test.create_stubs({api.tuskar.ResourceClass: ('delete', 'list')})
+ def test_delete_resource_class(self):
+ resource_class = self.tuskar_resource_classes.first()
+ all_resource_classes = self.tuskar_resource_classes.list()
+
+ api.tuskar.ResourceClass.delete(
+ IsA(http.HttpRequest),
+ resource_class.id)
+ api.tuskar.ResourceClass.list(
+ IsA(http.HttpRequest)).\
+ AndReturn(all_resource_classes)
+ self.mox.ReplayAll()
+
+ form_data = {'action':
+ 'resource_classes__delete__%s' % resource_class.id}
+ res = self.client.post(
+ reverse('horizon:infrastructure:resource_management:index'),
+ form_data)
+ self.assertRedirectsNoFollow(
+ res, reverse('horizon:infrastructure:resource_management:index'))
+
+ @test.create_stubs({
+ api.tuskar.ResourceClass: ('get', 'list_flavors', 'list_racks')
+ })
+ def test_detail_get(self):
+ resource_class = self.tuskar_resource_classes.first()
+ flavors = []
+ racks = []
+
+ api.tuskar.ResourceClass.get(
+ IsA(http.HttpRequest),
+ resource_class.id).\
+ MultipleTimes().AndReturn(resource_class)
+ self.mox.ReplayAll()
+
+ api.tuskar.ResourceClass.list_flavors = flavors
+ api.tuskar.ResourceClass.list_racks = racks
+
+ url = reverse('horizon:infrastructure:resource_management:'
+ 'resource_classes:detail', args=[resource_class.id])
+ res = self.client.get(url)
+ self.assertItemsEqual(res.context['flavors_table'].data,
+ flavors)
+ self.assertItemsEqual(res.context['racks_table'].data,
+ racks)
+ self.assertEqual(res.status_code, 200)
+ self.assertTemplateUsed(res,
+ 'infrastructure/resource_management/resource_classes/detail.html')
+
+ @test.create_stubs({api.tuskar.ResourceClass: ('get', 'list_flavors',
+ 'racks_ids')})
+ def test_detail_edit_racks_get(self):
+ resource_class = self.tuskar_resource_classes.first()
+ all_flavors = []
+ all_racks = []
+
+ api.tuskar.ResourceClass.\
+ get(IsA(http.HttpRequest), resource_class.id).\
+ MultipleTimes().AndReturn(resource_class)
+ self.mox.ReplayAll()
+
+ # FIXME I should probably track the racks and flavors methods
+ # so maybe they shouldn't be a @property
+ # properties set
+ api.tuskar.ResourceClass.all_racks = all_racks
+ api.tuskar.ResourceClass.all_flavors = all_flavors
+ api.tuskar.ResourceClass.list_flavors = all_flavors
+
+ url = reverse(
+ 'horizon:infrastructure:resource_management:'
+ 'resource_classes:update_racks',
+ args=[resource_class.id])
+ res = self.client.get(url)
+ self.assertEqual(res.status_code, 200)
+
+ @test.create_stubs({
+ api.tuskar.ResourceClass: ('get', 'list', 'update', 'set_racks')
+ })
+ def test_detail_edit_racks_post(self):
+ resource_class = self.tuskar_resource_classes.first()
+
+ add_racks_ids = []
+
+ api.tuskar.ResourceClass.get(
+ IsA(http.HttpRequest),
+ resource_class.id).\
+ AndReturn(resource_class)
+ api.tuskar.ResourceClass.list(
+ IsA(http.request.HttpRequest)).AndReturn(
+ self.tuskar_resource_classes.list())
+ api.tuskar.ResourceClass.\
+ update(IsA(http.HttpRequest), resource_class.id,
+ name=resource_class.name,
+ service_type=resource_class.service_type,
+ flavors=[]).\
+ AndReturn(resource_class)
+ api.tuskar.ResourceClass.\
+ set_racks(IsA(http.HttpRequest), add_racks_ids)
+ self.mox.ReplayAll()
+
+ form_data = {'resource_class_id': resource_class.id,
+ 'name': resource_class.name,
+ 'service_type': resource_class.service_type,
+ 'image': 'compute-img'}
+ url = reverse('horizon:infrastructure:resource_management:'
+ 'resource_classes:update_racks',
+ args=[resource_class.id])
+ res = self.client.post(url, form_data)
+ self.assertNoFormErrors(res)
+ self.assertMessageCount(success=1)
+
+ detail_url = "horizon:infrastructure:resource_management:"\
+ "resource_classes:detail"
+ redirect_url = "%s?tab=resource_class_details__racks" % (
+ reverse(detail_url, args=(resource_class.id,)))
+ self.assertRedirectsNoFollow(res, redirect_url)
+
+ @test.create_stubs({api.tuskar.ResourceClass: ('get', 'list_flavors',
+ 'racks_ids')})
+ def test_detail_edit_flavors_get(self):
+ resource_class = self.tuskar_resource_classes.first()
+ all_flavors = []
+ all_racks = []
+
+ api.tuskar.ResourceClass.\
+ get(IsA(http.HttpRequest), resource_class.id).\
+ MultipleTimes().AndReturn(resource_class)
+ self.mox.ReplayAll()
+
+ # FIXME I should probably track the racks and flavors methods
+ # so maybe they shouldn't be a @property
+ # properties set
+ api.tuskar.ResourceClass.all_racks = all_racks
+ api.tuskar.ResourceClass.all_flavors = all_flavors
+ api.tuskar.ResourceClass.list_flavors = all_flavors
+
+ url = reverse(
+ 'horizon:infrastructure:resource_management:'
+ 'resource_classes:update_flavors',
+ args=[resource_class.id])
+ res = self.client.get(url)
+ self.assertEqual(res.status_code, 200)
+
+ @test.create_stubs({
+ api.tuskar.ResourceClass: ('get', 'list', 'update', 'set_racks')
+ })
+ def test_detail_edit_flavors_post(self):
+ resource_class = self.tuskar_resource_classes.first()
+
+ add_racks_ids = []
+
+ api.tuskar.ResourceClass.get(
+ IsA(http.HttpRequest),
+ resource_class.id).\
+ AndReturn(resource_class)
+ api.tuskar.ResourceClass.list(
+ IsA(http.request.HttpRequest)).AndReturn(
+ self.tuskar_resource_classes.list())
+ api.tuskar.ResourceClass.\
+ update(IsA(http.HttpRequest), resource_class.id,
+ name=resource_class.name,
+ service_type=resource_class.service_type,
+ flavors=[]).\
+ AndReturn(resource_class)
+ api.tuskar.ResourceClass.\
+ set_racks(IsA(http.HttpRequest), add_racks_ids)
+ self.mox.ReplayAll()
+
+ form_data = {'resource_class_id': resource_class.id,
+ 'name': resource_class.name,
+ 'service_type': resource_class.service_type,
+ 'image': 'compute-img'}
+ url = reverse('horizon:infrastructure:resource_management:'
+ 'resource_classes:update_flavors',
+ args=[resource_class.id])
+ res = self.client.post(url, form_data)
+ self.assertNoFormErrors(res)
+ self.assertMessageCount(success=1)
+
+ redirect_url = "horizon:infrastructure:resource_management:"\
+ "resource_classes:detail"
+ redirect_url = "%s?tab=resource_class_details__flavors" % (
+ reverse(redirect_url, args=(resource_class.id,)))
+ self.assertRedirectsNoFollow(res, redirect_url)
+
+ # def test_detail_get_exception(self):
+ # index_url = reverse('horizon:infractructure:resource_management:'
+ # 'resource_classes:index')
+ # detail_url = reverse('horizon:infrastructure:resource_management:'
+ # 'resource_classes:detail', args=[42])
+
+ # res = self.client.get(detail_url)
+ # self.assertRedirectsNoFollow(res, index_url)
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/urls.py b/openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/urls.py
new file mode 100644
index 00000000..c4f48fcf
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/urls.py
@@ -0,0 +1,40 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from django.conf.urls.defaults import patterns, url, include
+
+from .views import (CreateView, UpdateView, DetailView, UpdateRacksView,
+ UpdateFlavorsView, DetailUpdateView, DetailActionView)
+
+RESOURCE_CLASS = r'^(?P<resource_class_id>[^/]+)/%s$'
+VIEW_MOD = 'openstack_dashboard.dashboards.infrastructure.' \
+ 'resource_management.resource_classes.views'
+
+urlpatterns = patterns(
+ VIEW_MOD,
+ url(r'^create$', CreateView.as_view(), name='create'),
+ url(r'^(?P<resource_class_id>[^/]+)/$',
+ DetailView.as_view(), name='detail'),
+ url(RESOURCE_CLASS % 'update', UpdateView.as_view(), name='update'),
+ url(RESOURCE_CLASS % 'detail_action', DetailActionView.as_view(),
+ name='detail_action'),
+ url(RESOURCE_CLASS % 'detail_update', DetailUpdateView.as_view(),
+ name='detail_update'),
+ url(RESOURCE_CLASS % 'update_racks', UpdateRacksView.as_view(),
+ name='update_racks'),
+ url(RESOURCE_CLASS % 'update_flavors', UpdateFlavorsView.as_view(),
+ name='update_flavors'),
+ url(RESOURCE_CLASS % 'rack_health.json', 'rack_health',
+ name='rack_health'),
+)
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/views.py b/openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/views.py
new file mode 100644
index 00000000..a8c9e89c
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/views.py
@@ -0,0 +1,202 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""
+Views for managing resource classes
+"""
+import logging
+import random
+
+from django.core.urlresolvers import reverse_lazy, reverse
+from django.http import HttpResponse
+from django.utils import simplejson
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import tabs
+from horizon import exceptions
+from horizon import forms
+from horizon import workflows
+
+from openstack_dashboard import api
+
+from .forms import DeleteForm
+from .workflows import (CreateResourceClass, UpdateResourceClass,
+ UpdateRacksWorkflow, UpdateFlavorsWorkflow,
+ DetailUpdateWorkflow)
+from .tables import ResourceClassesTable
+from .tabs import ResourceClassDetailTabs
+
+LOG = logging.getLogger(__name__)
+
+
+class CreateView(workflows.WorkflowView):
+ workflow_class = CreateResourceClass
+
+ def get_initial(self):
+ pass
+
+
+class UpdateView(workflows.WorkflowView):
+ workflow_class = UpdateResourceClass
+
+ def get_context_data(self, **kwargs):
+ context = super(UpdateView, self).get_context_data(**kwargs)
+ context["resource_class_id"] = self.kwargs['resource_class_id']
+ return context
+
+ def _get_object(self, *args, **kwargs):
+ if not hasattr(self, "_object"):
+ resource_class_id = self.kwargs['resource_class_id']
+ try:
+ self._object = \
+ api.tuskar.ResourceClass.get(self.request,
+ resource_class_id)
+ except:
+ redirect = self.success_url
+ msg = _('Unable to retrieve resource class details.')
+ exceptions.handle(self.request, msg, redirect=redirect)
+
+ return self._object
+
+ def get_initial(self):
+ resource_class = self._get_object()
+
+ return {'resource_class_id': resource_class.id,
+ 'name': resource_class.name,
+ 'service_type': resource_class.service_type}
+
+
+class DetailUpdateView(UpdateView):
+ workflow_class = DetailUpdateWorkflow
+
+
+class UpdateRacksView(UpdateView):
+ workflow_class = UpdateRacksWorkflow
+
+
+class UpdateFlavorsView(UpdateView):
+ workflow_class = UpdateFlavorsWorkflow
+
+
+class DetailView(tabs.TabView):
+ tab_group_class = ResourceClassDetailTabs
+ template_name = ('infrastructure/resource_management/resource_classes/'
+ 'detail.html')
+
+ def get_context_data(self, **kwargs):
+ context = super(DetailView, self).get_context_data(**kwargs)
+ context["resource_class"] = self.get_data()
+ return context
+
+ def get_data(self):
+ if not hasattr(self, "_resource_class"):
+ try:
+ resource_class_id = self.kwargs['resource_class_id']
+ resource_class = api.tuskar.\
+ ResourceClass.get(self.request,
+ resource_class_id)
+ except:
+ redirect = reverse('horizon:infrastructure:'
+ 'resource_management:index')
+ exceptions.handle(self.request,
+ _('Unable to retrieve details for '
+ 'resource class "%s".')
+ % resource_class_id,
+ redirect=redirect)
+ self._resource_class = resource_class
+ return self._resource_class
+
+ def get_tabs(self, request, *args, **kwargs):
+ resource_class = self.get_data()
+ return self.tab_group_class(request, resource_class=resource_class,
+ **kwargs)
+
+
+class DetailActionView(forms.ModalFormView):
+ template_name = ('infrastructure/resource_management/'
+ 'resource_classes/action.html')
+
+ def get_form(self, form_class):
+ """
+ Returns an instance of the form to be used in this view.
+ """
+ try:
+ action = self.request.GET.get('action')
+ if action == "delete":
+ form_class = DeleteForm
+
+ return form_class(self.request, **self.get_form_kwargs())
+ except:
+ exceptions.handle(self.request, _("Unable to build an Action."))
+
+ def get_success_url(self):
+ # FIXME this should be set on form level
+ return reverse("horizon:infrastructure:resource_management:index")
+
+ def get_context_data(self, **kwargs):
+ context = super(DetailActionView, self).get_context_data(**kwargs)
+ context['resource_class_id'] = self.kwargs['resource_class_id']
+ context['action'] = context['form'].initial.get('action', None)
+ context['header'] = context['form'].command.header
+ return context
+
+ def get_initial(self):
+ try:
+ resource_class = api.tuskar.ResourceClass.get(
+ self.request, self.kwargs['resource_class_id'])
+ action = self.request.GET.get('action')
+ except:
+ exceptions.handle(self.request,
+ _("Unable to retrieve resource class data."))
+ return {'resource_class': resource_class,
+ 'action': action}
+
+
+def rack_health(request, resource_class_id=None):
+ # FIXME replace mock data
+ random.seed()
+ data = []
+ statuses = ["Good", "Warnings", "Disaster"]
+ colors = ["rgb(244,244,244)", "rgb(240,170,0)", "rgb(200,0,0)"]
+
+ resource_class = (api.tuskar.
+ ResourceClass.get(request,
+ resource_class_id))
+
+ for rack in resource_class.list_racks:
+ rand_index = random.randint(0, 2)
+ percentage = (2 - rand_index) * 50
+ color = colors[rand_index]
+
+ tooltip = ("<p>Rack: <strong>{0}</strong></p><p>{1}</p>").format(
+ rack.name,
+ statuses[rand_index])
+
+ data.append({'tooltip': tooltip,
+ 'color': color,
+ 'status': statuses[rand_index],
+ 'percentage': percentage,
+ 'id': rack.id,
+ 'name': rack.name,
+ 'url': "FIXME url"})
+
+ data.sort(key=lambda x: x['percentage'])
+
+ res = {'data': data}
+ return HttpResponse(simplejson.dumps(res),
+ mimetype="application/json")
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/workflows.py b/openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/workflows.py
new file mode 100644
index 00000000..24b7c4ae
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/workflows.py
@@ -0,0 +1,320 @@
+
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# 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 exceptions
+from horizon import workflows
+from horizon import forms
+
+from openstack_dashboard import api
+
+import re
+
+from .tables import FlavorsTable, RacksTable
+
+
+class ResourceClassInfoAndFlavorsAction(workflows.Action):
+ name = forms.CharField(max_length=255,
+ label=_("Class Name"),
+ help_text="",
+ required=True)
+ service_type = forms.ChoiceField(label=_('Class Type'),
+ required=True,
+ choices=[('', ''),
+ ('compute',
+ ('Compute')),
+ ('not_compute',
+ ('Non Compute')),
+ ],
+ widget=forms.Select(
+ attrs={'class': 'switchable'})
+ )
+ image = forms.ChoiceField(label=_('Provisioning Image'),
+ required=True,
+ choices=[('compute-img', ('overcloud-compute'))],
+ widget=forms.Select(
+ attrs={'class': 'switchable'})
+ )
+
+ def clean(self):
+ cleaned_data = super(ResourceClassInfoAndFlavorsAction,
+ self).clean()
+
+ name = cleaned_data.get('name')
+ resource_class_id = self.initial.get('resource_class_id', None)
+ try:
+ resource_classes = api.tuskar.ResourceClass.list(self.request)
+ except:
+ resource_classes = []
+ msg = _('Unable to get resource class list')
+ exceptions.check_message(["Connection", "refused"], msg)
+ raise
+ for resource_class in resource_classes:
+ if resource_class.name == name and \
+ resource_class_id != resource_class.id:
+ raise forms.ValidationError(
+ _('The name "%s" is already used by'
+ ' another resource class.')
+ % name
+ )
+
+ return cleaned_data
+
+ class Meta:
+ name = _("Class Settings")
+ help_text = _("From here you can fill the class "
+ "settings and add flavors to class.")
+
+
+class CreateResourceClassInfoAndFlavors(workflows.TableStep):
+ table_classes = (FlavorsTable,)
+
+ action_class = ResourceClassInfoAndFlavorsAction
+ template_name = 'infrastructure/resource_management/resource_classes/'\
+ '_resource_class_info_and_flavors_step.html'
+ contributes = ("name", "service_type", "flavors_object_ids",
+ 'max_vms')
+
+ def contribute(self, data, context):
+ request = self.workflow.request
+ if data:
+ context["flavors_object_ids"] =\
+ request.POST.getlist("flavors_object_ids")
+
+ # todo: lsmola django can't parse dictionaruy from POST
+ # this should be rewritten to django formset
+ context["max_vms"] = {}
+ for index, value in request.POST.items():
+ match = re.match(
+ '^(flavors_object_ids__max_vms__(.*?))$',
+ index)
+ if match:
+ context["max_vms"][match.groups()[1]] = value
+
+ context.update(data)
+ return context
+
+ def get_flavors_data(self):
+ try:
+ resource_class_id = self.workflow.context.get("resource_class_id")
+ if resource_class_id:
+ resource_class = api.tuskar.ResourceClass.get(
+ self.workflow.request,
+ resource_class_id)
+
+ # TODO: lsmola ugly interface, rewrite
+ self._tables['flavors'].active_multi_select_values = \
+ resource_class.flavortemplates_ids
+
+ all_flavors = resource_class.all_flavors
+ else:
+ all_flavors = api.tuskar.FlavorTemplate.list(
+ self.workflow.request)
+ except Exception, ex:
+ all_flavors = []
+ exceptions.handle(self.workflow.request,
+ _('Unable to retrieve resource flavors list.'))
+ return all_flavors
+
+
+class RacksAction(workflows.Action):
+ class Meta:
+ name = _("Racks")
+
+
+class CreateRacks(workflows.TableStep):
+ table_classes = (RacksTable,)
+
+ action_class = RacksAction
+ contributes = ("racks_object_ids")
+ template_name = 'infrastructure/resource_management/'\
+ 'resource_classes/_racks_step.html'
+
+ def contribute(self, data, context):
+ request = self.workflow.request
+ context["racks_object_ids"] =\
+ request.POST.getlist("racks_object_ids")
+
+ context.update(data)
+ return context
+
+ def get_racks_data(self):
+ try:
+ resource_class_id = self.workflow.context.get("resource_class_id")
+ if resource_class_id:
+ resource_class = api.tuskar.ResourceClass.get(
+ self.workflow.request,
+ resource_class_id)
+ # TODO: lsmola ugly interface, rewrite
+ self._tables['racks'].active_multi_select_values = \
+ resource_class.racks_ids
+ racks = \
+ resource_class.all_racks
+ else:
+ racks = \
+ api.tuskar.Rack.list(self.workflow.request, True)
+ except:
+ racks = []
+ exceptions.handle(self.workflow.request,
+ _('Unable to retrieve racks list.'))
+
+ return racks
+
+
+class ResourceClassWorkflowMixin:
+ # FIXME active tabs coflict
+ # When on page with tabs, the workflow with more steps is used,
+ # there is a conflict of active tabs and it always shows the
+ # first tab after an action. So I explicitly specify to what
+ # tab it should redirect after action, until the coflict will
+ # be fixed in Horizon.
+ def get_index_url(self):
+ """This url is used both as success and failure url"""
+ return "%s?tab=resource_management_tabs__resource_classes_tab" %\
+ reverse("horizon:infrastructure:resource_management:index")
+
+ def get_success_url(self):
+ return self.get_index_url()
+
+ def get_failure_url(self):
+ return self.get_index_url()
+
+ def format_status_message(self, message):
+ name = self.context.get('name')
+ return message % name
+
+ def _get_flavors(self, request, data):
+ flavors = []
+ flavor_ids = data.get('flavors_object_ids') or []
+ max_vms = data.get('max_vms')
+ resource_class_name = data['name']
+ for template_id in flavor_ids:
+ template = api.tuskar.FlavorTemplate.get(request, template_id)
+ capacities = []
+ for c in template.capacities:
+ capacities.append({'name': c.name,
+ 'value': str(c.value),
+ 'unit': c.unit})
+ # FIXME: tuskar uses resource-class-name prefix for flavors,
+ # e.g. m1.large, we add rc name to the template name:
+ flavor_name = "%s.%s" % (resource_class_name, template.name)
+ flavors.append({'name': flavor_name,
+ 'max_vms': max_vms.get(template.id, None),
+ 'capacities': capacities})
+ return flavors
+
+ def _add_racks(self, request, data, resource_class):
+ ids_to_add = data.get('racks_object_ids') or []
+ resource_class.set_racks(request, ids_to_add)
+
+
+class CreateResourceClass(ResourceClassWorkflowMixin, workflows.Workflow):
+ default_steps = (CreateResourceClassInfoAndFlavors,
+ CreateRacks)
+
+ slug = "create_resource_class"
+ name = _("Create Class")
+ finalize_button_name = _("Create Class")
+ success_message = _('Created class "%s".')
+ failure_message = _('Unable to create class "%s".')
+
+ def _create_resource_class_info(self, request, data):
+ try:
+ flavors = self._get_flavors(request, data)
+ return api.tuskar.ResourceClass.create(
+ request,
+ name=data['name'],
+ service_type=data['service_type'],
+ flavors=flavors)
+ except:
+ redirect = self.get_failure_url()
+ exceptions.handle(request,
+ _('Unable to create resource class.'),
+ redirect=redirect)
+ return None
+
+ def handle(self, request, data):
+ resource_class = self._create_resource_class_info(request, data)
+ self._add_racks(request, data, resource_class)
+ return True
+
+
+class UpdateResourceClassInfoAndFlavors(CreateResourceClassInfoAndFlavors):
+ depends_on = ("resource_class_id",)
+
+
+class UpdateRacks(CreateRacks):
+ depends_on = ("resource_class_id",)
+
+
+class UpdateResourceClass(ResourceClassWorkflowMixin, workflows.Workflow):
+ default_steps = (UpdateResourceClassInfoAndFlavors,
+ UpdateRacks)
+
+ slug = "update_resource_class"
+ name = _("Update Class")
+ finalize_button_name = _("Update Class")
+ success_message = _('Updated class "%s".')
+ failure_message = _('Unable to update class "%s".')
+
+ def _update_resource_class_info(self, request, data):
+ try:
+ flavors = self._get_flavors(request, data)
+ return api.tuskar.ResourceClass.update(
+ request,
+ data['resource_class_id'],
+ name=data['name'],
+ service_type=data['service_type'],
+ flavors=flavors)
+ except:
+ redirect = self.get_failure_url()
+ exceptions.handle(request,
+ _('Unable to create resource class.'),
+ redirect=redirect)
+ return None
+
+ def handle(self, request, data):
+ resource_class = self._update_resource_class_info(request, data)
+ self._add_racks(request, data, resource_class)
+ return True
+
+
+class DetailUpdateWorkflow(UpdateResourceClass):
+ def get_index_url(self):
+ """This url is used both as success and failure url"""
+ url = "horizon:infrastructure:resource_management:resource_classes:"\
+ "detail"
+ return "%s?tab=resource_class_details__overview" % (
+ reverse(url, args=(self.context["resource_class_id"])))
+
+
+class UpdateRacksWorkflow(UpdateResourceClass):
+ def get_index_url(self):
+ """This url is used both as success and failure url"""
+ url = "horizon:infrastructure:resource_management:resource_classes:"\
+ "detail"
+ return "%s?tab=resource_class_details__racks" % (
+ reverse(url, args=(self.context["resource_class_id"])))
+
+
+class UpdateFlavorsWorkflow(UpdateResourceClass):
+ def get_index_url(self):
+ """This url is used both as success and failure url"""
+ url = "horizon:infrastructure:resource_management:resource_classes:"\
+ "detail"
+ return "%s?tab=resource_class_details__flavors" % (
+ reverse(url, args=(self.context["resource_class_id"])))
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/tabs.py b/openstack_dashboard/dashboards/infrastructure/resource_management/tabs.py
new file mode 100644
index 00000000..a4800022
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/tabs.py
@@ -0,0 +1,93 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import exceptions
+from horizon import messages
+from horizon import tabs
+
+from openstack_dashboard.api import tuskar
+
+from .flavors.tables import FlavorsTable
+from .racks.tables import RacksTable
+from .resource_classes.tables import ResourceClassesTable
+
+
+class RacksTab(tabs.TableTab):
+ table_classes = (RacksTable,)
+ name = _("Racks")
+ slug = "racks_tab"
+ template_name = ("infrastructure/resource_management/"
+ "racks/_index_table.html")
+
+ def get_racks_data(self):
+ try:
+ racks = tuskar.Rack.list(self.request)
+ except:
+ racks = []
+ exceptions.handle(self.request,
+ _('Unable to retrieve racks.'))
+ return racks
+
+ def get_context_data(self, request):
+ context = super(RacksTab, self).get_context_data(request)
+ try:
+ context["nodes"] = tuskar.Node.list_unracked(self.request)
+ except:
+ context["nodes"] = []
+ exceptions.handle(request,
+ _('Unable to retrieve nodes.'))
+ return context
+
+
+class FlavorsTab(tabs.TableTab):
+ table_classes = (FlavorsTable,)
+ name = _("Flavors")
+ slug = "flavors_tab"
+ template_name = "horizon/common/_detail_table.html"
+
+ def get_flavors_data(self):
+ try:
+ flavors = tuskar.FlavorTemplate.list(self.request)
+ except:
+ flavors = []
+ exceptions.handle(self.request,
+ _('Unable to retrieve tuskar flavors.'))
+ return flavors
+
+
+class ResourceClassesTab(tabs.TableTab):
+ table_classes = (ResourceClassesTable,)
+ name = _("Classes")
+ slug = "resource_classes_tab"
+ template_name = "horizon/common/_detail_table.html"
+ #preload = False buggy, checkboxes doesn't work wit table actions
+
+ def get_resource_classes_data(self):
+ try:
+ resource_classes = tuskar.ResourceClass.list(self.request)
+ except:
+ resource_classes = []
+ exceptions.handle(self.request,
+ _('Unable to retrieve resource classes list.'))
+ return resource_classes
+
+
+class ResourceManagementTabs(tabs.TabGroup):
+ slug = "resource_management_tabs"
+ tabs = (ResourceClassesTab, RacksTab, FlavorsTab)
+ sticky = True
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/flavors/_create.html b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/flavors/_create.html
new file mode 100644
index 00000000..7353b93b
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/flavors/_create.html
@@ -0,0 +1,26 @@
+{% extends "horizon/common/_modal_form.html" %}
+{% load i18n %}
+{% load url from future %}
+
+{% block form_id %}create_flavor_form{% endblock %}
+{% block form_action %}{% url 'horizon:infrastructure:resource_management:flavors:create' %}{% endblock %}
+
+{% block modal_id %}create_flavor_modal{% endblock %}
+{% block modal-header %}{% trans "Create Flavor" %}{% 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 "From here you can define the sizing of a new flavor." %}</p>
+</div>
+{% endblock %}
+
+{% block modal-footer %}
+ <input class="btn btn-primary pull-right" type="submit" value="{% trans "Create Flavor" %}" />
+ <a href="{% url 'horizon:infrastructure:resource_management:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/flavors/_detail_overview.html b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/flavors/_detail_overview.html
new file mode 100644
index 00000000..ac637b0b
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/flavors/_detail_overview.html
@@ -0,0 +1,128 @@
+{% load i18n sizeformat %}
+{% load url from future %}
+
+<div class="info row-fluid detail">
+ <div class="span4">
+ <h4>{% trans "About" %}</h4>
+ <hr class="header_rule">
+ <dl>
+ <dt>{% trans "Name" %}</dt>
+ <dd>{{ flavor.name|default:_("None") }}</dd>
+ <dt>{% trans "Instances" %}</dt>
+ <dd>{{ flavor.running_virtual_machines }}</dd>
+ </dl>
+ </div>
+
+ <div class="span4">
+ <h4>{% trans "Specification" %}</h4>
+ <hr class="header_rule">
+ <dl>
+ <dt>{% trans "VCPU" %}</dt>
+ <dd>{{ flavor.cpu.value }}</dd>
+ <dt>{% trans "RAM" %}</dt>
+ <dd>{{ flavor.memory.value }} {{ flavor.memory.unit }}</dd>
+ <dt>{% trans "Root Disk" %}</dt>
+ <dd>{{ flavor.storage.value }} {{ flavor.storage.unit }}</dd>
+ <dt>{% trans "Ephemeral Disk" %}</dt>
+ <dd>{{ flavor.ephemeral_disk.value }} {{ flavor.ephemeral_disk.unit }}</dd>
+ <dt>{% trans "Swap Disk" %}</dt>
+ <dd>{{ flavor.swap_disk.value }} {{ flavor.swap_disk.unit }}</dd>
+ </dl>
+ </div>
+
+ <div class="span4">
+ <h4>{% trans "Capacities" %}</h4>
+ <hr class="header_rule">
+ <table class="capacities">
+ <tr>
+ <td class="capacity_label">{% trans "CPU" %}:</td>
+ <td>
+ <div id="cpu_capacity_usage"
+ class="capacity_bar"
+ data-chart-type="capacity_bar_chart"
+ data-capacity-limit="{{ flavor.cpu.value }}"
+ data-capacity-used="{{ flavor.cpu.usage }}"
+ data-average-capacity-used="{{ flavor.cpu.average }}">
+ </div>
+ </td>
+ <td>
+ <a href="#" data-chart-type="modal_line_chart" data-url="/infrastructure/resource_management/racks/usage_data" data-series="cpu">{{ flavor.cpu.usage }}/{{ flavor.cpu.value }} {{ flavor.cpu.unit }}</a>
+ </td>
+ </tr>
+ <tr>
+ <td class="capacity_label">{% trans "RAM" %}:</td>
+ <td>
+ <div id="ram_capacity_usage"
+ class="capacity_bar"
+ data-chart-type="capacity_bar_chart"
+ data-capacity-limit="{{ flavor.memory.value }}"
+ data-capacity-used="{{ flavor.memory.usage }}"
+ data-average-capacity-used="{{ flavor.memory.average }}">
+ </div>
+ </td>
+ <td>
+ <a href="#" data-chart-type="modal_line_chart" data-url="/infrastructure/resource_management/racks/usage_data" data-series="ram">{{ flavor.memory.usage }}/{{ flavor.memory.value }} {{ flavor.memory.unit }}</a>
+ </td>
+ </tr>
+ <tr>
+ <td class="capacity_label">{% trans "Root Disk" %}:</td>
+ <td>
+ <div id="root_disk_capacity_usage"
+ class="capacity_bar"
+ data-chart-type="capacity_bar_chart"
+ data-capacity-limit="{{ flavor.storage.value }}"
+ data-capacity-used="{{ flavor.storage.usage }}"
+ data-average-capacity-used="{{ flavor.storage.average }}">
+ </div>
+ </td>
+ <td>
+ <a href="#" data-chart-type="modal_line_chart" data-url="/infrastructure/resource_management/racks/usage_data" data-series="root_disk">{{ flavor.storage.usage }}/{{ flavor.storage.value }} {{ flavor.storage.unit }}</a>
+ </td>
+ </tr>
+ <tr>
+ <td class="capacity_label">{% trans "Ephemeral Disk" %}:</td>
+ <td>
+ <div id="ephemeral_disk_capacity_usage"
+ class="capacity_bar"
+ data-chart-type="capacity_bar_chart"
+ data-capacity-limit="{{ flavor.ephemeral_disk.value }}"
+ data-capacity-used="{{ flavor.ephemeral_disk.usage }}"
+ data-average-capacity-used="{{ flavor.ephemeral_disk.average }}">
+ </div>
+ </td>
+ <td>
+ <a href="#" data-chart-type="modal_line_chart" data-url="/infrastructure/resource_management/racks/usage_data" data-series="ephemeral_disk">{{ flavor.ephemeral_disk.usage }}/{{ flavor.ephemeral_disk.value }} {{ flavor.ephemeral_disk.unit }}</a>
+ </td>
+ </tr>
+ <tr>
+ <td class="capacity_label">{% trans "Swap Disk" %}:</td>
+ <td>
+ <div id="swap_disk_capacity_usage"
+ class="capacity_bar"
+ data-chart-type="capacity_bar_chart"
+ data-capacity-limit="{{ flavor.swap_disk.value }}"
+ data-capacity-used="{{ flavor.swap_disk.usage }}"
+ data-average-capacity-used="{{ flavor.swap_disk.average }}">
+ </div>
+ </td>
+ <td>
+ <a href="#" data-chart-type="modal_line_chart" data-url="/infrastructure/resource_management/racks/usage_data" data-series="ephemeral_disk">{{ flavor.swap_disk.usage }}/{{ flavor.swap_disk.value }} {{ flavor.swap_disk.unit }}</a>
+ </td>
+ </tr>
+ </table>
+ </div>
+</div>
+
+<div class="info row-fluid detail">
+ <div class="span12">
+ <h4>{% trans "Active Instances" %}</h4>
+ <hr class="header_rule">
+ <table class="chart">
+ <tr>
+ <td>
+ <div data-chart-type="line_chart" data-url="{% url 'horizon:infrastructure:resource_management:flavors:active_instances_data' flavor.id%}" data-series="active_instances"></div>
+ </td>
+ </tr>
+ </table>
+ </div>
+</div>
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/flavors/_edit.html b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/flavors/_edit.html
new file mode 100644
index 00000000..44740745
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/flavors/_edit.html
@@ -0,0 +1,27 @@
+{% extends "horizon/common/_modal_form.html" %}
+{% load i18n %}
+{% load url from future %}
+
+{% block form_id %}edit_flavor_form{% endblock %}
+{% block form_action %}{% url form_url flavor_id %}{% endblock %}
+
+{% block modal_id %}edit_flavor_modal{% endblock %}
+{% block modal-header %}{% trans "Edit Flavor" %}{% 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 "From here you can alter the sizing of the current flavor." %}</p>
+ <p>{% trans "Note: this will not affect the resources allocated to any existing instances using this flavor." %}</p>
+</div>
+{% endblock %}
+
+{% block modal-footer %}
+ <input class="btn btn-primary pull-right" type="submit" value="{% trans "Save" %}" />
+ <a href="{{ success_url }}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/flavors/create.html b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/flavors/create.html
new file mode 100644
index 00000000..afa99a12
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/flavors/create.html
@@ -0,0 +1,11 @@
+{% 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 "infrastructure/resource_management/flavors/_create.html" %}
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/flavors/detail.html b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/flavors/detail.html
new file mode 100644
index 00000000..02a948b8
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/flavors/detail.html
@@ -0,0 +1,24 @@
+{% extends 'infrastructure/base_detail.html' %}
+{% load i18n %}
+{% block title %}{% trans "Flavor Detail"%}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Flavor Detail") %}
+{% endblock page_header %}
+
+{% block actions %}
+ <div class="btn-group">
+ <a class="btn ajax-modal" href="{% url 'horizon:infrastructure:resource_management:flavors:detail_edit' flavor.id %}">{% trans "Edit" %}</a>
+ </div>
+{% endblock %}
+
+{% block breadcrumbs %}
+ <div class="breadcrumbs">
+ <a href="{% url 'horizon:infrastructure:resource_management:index' %}?tab=resource_management_tabs__resource_classes_tab" >Home</a>
+ <span class="separator"></span>
+ <a href="{% url 'horizon:infrastructure:resource_management:index' %}?tab=resource_management_tabs__flavors_tab" >Flavors</a>
+ <span class="separator"></span>
+ </div>
+{% endblock breadcrumbs %}
+
+{% block name %}{{ flavor.name }}{% endblock %}
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/flavors/edit.html b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/flavors/edit.html
new file mode 100644
index 00000000..1ccb910b
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/flavors/edit.html
@@ -0,0 +1,12 @@
+{% extends 'infrastructure/base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Edit Flavor" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Edit Flavor") %}
+{% endblock page_header %}
+
+{% block main %}
+ {% include "infrastructure/resource_management/flavors/_edit.html" %}
+{% endblock %}
+
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/index.html b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/index.html
new file mode 100644
index 00000000..a86727e7
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/index.html
@@ -0,0 +1,25 @@
+{% extends 'infrastructure/base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Resource Management" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Resource Management") %}
+{% endblock page_header %}
+
+{% block main %}
+<div class="row-fluid">
+ <div class="span12">
+ <div class="breadcrumbs">
+ <a href="{% url 'horizon:infrastructure:resource_management:index' %}?tab=resource_management_tabs__resource_classes_tab" >Home</a>
+ <span class="separator"></span>
+ </div>
+ </div>
+</div>
+
+<div class="row-fluid">
+ <div class="span12">
+ {{ tab_group.render }}
+ </div>
+</div>
+{% endblock %}
+
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/nodes/_detail_overview.html b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/nodes/_detail_overview.html
new file mode 100644
index 00000000..71b83891
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/nodes/_detail_overview.html
@@ -0,0 +1,258 @@
+{% load i18n sizeformat %}
+{% load url from future %}
+{% load chart_helpers %}
+
+<div class="info row-fluid detail">
+ <div class="span4">
+ <h4>{% trans "About" %}</h4>
+ <hr class="header_rule">
+ <dl>
+ <dt>{% trans "MAC Address" %}</dt>
+ <dd>{{ node.mac_address|default:_("None") }}</dd>
+ <dt>{% trans "IPs" %}</dt>
+ <dd>{{ node.ip_address_other|default:_("None") }}</dd>
+ <dt>{% trans "Management IP" %}</dt>
+ <dd>{{ node.pm_address|default:_("None") }}</dd>
+ <dt>{% trans "Power Management" %}</dt>
+ <dd>{{ node.rack.power_management|default:_("-") }}</dd>
+ <dt>{% trans "Status" %}</dt>
+ <dd>{{ node.status|default:_("None") }}</dd>
+ </dl>
+ </div>
+ <div class="span4">
+ <h4>{% trans "Resource Assignment" %}</h4>
+ <hr class="header_rule">
+ <dl>
+ <dt>{% trans "Rack" %}</dt>
+ {% if node.rack %}
+ <dd><a href="{% url 'horizon:infrastructure:resource_management:racks:detail' node.rack.id %}">{{ node.rack.name|default:_("None") }}</a></dd>
+ {% else %}
+ <dd>{% trans "None" %}</dd>
+ {% endif %}
+ <!--<dt>{% trans "Region" %}</dt>
+ <dd>{{ node.region|default:_("None") }}</dd>-->
+ <dt>{% trans "Node Type" %}</dt>
+ <dd>{{ node.type|default:_("none") }}</dd>
+ <dt>{% trans "Provisioned Image" %}</dt>
+ <dd>{{ node.image|default:_("None") }}</dd>
+ <dt>{% trans "Running Instances" %}</dt>
+ <dd>{{ node.running_virtual_machines|length }}</dd>
+ </dl>
+ </div>
+ <div class="span4">
+ <h4>{% trans "Capacities" %}</h4>
+ <hr class="header_rule">
+ <table class="capacities">
+ <tr>
+ <td class="capacity_label">{% trans "CPU" %}:</td>
+ {% if node.is_provisioned %}
+ <td>
+ <div id="cpu_capacity_usage"
+ class="capacity_bar"
+ data-chart-type="capacity_bar_chart"
+ data-capacity-limit="{{ node.cpu.value }}"
+ data-capacity-used="{{ node.cpu.usage }}"
+ data-average-capacity-used="{{ 2 }}">
+ </div>
+ </td>
+ <td>
+ <a href="#" data-chart-type="modal_line_chart" data-url="/infrastructure/resource_management/racks/usage_data" data-series="cpu">{{ node.cpu.usage }}/{{ node.cpu.value }} {{ node.cpu.unit }}</a>
+ </td>
+ {% else %}
+ <td>
+ <div id="cpu_capacity_usage"
+ class="capacity_bar"
+ data-chart-type="capacity_bar_chart">
+ </div>
+ </td>
+ <td></td>
+ {% endif %}
+ </tr>
+ <tr>
+ <td class="capacity_label">{% trans "RAM" %}:</td>
+ {% if node.is_provisioned %}
+ <td>
+ <div id="ram_capacity_usage"
+ class="capacity_bar"
+ data-chart-type="capacity_bar_chart"
+ data-capacity-limit="{{ node.ram.value }}"
+ data-capacity-used="{{ node.ram.usage }}"
+ data-average-capacity-used="{{ 12 }}">
+ </div>
+ </td>
+ <td>
+ <a href="#" data-chart-type="modal_line_chart" data-url="/infrastructure/resource_management/racks/usage_data" data-series="ram">{{ node.ram.usage }}/{{ node.ram.value }} {{ node.ram.unit }}</a>
+ </td>
+ {% else %}
+ <td>
+ <div id="ram_capacity_usage"
+ class="capacity_bar"
+ data-chart-type="capacity_bar_chart">
+ </div>
+ </td>
+ <td></td>
+ {% endif %}
+ </tr>
+ <tr>
+ <td class="capacity_label">{% trans "Storage" %}:</td>
+ {% if node.is_provisioned %}
+ <td>
+ <div id="storage_capacity_usage"
+ class="capacity_bar"
+ data-chart-type="capacity_bar_chart"
+ data-capacity-limit="{{ node.storage.value }}"
+ data-capacity-used="{{ node.storage.usage }}"
+ data-average-capacity-used="{{ 6 }}">
+ </div>
+ </td>
+ <td>
+ <a href="#" data-chart-type="modal_line_chart" data-url="/infrastructure/resource_management/racks/usage_data" data-series="storage">{{ node.storage.usage }}/{{ node.storage.value }} {{ node.storage.unit }}</a>
+ </td>
+ {% else %}
+ <td>
+ <div id="storage_capacity_usage"
+ class="capacity_bar"
+ data-chart-type="capacity_bar_chart">
+ </div>
+ </td>
+ <td></td>
+ {% endif %}
+ </tr>
+ <tr>
+ <td class="capacity_label">{% trans "Network" %}:</td>
+ {% if node.is_provisioned %}
+ <td>
+ <div id="network_capacity_usage"
+ class="capacity_bar"
+ data-chart-type="capacity_bar_chart"
+ data-capacity-limit="{{ node.network.value }}"
+ data-capacity-used="{{ node.network.usage }}"
+ data-average-capacity-used="{{ 40 }}">
+ </div>
+ </td>
+ <td>
+ <a href="#" data-chart-type="modal_line_chart" data-url="/infrastructure/resource_management/racks/usage_data" data-series="network">{{ node.network.usage }}/{{ node.network.value }} {{ node.network.unit }}</a>
+ </td>
+ {% else %}
+ <td>
+ <div id="network_capacity_usage"
+ class="capacity_bar"
+ data-chart-type="capacity_bar_chart">
+ </div>
+ </td>
+ <td></td>
+ {% endif %}
+ </tr>
+ </table>
+ </div>
+</div>
+
+<div class="row-fluid detail">
+ <div class="span6">
+ <h4>{% trans "Summary of Instances and Usage" %}</h4>
+ <hr class="header_rule">
+ {% if node.is_provisioned %}
+ <div>
+ <strong>{{ node.running_instances }}</strong> instances
+ <strong>{{ node.remaining_capacity }}%</strong> capacity remaining
+ </div>
+ <div class="flavor_usage_bar"
+ data-popup-free='{{ node|remaining_capacity_by_flavors }}'
+ data-single-bar-orientation="horizontal"
+ data-single-bar-height="50"
+ data-single-bar-width="100%"
+ data-single-bar-used="{{ node|all_used_instances }}"
+ data-single-bar-auto-scale-selector=".flavors_scale_selector"
+ data-single-bar-color-scale-range='["#000060", "#99FFFF"]'>
+ </div>
+
+ <table class="flavor_usages">
+ <tr>
+ {% for flavor in node.list_flavors %}
+ <td class="flavor_usage_label">
+ <a href="{% url 'horizon:infrastructure:resource_management:flavors:detail' flavor.id %}">{{ flavor.name }}</a>
+ </td>
+ {% endfor %}
+ </tr>
+ <tr>
+ {% for flavor in node.list_flavors %}
+ <td>
+ <div class="flavor_usage_bar flavors_scale_selector"
+ data-popup-average='<p>Average capacity consumed by instances of {{flavor.name}} flavor in {{node.name}} class.</p>
+ <p>{{ flavor.used_instances }}%, <strong>{{ flavor.used_instances }} instances</strong></p>'
+ data-single-bar-orientation="vertical"
+ data-single-bar-height="100%"
+ data-single-bar-width="40"
+ data-single-bar-used="{{ flavor.used_instances }}"
+ data-single-bar-average-used="{{ 50 }}"
+ data-single-bar-auto-scale-selector=".flavors_scale_selector"
+ data-single-bar-color-scale-range='["#000060", "#99FFFF"]'>
+ </div>
+ </td>
+ {% endfor %}
+ </tr>
+ <tr>
+ {% for flavor in node.list_flavors %}
+ <td class="modal_chart flavor_usage_text"><a href="{{ "#" }}">{{ flavor.used_instances }}%</a></td>
+ {% endfor %}
+ </tr>
+ <tr>
+ {% for flavor in node.list_flavors %}
+ <td class="flavor_usage_text">{{ flavor.used_instances }} inst.</td>
+ {% endfor %}
+ </tr>
+ </table>
+
+ {% else %}
+ <p>{% trans "No data available yet." %}</p>
+ {% endif %}
+ </div>
+
+ <div class="span6">
+ <h4>{% trans "Active alerts" %}</h4>
+ <hr class="header_rule">
+ {% if node.is_provisioned %}
+ <ul>
+ {% for alert in node.alerts %}
+ <li><i class="icon-warning-sign"></i>{{ alert.message }}</li>
+ {% endfor %}
+ </ul>
+ {% else %}
+ <p>{% trans "No data available yet." %}</p>
+ {% endif %}
+ </div>
+</div>
+
+<div class="row-fluid detail">
+ <div class="span6">
+ <h4>{% trans "Top Communicating Nodes" %}</h4>
+ <hr class="header_rule">
+ {% if node.is_provisioned %}
+ <div class="communication_charts_wrapper">
+ <div class="communication_chart_wrapper">
+ <h5>The most contacting</h5>
+ <div id="most_contacting_racks"
+ class="communication_chart"
+ data-chart-type="circles_chart"
+ data-url="{% url 'horizon:infrastructure:resource_management:racks:top_communicating' node.rack.id %}?cond=from"
+ data-time="now"
+ data-size="22">
+ </div>
+ </div>
+ <div class="communication_chart_connection"></div>
+ <div class="communication_chart_wrapper">
+ <h5>The most contacted</h5>
+ <div id="most_contacted_racks"
+ class="communication_chart"
+ data-chart-type="circles_chart"
+ data-url="{% url 'horizon:infrastructure:resource_management:racks:top_communicating' node.rack.id %}?cond=to"
+ data-time="now"
+ data-size="22">
+ </div>
+ </div>
+ </div>
+ {% else %}
+ <p>{% trans "No data available yet." %}</p>
+ {% endif %}
+ </div>
+</div>
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/nodes/detail.html b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/nodes/detail.html
new file mode 100644
index 00000000..729727e2
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/nodes/detail.html
@@ -0,0 +1,48 @@
+{% extends 'infrastructure/base_detail.html' %}
+{% load i18n %}
+{% block title %}{% trans "Node Detail"%}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Node Detail") %}
+{% endblock page_header %}
+
+{% block breadcrumbs %}
+ <div class="breadcrumbs">
+ <a href="{% url 'horizon:infrastructure:resource_management:index' %}?tab=resource_management_tabs__resource_classes_tab" >Home</a>
+ <span class="separator"></span>
+ <a href="{% url 'horizon:infrastructure:resource_management:index' %}?tab=resource_management_tabs__racks_tab" >Racks</a>
+ <span class="separator"></span>
+ {% if node.rack %}
+ <a href="{% url 'horizon:infrastructure:resource_management:racks:detail' node.rack.id %}">{{ node.rack.name }}</a>
+ {% else %}
+ <a href="{% url 'horizon:infrastructure:resource_management:nodes:unracked' %}" >Unracked Nodes</a>
+ {% endif %}
+ <span class="separator"></span>
+ </div>
+{% endblock breadcrumbs %}
+
+{% block name %}{{ node.name }}{% endblock %}
+
+{% block overall_usage %}
+ <table class="capacities overall_usage">
+ <tr>
+ <td class="capacity_label">{% trans "Usage" %}:</td>
+ <td>
+ {% if node.is_provisioned %}
+ <div id="node_usage"
+ class="capacity_bar"
+ data-chart-type="capacity_bar_chart"
+ data-capacity-limit="{{ node.vm_capacity.value }}"
+ data-capacity-used="{{ node.vm_capacity.usage }}"
+ data-average-capacity-used="{{ 4 }}">
+ </div>
+ {% else %}
+ <div id="node_usage"
+ class="capacity_bar"
+ data-chart-type="capacity_bar_chart">
+ </div>
+ {% endif %}
+ </td>
+ </tr>
+ </table>
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/nodes/unracked.html b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/nodes/unracked.html
new file mode 100644
index 00000000..19a3145a
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/nodes/unracked.html
@@ -0,0 +1,29 @@
+{% extends 'infrastructure/base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Resource Management" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Resource Management") %}
+{% endblock page_header %}
+
+
+{% block main %}
+<div class="row-fluid">
+ <div class="span12">
+ <div class="breadcrumbs">
+ <a href="{% url 'horizon:infrastructure:resource_management:index' %}?tab=resource_management_tabs__resource_classes_tab" >Home</a>
+ <span class="separator"></span>
+ <a href="{% url 'horizon:infrastructure:resource_management:index' %}?tab=resource_management_tabs__racks_tab" >Racks</a>
+ <span class="separator"></span>
+ </div>
+
+ <h3>{% trans "Unracked Nodes" %}</h3>
+ </div>
+</div>
+
+<div class="row-fluid">
+ <div class="span12">
+ {{ table.render }}
+ </div>
+</div>
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/_create.html b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/_create.html
new file mode 100644
index 00000000..491ad77d
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/_create.html
@@ -0,0 +1,22 @@
+{% extends "horizon/common/_modal_form.html" %}
+{% load i18n %}
+{% load url from future %}
+
+{% block form_id %}create_rack_form{% endblock %}
+{% block form_action %}{% url 'horizon:infrastructure:resource_management:racks:create' %}{% endblock %}
+
+{% block modal_id %}create_rack_modal{% endblock %}
+{% block modal-header %}{% trans "Create Rack" %}{% endblock %}
+
+{% block modal-body %}
+<div>
+ <fieldset>
+ {% include "horizon/common/_form_fields.html" %}
+ </fieldset>
+</div>
+{% endblock %}
+
+{% block modal-footer %}
+ <input class="btn btn-primary pull-right" type="submit" value="{% trans "Create Rack" %}" />
+ <a href="{% url 'horizon:infrastructure:resource_management:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/_detail_overview.html b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/_detail_overview.html
new file mode 100644
index 00000000..3292f5a4
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/_detail_overview.html
@@ -0,0 +1,316 @@
+{% load i18n sizeformat %}
+{% load url from future %}
+{% load chart_helpers %}
+
+{% if not rack.is_provisioned or rack.is_provisioning %}
+ <div class="info row-fluid detail">
+ <div class="span12">
+ <div data-state="{{rack.state}}"
+ data-url="{% url 'horizon:infrastructure:resource_management:racks:check_state' rack.id %}"
+ data-interval="20000"
+ class="overall-state well provision-block" style="text-align: center; margin: 2px;">
+ {% if rack.is_provisioning %}
+ <p>Provisioning</p><img src="/static/dashboard/img/horizontal_loader.gif" />
+ {% else %}
+ <a class="btn btn-large btn-block btn-primary ajax-modal"
+ href="{% url 'horizon:infrastructure:resource_management:racks:edit_status' rack.id %}?action=provision">
+ {% trans "Provision Rack" %}
+ </a>
+ {% endif %}
+ </div>
+ </div>
+ </div>
+{% endif %}
+<div class="info row-fluid detail">
+ <div class="span4">
+ <h4>{% trans "About" %}</h4>
+ <hr class="header_rule">
+ <dl>
+ <dt>{% trans "IP Subnet" %}</dt>
+ <dd>{{ rack.subnet|default:_("None") }}</dd>
+ <dt>{% trans "Switch IPs" %}</dt>
+ <dd>{{ rack.switch_ips|default:_("None") }}</dd>
+ <dt>{% trans "Location" %}</dt>
+ <dd>{{ rack.location|default:_("None") }}</dd>
+ <dt>{% trans "State" %}</dt>
+ <dd>{{ rack.state|default:_("None") }}</dd>
+ </dl>
+ </div>
+ <div class="span4">
+ <h4>{% trans "Resource Assignment" %}</h4>
+ <hr class="header_rule">
+ <dl>
+ <dt>{% trans "Nodes" %}</dt>
+ <dd>
+ <a href="{% url 'horizon:infrastructure:resource_management:racks:detail' rack.id %}?tab=rack_detail_tabs__nodes">
+ {{ rack.nodes_count|default:_("None") }}
+ </a>
+ </dd>
+ <dt>{% trans "Class" %}</dt>
+ <dd>
+ <a href="{% url 'horizon:infrastructure:resource_management:resource_classes:detail' rack.resource_class.id %}">
+ {{ rack.resource_class.name|default:_("None") }}
+ </a>
+ </dd>
+ <dt>{% trans "Class Image" %}</dt>
+ <dd>overcloud-compute</dd> {# FIXME It will eventually have to be not-hardcoded later #}
+ </dl>
+ </div>
+ <div class="span4">
+ <h4>{% trans "Capacities" %}</h4>
+ <hr class="header_rule">
+ {% if rack.capacities %}
+ <table class="capacities">
+ {% for capacity in rack.capacities %}
+ <tr>
+ <td class="capacity_label">{{ capacity.name }}:</td>
+ {% if rack.is_provisioned %}
+ <td>
+ <div id="{{ capacity.name }}_capacity_usage"
+ class="capacity_bar"
+ data-chart-type="capacity_bar_chart"
+ data-capacity-limit="{{ capacity.value }}"
+ data-capacity-used="{{ capacity.usage }}"
+ data-average-capacity-used="{{ capacity.average }}">
+ </div>
+ </td>
+ <td>
+ <a href="#" data-chart-type="modal_line_chart" data-url="/infrastructure/resource_management/racks/usage_data">{{ capacity.usage|default:_(" - ") }}/{{ capacity.value|default:_(" - ") }} {{ capacity.unit }}</a>
+ </td>
+ {% else %}
+ <td>
+ <div id="{{ capacity.name }}_capacity_usage"
+ class="capacity_bar"
+ data-chart-type="capacity_bar_chart">
+ </div>
+ </td>
+ <td></td>
+ {% endif %}
+ </tr>
+ {% endfor %}
+ </table>
+ {% else %}
+ <p>No data available yet.</p>
+ {% endif %}
+ </div>
+</div>
+
+<div class="info row-fluid detail">
+ <div class="span6">
+ <h4>{% trans "Summary of instances and Usage" %}</h4>
+ <hr class="header_rule">
+ {% if rack.is_provisioned %}
+ <div>
+ <strong>{{ rack.total_instances }}</strong> instances
+ <strong>{{ rack.remaining_capacity }}%</strong> capacity remaining
+ </div>
+
+ <div class="flavor_usage_bar"
+ data-popup-free='{{ rack|remaining_capacity_by_flavors }}'
+ data-single-bar-orientation="horizontal"
+ data-single-bar-height="35"
+ data-single-bar-width="100%"
+ data-single-bar-used="{{ rack|all_used_instances }}"
+ data-single-bar-auto-scale-selector=".flavors_scale_selector"
+ data-single-bar-color-scale-range='["#000060", "#99FFFF"]'>
+ </div>
+
+ <table class="flavor_usages">
+ <tr>
+ {% for flavor in rack.list_flavors %}
+ <td class="flavor_usage_label">
+ <a href="{% url 'horizon:infrastructure:resource_management:flavors:detail' flavor.id %}">{{ flavor.name }}</a>
+ </td>
+ {% endfor %}
+ </tr>
+ <tr>
+ {% for flavor in rack.list_flavors %}
+ <td>
+ <div class="flavor_usage_bar flavors_scale_selector"
+ data-popup-average='<p>Average capacity consumed by instances of {{flavor.name}} flavor in {{rack.name}} class.</p>
+ <p>{{ flavor.used_instances }}%, <strong>{{ flavor.used_instances }} instances</strong></p>'
+ data-single-bar-orientation="vertical"
+ data-single-bar-height="100%"
+ data-single-bar-width="30"
+ data-single-bar-used="{{ flavor.used_instances }}"
+ data-single-bar-average-used="{{ 50 }}"
+ data-single-bar-auto-scale-selector=".flavors_scale_selector"
+ data-single-bar-color-scale-range='["#000060", "#99FFFF"]'>
+ </div>
+ </td>
+ {% endfor %}
+ </tr>
+ <tr>
+ {% for flavor in rack.list_flavors %}
+ <td class="modal_chart flavor_usage_text"><a href="{{ "#" }}">{{ flavor.used_instances }}%</a></td>
+ {% endfor %}
+ </tr>
+ <tr>
+ {% for flavor in rack.list_flavors %}
+ <td class="flavor_usage_text">{{ flavor.used_instances }} inst.</td>
+ {% endfor %}
+ </tr>
+ </table>
+ {% else %}
+ <p>{% trans "No data available yet." %}</p>
+ {% endif %}
+ </div>
+
+ <div class="span6 alerts">
+ <h4>{% trans "Active Alerts" %}</h4>
+ <hr class="header_rule">
+ {% if rack.is_provisioned %}
+ <ul>
+ {% for alert in rack.alerts %}
+ <li><i class="icon-warning-sign"></i>{{ alert.message }}</li>
+ {% endfor %}
+ {% for node in rack.aggregated_alerts %}
+ <li>
+ <i class="icon-warning-sign"></i>
+ Node <a href="{% url 'horizon:infrastructure:resource_management:nodes:detail' node.id %}">{{ node.name }}</a> has some problems
+ </li>
+ {% endfor %}
+ </ul>
+ {% else %}
+ <p>{% trans "No data available yet." %}</p>
+ {% endif %}
+ </div>
+</div>
+
+<div class="row-fluid">
+ <div class="span6">
+ <!-- FIXME Will be added later
+ <div class="circles_chart_time_picker">
+ <select data-circles-chart-command="change_time"
+ data-receiver="#most_contacting_racks, #most_contacted_racks">
+ <option value="now">Now</option>
+ <option value="yesterday">Yesterday</option>
+ <option value="last_week">Last Week</option>
+ <option value="last_month">Last Month</option>
+ </select>
+ </div>
+ -->
+
+ <h4>Top Communicating Racks</h4>
+ <hr class="header_rule">
+ <div class="clear"></div>
+ {% if rack.nodes_count and rack.is_provisioned %}
+ <div class="communication_charts_wrapper">
+ <div class="communication_chart_wrapper">
+ <h5>The most contacting</h5>
+ <div id="most_contacting_racks"
+ class="communication_chart"
+ data-chart-type="circles_chart"
+ data-url="{% url 'horizon:infrastructure:resource_management:racks:top_communicating' rack.id %}?cond=from"
+ data-time="now"
+ data-size="22">
+ </div>
+ </div>
+ <div class="communication_chart_connection"></div>
+ <div class="communication_chart_wrapper">
+ <h5>The most contacted</h5>
+ <div id="most_contacted_racks"
+ class="communication_chart"
+ data-chart-type="circles_chart"
+ data-url="{% url 'horizon:infrastructure:resource_management:racks:top_communicating' rack.id %}?cond=to"
+ data-time="now"
+ data-size="22">
+ </div>
+ </div>
+ </div>
+ {% else %}
+ <p>No data available yet.</p>
+ {% endif %}
+ </div>
+ <div class="span6">
+ <!-- FIXME Will be added later
+ <div class="circles_chart_time_picker">
+ <select data-circles-chart-command="change_time"
+ data-receiver="#rack_health_chart">
+ <option value="now">Now</option>
+ <option value="yesterday">Yesterday</option>
+ <option value="last_week">Last Week</option>
+ <option value="last_month">Last Month</option>
+ </select>
+ </div>
+ -->
+
+ <h4>Node health</h4>
+ {% if rack.nodes_count and rack.is_provisioned %}
+
+ <ul class="nav nav-tabs"
+ data-circles-chart-command="change_url"
+ data-receiver="#rack_health_chart">
+ <li class="active">
+ <a data-url="{% url 'horizon:infrastructure:resource_management:racks:node_health' rack.id %}?type=overall_health" href="#">
+ Overall Health</a>
+ </li>
+ <li>
+ <a data-url="{% url 'horizon:infrastructure:resource_management:racks:node_health' rack.id %}?type=alerts" href="#">
+ Alerts</a>
+ </li>
+ <li>
+ <a data-url="{% url 'horizon:infrastructure:resource_management:racks:node_health' rack.id %}?type=capacities" href="#">
+ Capacities</a>
+ </li>
+ <li>
+ <a data-url="{% url 'horizon:infrastructure:resource_management:racks:node_health' rack.id %}?type=status" href="#">
+ Status</a>
+ </li>
+ </ul>
+ <div id ="rack_health_chart"
+ class="health_chart"
+ data-chart-type="circles_chart"
+ data-url="{% url 'horizon:infrastructure:resource_management:racks:node_health' rack.id %}"
+ data-time="now"
+ data-size="22">
+ </div>
+ {% else %}
+ <hr class="header_rule">
+ <div class="clear"></div>
+
+ <p>No data available yet.</p>
+ {% endif %}
+ </div>
+</div>
+
+
+<script type="text/javascript">
+/* polling of status. */
+horizon.detail_overview = {
+ update: function () {
+ var state_obj = $('.overall-state').first();
+ var state = state_obj.data('state');
+ var interval = state_obj.data('interval');
+ var url = state_obj.data('url');
+ if (state == 'provisioning') {
+ // Wait and try to update again in next interval instead
+ setTimeout(horizon.detail_overview.update, interval);
+
+ this.jqxhr = $.getJSON( url, function() {
+ state_obj.html('<p>Provisioning</p><img src="/static/dashboard/img/horizontal_loader.gif" />')
+ })
+ .done(function(data) {
+ // FIXME find a way how to only update graph with new data
+ // not delete and create
+ if (data['state'] != 'provisioning'){
+ window.location.reload();
+ }
+ })
+ .fail(function() {
+ // FIXME add proper fail message
+ console.log( "error" );
+ })
+ .always(function() {
+ // FIXME add behaviour that should be always done
+ });
+ }
+ }
+};
+
+
+horizon.addInitFunction(function () {
+ horizon.detail_overview.update();
+});
+
+</script>
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/_edit.html b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/_edit.html
new file mode 100644
index 00000000..87ccb864
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/_edit.html
@@ -0,0 +1,22 @@
+{% extends "horizon/common/_modal_form.html" %}
+{% load i18n %}
+{% load url from future %}
+
+{% block form_id %}edit_rack_form{% endblock %}
+{% block form_action %}{% url 'horizon:infrastructure:resource_management:racks:edit' rack_id %}{% endblock %}
+
+{% block modal_id %}edit_rack_modal{% endblock %}
+{% block modal-header %}{% trans "Edit Rack" %}{% endblock %}
+
+{% block modal-body %}
+<div>
+ <fieldset>
+ {% include "horizon/common/_form_fields.html" %}
+ </fieldset>
+</div>
+{% endblock %}
+
+{% block modal-footer %}
+ <input class="btn btn-primary pull-right" type="submit" value="{% trans "Save" %}" />
+ <a href="{% url 'horizon:infrastructure:resource_management:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/_edit_status.html b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/_edit_status.html
new file mode 100644
index 00000000..f2a81170
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/_edit_status.html
@@ -0,0 +1,23 @@
+{% extends "horizon/common/_modal_form.html" %}
+{% load i18n %}
+{% load url from future %}
+
+{% block form_id %}edit_rack_status_form{% endblock %}
+{% block form_action %}{% url 'horizon:infrastructure:resource_management:racks:edit_status' rack_id %}?action={{ action }}{% endblock %}
+
+{% block modal_id %}edit_rack_status_modal{% endblock %}
+{% block modal-header %}{% trans "Rack Action Confirmation" %}{% endblock %}
+
+{% block modal-body %}
+<div>
+ {% trans "Are you sure?" %}
+ <fieldset>
+ {% include "horizon/common/_form_fields.html" %}
+ </fieldset>
+</div>
+{% endblock %}
+
+{% block modal-footer %}
+ <input class="btn btn-primary" type="submit" value="{% trans "Yes" %}" />
+ <a href="{% url 'horizon:infrastructure:resource_management:racks:detail' rack_id %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/_index_table.html b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/_index_table.html
new file mode 100644
index 00000000..391afe98
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/_index_table.html
@@ -0,0 +1,3 @@
+{% load i18n %}
+{{ racks_table.render }}
+<a href="{% url 'horizon:infrastructure:resource_management:nodes:unracked' %}">{% trans "View Unracked Nodes"%} ({{nodes|length}})</a>
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/_upload.html b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/_upload.html
new file mode 100644
index 00000000..44097d3d
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/_upload.html
@@ -0,0 +1,37 @@
+{% extends "horizon/common/_modal_form.html" %}
+{% load i18n %}
+{% load url from future %}
+
+{% block form_id %}upload_rack_form{% endblock %}
+{% block form_action %}{% url 'horizon:infrastructure:resource_management:racks:upload' %}{% endblock %}
+{% block form_attrs %}enctype="multipart/form-data"{% endblock %}
+
+{% block modal_id %}upload_rack_modal{% endblock %}
+{% block modal-header %}{% trans "Upload Rack" %}{% endblock %}
+
+{% block modal-body %}
+ <div>
+ <fieldset>
+ <span class="control-group clearfix error">
+ {% if form.csv_file.errors %}
+ <div class="error help-inline">{{ form.csv_file.errors }}</div>
+ {% endif %}
+ </span>
+ {{ form.csv_file }}
+ <input class="btn btn-primary pull-right always-enabled" type="submit" name=upload value="{% trans "Upload File" %}" />
+ <div class="help-block">
+ CSV file format:<br>rack name,resource class name,subnet,region,list of mac addresses separated by space
+ </div>
+ {{ form.uploaded_data }}
+ </fieldset>
+ </div>
+ <div class="csv_rack_table">
+ {% trans "Uploaded racks, which you are going to add to the system:" %}
+ {{ racks_table.render }}
+ </div>
+{% endblock %}
+
+{% block modal-footer %}
+<input class="btn btn-primary pull-right always-enabled" type="submit" name=add_racks value="{% trans "Add Racks" %}" {% if not form.uploaded_data.value %}disabled{% endif %}/>
+ <a href="{% url 'horizon:infrastructure:resource_management:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/create.html b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/create.html
new file mode 100644
index 00000000..9ad42b2d
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/create.html
@@ -0,0 +1,11 @@
+{% extends 'infrastructure/base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Create Rack" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Create Rack") %}
+{% endblock page_header %}
+
+{% block infrastructure_main %}
+ {% include "infrastructure/resource_management/racks/_create.html" %}
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/detail.html b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/detail.html
new file mode 100644
index 00000000..4b377d73
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/detail.html
@@ -0,0 +1,59 @@
+{% extends 'infrastructure/base_detail.html' %}
+{% load i18n %}
+{% block title %}{% trans "Rack Detail"%}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Rack Detail") %}
+{% endblock page_header %}
+
+{% block breadcrumbs %}
+ <div class="breadcrumbs">
+ <a href="{% url 'horizon:infrastructure:resource_management:index' %}?tab=resource_management_tabs__resource_classes_tab" >Home</a>
+ <span class="separator"></span>
+ <a href="{% url 'horizon:infrastructure:resource_management:index' %}?tab=resource_management_tabs__racks_tab" >Racks</a>
+ <span class="separator"></span>
+ </div>
+{% endblock breadcrumbs %}
+
+{% block name %}{{ rack.name }}{% endblock %}
+
+{% block actions %}
+ <div class="btn-group">
+ {% if rack.is_provisioned %}
+ <a class="btn ajax-modal" href="{% url 'horizon:infrastructure:resource_management:racks:edit_status' rack.id %}?action=unprovision">{% trans "Unprovision Rack" %}</a>
+ {% endif %}
+ </div>
+ {# <div class="btn-group"> #}
+ {# <a class="btn ajax-modal" href="{% url 'horizon:infrastructure:resource_management:racks:edit_status' rack.id %}?action=start">{% trans "Start" %}</a> #}
+ {# <a class="btn ajax-modal" href="{% url 'horizon:infrastructure:resource_management:racks:edit_status' rack.id %}?action=reboot">{% trans "Reboot" %}</a> #}
+ {# <a class="btn ajax-modal" href="{% url 'horizon:infrastructure:resource_management:racks:edit_status' rack.id %}?action=shutdown">{% trans "Shutdown" %}</a> #}
+ {# </div> #}
+ <div class="btn-group">
+ <a class="btn ajax-modal" href="{% url 'horizon:infrastructure:resource_management:racks:detail_edit' rack.id %}">{% trans "Edit" %}</a>
+ </div>
+{% endblock %}
+
+{% block overall_usage %}
+ <table class="capacities overall_usage">
+ <tr>
+ <td class="capacity_label">{% trans "Usage" %}:</td>
+ <td>
+ {% if rack.is_provisioned %}
+ <div id="rack_usage"
+ class="capacity_bar"
+ data-chart-type="capacity_bar_chart"
+ data-capacity-limit="{{ rack.vm_capacity.value }}"
+ data-capacity-used="{{ rack.vm_capacity.usage }}"
+ data-average-capacity-used="{{ rack.vm_capacity.average }}">
+ </div>
+ {% else %}
+ <div id="rack_usage"
+ class="capacity_bar"
+ data-chart-type="capacity_bar_chart">
+ </div>
+ {% endif %}
+ </td>
+ </tr>
+ </table>
+{% endblock %}
+
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/edit.html b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/edit.html
new file mode 100644
index 00000000..76a8e439
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/edit.html
@@ -0,0 +1,11 @@
+{% extends 'infrastructure/base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Edit Rack" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Edit Rack") %}
+{% endblock page_header %}
+
+{% block infrastructure_main %}
+ {% include "infrastructure/resource_management/racks/_edit.html" %}
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/edit_status.html b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/edit_status.html
new file mode 100644
index 00000000..99789380
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/edit_status.html
@@ -0,0 +1,11 @@
+{% extends 'infrastructure/base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Rack Action Confirmation" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Edit Rack Status") %}
+{% endblock page_header %}
+
+{% block infrastructure_main %}
+ {% include "infrastructure/resource_management/racks/_edit_status.html" %}
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/upload.html b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/upload.html
new file mode 100644
index 00000000..1df9454d
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/racks/upload.html
@@ -0,0 +1,11 @@
+{% extends 'infrastructure/base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Upload Rack" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Upload Rack") %}
+{% endblock page_header %}
+
+{% block main %}
+ {% include "infrastructure/resource_management/racks/_upload.html" %}
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/_action.html b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/_action.html
new file mode 100644
index 00000000..5f7108ea
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/_action.html
@@ -0,0 +1,22 @@
+{% extends "horizon/common/_modal_form.html" %}
+{% load i18n %}
+{% load url from future %}
+{% block form_id %}resource_class_action_form{% endblock %}
+{% block form_action %}
+ {% url 'horizon:infrastructure:resource_management:resource_classes:detail_action' resource_class_id %}?action={{ action }}
+{% endblock %}
+{% block modal_id %}resource_class_action_modal{% endblock %}
+{% block modal-header %}{{header}}{% endblock %}
+{% block modal-body %}
+<div>
+ {% trans "Are you sure?" %}
+ <fieldset>
+ {% include "horizon/common/_form_fields.html" %}
+ </fieldset>
+</div>
+{% endblock %}
+{% block modal-footer %}
+ <input class="btn btn-primary" type="submit" value="{% trans "Yes" %}" />
+ <a href="{% url 'horizon:infrastructure:resource_management:resource_classes:detail' resource_class_id %}"
+ class="btn secondary cancel close">{% trans "Cancel" %}</a>
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/_detail_flavors.html b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/_detail_flavors.html
new file mode 100644
index 00000000..87aabcfa
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/_detail_flavors.html
@@ -0,0 +1 @@
+{{ flavors_table.render }}
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/_detail_overview.html b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/_detail_overview.html
new file mode 100644
index 00000000..8916e47e
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/_detail_overview.html
@@ -0,0 +1,239 @@
+{% load i18n sizeformat %}
+{% load url from future %}
+{% load chart_helpers %}
+
+<div class="status row-fluid detail">
+ <div class="span4">
+ <h4>{% trans "About" %}</h4>
+ <hr class="header_rule">
+ <dl>
+ <dt>{% trans "Racks" %}</dt>
+ <dd><a href="?tab=resource_class_details__racks" >{{ resource_class.racks|length }} {% trans "racks" %}</a></dd>
+ <dt>{% trans "Nodes" %}</dt>
+ <dd>{{ resource_class.nodes|length }} {% trans "nodes" %}</dd>
+ </dl>
+ </div>
+
+ <div class="span4">
+ <h4>{% trans "Provided Service" %}</h4>
+ <hr class="header_rule">
+ <dl>
+ <dt>{% trans "Type" %}</dt>
+ <dd>{{ resource_class.service_type }}</dd>
+ <dt>{% trans "Flavors" %}</dt>
+ <dd><a href="?tab=resource_class_details__flavors" >{{ resource_class.list_flavors|length }} {% trans "flavors" %}</a></dd>
+ <dd></dd>
+ <dt>{% trans "Active Instances" %}</dt>
+ <dd>{{ resource_class.running_virtual_machines|length }} {% trans "instances" %}</dd>
+ </dl>
+ </dl>
+ </div>
+
+ <div class="span4">
+ <h4>{% trans "Capacities" %}</h4>
+ <hr class="header_rule">
+ {% if resource_class.capacities %}
+ <table class="capacities">
+ {% for capacity in resource_class.capacities %}
+ <tr>
+ {% if resource_class.has_provisioned_rack %}
+ <td class="capacity_label">{{ capacity.name }}:</td>
+ <td>
+ <div id="{{ capacity.name }}_capacity_usage"
+ class="capacity_bar"
+ data-chart-type="capacity_bar_chart"
+ data-capacity-limit="{{ capacity.value }}"
+ data-capacity-used="{{ capacity.usage }}"
+ data-average-capacity-used="{{ capacity.average }}">
+ </div>
+ </td>
+ <td>
+ <a href="#" data-chart-type="modal_line_chart" data-url="/infrastructure/resource_management/racks/usage_data" data-series="{{ capacity.name }}">{{ capacity.usage|default:_(" - ") }}/{{ capacity.value|default:_(" - ") }} {{ capacity.unit }}</a>
+ </td>
+ {% else %}
+ <td>
+ <div id="{{ capacity.name }}_capacity_usage"
+ class="capacity_bar"
+ data-chart-type="capacity_bar_chart">
+ </div>
+ </td>
+ <td></td>
+ {% endif %}
+ </tr>
+ {% endfor %}
+ </table>
+ {% else %}
+ <p>No data available yet.</p>
+ {% endif %}
+ </div>
+</div>
+
+<div class="status row-fluid detail">
+ <div class="span6">
+ <h4>{% trans "Capacity Usage" %}</h4>
+ <hr class="header_rule">
+ {% if resource_class.has_provisioned_rack %}
+ <div data-chart-type="line_chart" data-url="/infrastructure/resource_management/racks/usage_data" data-series="cpu,ram,storage,network"></div>
+ {% else %}
+ <p>{% trans "No data available yet." %}</p>
+ {% endif %}
+ </div>
+
+ <div class="span6 alerts">
+ <h4>{% trans "Active Alerts" %}</h4>
+ <hr class="header_rule">
+ {% if resource_class.has_provisioned_rack %}
+ <ul>
+ {% for rack in resource_class.aggregated_alerts %}
+ <li>
+ <i class="icon-warning-sign"></i>
+ Rack <a href="{% url 'horizon:infrastructure:resource_management:racks:detail' rack.id %}">{{ rack.name }}</a> has some problems
+ </li>
+ {% endfor %}
+ </ul>
+ {% else %}
+ <p>{% trans "No data available yet." %}</p>
+ {% endif %}
+ </div>
+</div>
+
+<div class="row-fluid detail">
+ <div class="span6">
+ <h4>{% trans "Summary of Instances and Usage" %}</h4>
+ <hr class="header_rule">
+ {% if resource_class.has_provisioned_rack %}
+ <!--
+ <dl>
+ {% for flavor in resource_class.running_virtual_machines %}
+ <dt>{{ flavor.name }}</dt>
+ <dd>{{ flavor.max_vms|default:_(" - ") }}</dd>
+ {% endfor %}
+ </dl>
+ -->
+
+ <div class="clear"></div>
+ <div>
+ <strong>{{ resource_class.total_instances }}</strong> instances
+ <strong>{{resource_class.remaining_capacity}}%</strong> capacity remaining
+ </div>
+ <div class="flavor_usage_bar"
+ data-popup-free='{{resource_class|remaining_capacity_by_flavors}}'
+ data-single-bar-orientation="horizontal"
+ data-single-bar-height="35"
+ data-single-bar-width="100%"
+ data-single-bar-used="{{ resource_class|all_used_instances }}"
+ data-single-bar-auto-scale-selector=".flavors_scale_selector"
+ data-single-bar-color-scale-range='["#000060", "#99FFFF"]'
+ >
+ </div>
+
+ <table class="flavor_usages">
+ <tr>
+ {% for flavor in resource_class.list_flavors %}
+ <td class="flavor_usage_label">
+ <a href="{% url 'horizon:infrastructure:resource_management:flavors:detail' flavor.id %}">{{ flavor.name }}</a>
+ </td>
+ {% endfor %}
+ </tr>
+ <tr>
+ {% for flavor in resource_class.list_flavors %}
+ <td>
+ <div
+ class="flavor_usage_bar flavors_scale_selector"
+ data-popup-average='
+ <p>Average capacity consumed by instances of {{flavor.name}} flavor in {{resource_class.name}} class.</p>
+ <p>{{ flavor.used_instances }}%, <strong>{{ flavor.used_instances }} instances</strong></p>
+ '
+ data-single-bar-orientation="vertical"
+ data-single-bar-height="100%"
+ data-single-bar-width="30"
+ data-single-bar-used="{{ flavor.used_instances }}"
+ data-single-bar-average-used="{{ 50 }}"
+ data-single-bar-auto-scale-selector=".flavors_scale_selector"
+ data-single-bar-color-scale-range='["#000060", "#99FFFF"]'
+ >
+ </div>
+ </td>
+ {% endfor %}
+ </tr>
+ <tr>
+ {% for flavor in resource_class.list_flavors %}
+ <td class="modal_chart flavor_usage_text"><a href="{{ "#" }}">{{ flavor.used_instances }}%</a></td>
+ {% endfor %}
+ </tr>
+ <tr>
+ {% for flavor in resource_class.list_flavors %}
+ <td class="flavor_usage_text">{{ flavor.used_instances }} inst.</td>
+ {% endfor %}
+ </tr>
+ </table>
+ {% else %}
+ <p>{% trans "No data available yet." %}</p>
+ {% endif %}
+ </div>
+
+ <div class="span6">
+ <!-- FIXME Will be added later
+ <div class="circles_chart_time_picker">
+ <select data-circles-chart-command="change_time"
+ data-receiver=".rack_health_chart">
+ <option value="now">Now</option>
+ <option value="yesterday">Yesterday</option>
+ <option value="last_week">Last Week</option>
+ <option value="last_month">Last Month</option>
+ </select>
+ </div>
+ -->
+
+ <h4>Rack health</h4>
+
+ {% if resource_class.has_provisioned_rack %}
+ <ul class="nav nav-tabs"
+ data-circles-chart-command="change_url"
+ data-receiver=".rack_health_chart">
+ <li class="active">
+ <a data-url="{% url 'horizon:infrastructure:resource_management:resource_classes:rack_health' resource_class.id %}?type=overall_health" href="#">
+ Overall Health</a>
+ </li>
+ <li>
+ <a data-url="{% url 'horizon:infrastructure:resource_management:resource_classes:rack_health' resource_class.id %}?type=alerts" href="#">
+ Alerts</a>
+ </li>
+ <li>
+ <a data-url="{% url 'horizon:infrastructure:resource_management:resource_classes:rack_health' resource_class.id %}?type=capacities" href="#">
+ Capacities</a>
+ </li>
+ <li>
+ <a data-url="{% url 'horizon:infrastructure:resource_management:resource_classes:rack_health' resource_class.id %}?type=status" href="#">
+ Status</a>
+ </li>
+ </ul>
+ <!--<h5>Region 1<h5>
+ <div class="rack_health_chart"
+ data-chart-type="circles_chart"
+ data-url="/infrastructure/racks/1/top_communicating.json?region=1"
+ data-region=1 //// implement region parameter
+ data-time="now"
+ data-size="10">
+ </div>
+ <h5>Region 2<h5>
+ <div class="rack_health_chart"
+ data-chart-type="circles_chart"
+ data-url="/infrastructure/racks/1/top_communicating.json?region=2"
+ data-region=1 //// implement region parameter
+ data-time="now"
+ data-size="10">
+ </div>
+ <h5>Region 3<h5>-->
+ <div class="rack_health_chart"
+ data-chart-type="circles_chart"
+ data-url="{% url 'horizon:infrastructure:resource_management:resource_classes:rack_health' resource_class.id %}"
+ data-time="now"
+ data-size="22">
+ </div>
+ {% else %}
+ <hr class="header_rule">
+ <p>{% trans "No data available yet." %}</p>
+ {% endif %}
+ </div>
+</div>
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/_detail_racks.html b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/_detail_racks.html
new file mode 100644
index 00000000..3d8331ac
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/_detail_racks.html
@@ -0,0 +1 @@
+{{ racks_table.render }}
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/_racks_step.html b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/_racks_step.html
new file mode 100644
index 00000000..2c6d885b
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/_racks_step.html
@@ -0,0 +1,3 @@
+<noscript><h3>{{ step }}</h3></noscript>
+
+{{ racks_table.render }}
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/_resource_class_info_and_flavors_step.html b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/_resource_class_info_and_flavors_step.html
new file mode 100644
index 00000000..31463083
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/_resource_class_info_and_flavors_step.html
@@ -0,0 +1,56 @@
+<noscript><h3>{{ step }}</h3></noscript>
+<table class="table-fixed">
+ <tbody>
+ <tr>
+ <td class="actions">
+ {% include "horizon/common/_form_fields.html" %}
+ </td>
+ <td class="help_text">
+ {{ step.get_help_text }}
+ </td>
+ </tr>
+ </tbody>
+</table>
+
+<div id="id_resource_class_flavors_table">
+ {{ flavors_table.render }}
+</div>
+
+
+<script type="text/javascript">
+ // show the flavors table only when service_type is compute
+ var toggle_table = function(value){
+ if (value == 'compute'){
+ $('#id_resource_class_flavors_table').show();
+ }
+ else{
+ $('#id_resource_class_flavors_table').hide();
+ }
+ };
+ toggle_table($('#id_service_type').val())
+
+ $('#id_service_type').change(function(){
+ toggle_table($('#id_service_type').val())
+ });
+
+ var toggle_max_vms = function(check_box){
+ var checked = check_box.prop('checked');
+ var id = check_box.val();
+
+ if (checked){
+ $('input[name=flavors_object_ids__max_vms__' + id + "]").show();
+ }
+ else{
+ $('input[name=flavors_object_ids__max_vms__' + id + "]").hide();
+ }
+ };
+
+ $(".modal #flavors input[type=checkbox]").each(function() {
+ toggle_max_vms($(this));
+ });
+ $(".modal #flavors input[type=checkbox]").bind('click', function() {
+ toggle_max_vms($(this));
+ });
+
+
+</script>
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/action.html b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/action.html
new file mode 100644
index 00000000..c2b03cf1
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/action.html
@@ -0,0 +1,9 @@
+{% extends 'infrastructure/base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Resource Class Action Confirmation" %}{% endblock %}
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Resource Class Action") %}
+{% endblock page_header %}
+{% block infrastructure_main %}
+ {% include "infrastructure/resource_management/resource_classes/_edit_status.html" %}
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/detail.html b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/detail.html
new file mode 100644
index 00000000..0c6ecaef
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/detail.html
@@ -0,0 +1,62 @@
+{% extends 'infrastructure/base_detail.html' %}
+{% load i18n sizeformat %}
+{% block title %}{% trans "Resource Class Detail" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title="Resource Class Detail" %}
+{% endblock page_header %}
+
+{% block breadcrumbs %}
+ <div class="breadcrumbs">
+ <a href="{% url 'horizon:infrastructure:resource_management:index' %}?tab=resource_management_tabs__resource_classes_tab" >Home</a>
+ <span class="separator"></span>
+ <a href="{% url 'horizon:infrastructure:resource_management:index' %}?tab=resource_management_tabs__resource_classes_tab" >Classes</a>
+ <span class="separator"></span>
+ </div>
+{% endblock breadcrumbs %}
+
+{% block name %}{{ resource_class.name }}{% endblock %}
+
+{% block actions %}
+ <div class="btn-group">
+ <a class="btn ajax-modal"
+ href="{% url 'horizon:infrastructure:resource_management:resource_classes:detail_update' resource_class.id %}">
+ {% trans "Edit" %}
+ </a>
+ <a {% if resource_class.deletable %}
+ class="btn ajax-modal"
+ href="{% url 'horizon:infrastructure:resource_management:resource_classes:detail_action' resource_class.id %}?action=delete"
+ {% else %}
+ title="{% trans "Resource Class contains racks and can't be deleted." %}"
+ class="btn disabled"
+ href="#"
+ {% endif %}
+ >
+ {% trans "Delete" %}
+ </a>
+ </div>
+{% endblock %}
+
+
+{% block overall_usage %}
+ <table class="capacities overall_usage">
+ <tr>
+ <td class="capacity_label">{% trans "Usage" %}:</td>
+ <td>
+ {% if resource_class.has_provisioned_rack %}
+ <div id="resource_class_usage"
+ class="capacity_bar"
+ data-chart-type="capacity_bar_chart"
+ data-capacity-limit="{{ resource_class.vm_capacity.value }}"
+ data-capacity-used="{{ resource_class.vm_capacity.usage }}"
+ </div>
+ {% else %}
+ <div id="resource_class_usage"
+ class="capacity_bar"
+ data-chart-type="capacity_bar_chart">
+ </div>
+ {% endif %}
+ </td>
+ </tr>
+ </table>
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/tests.py b/openstack_dashboard/dashboards/infrastructure/resource_management/tests.py
new file mode 100644
index 00000000..2535a5b4
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/tests.py
@@ -0,0 +1,84 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from django import http
+from django.core.urlresolvers import reverse
+
+from mox import IsA
+
+from openstack_dashboard import api
+from openstack_dashboard.test import helpers as test
+
+
+class ResourceManagementTests(test.BaseAdminViewTests):
+ def setUp(self):
+ super(ResourceManagementTests, self).setUp()
+
+ @test.create_stubs({
+ api.tuskar.ResourceClass: (
+ 'list',
+ 'list_racks',
+ 'nodes'),
+ api.tuskar.FlavorTemplate: (
+ 'list',),
+ api.tuskar.Rack: (
+ 'list',)})
+ def test_index(self):
+ # FlavorTemplate stubs
+ flavors = self.tuskar_flavors.list()
+
+ api.tuskar.FlavorTemplate.list(IsA(http.HttpRequest)).AndReturn(
+ flavors)
+ # FlavorTemplate stubs end
+
+ # ResourceClass stubs
+ all_resource_classes = self.tuskar_resource_classes.list()
+ nodes = []
+ racks = []
+
+ api.tuskar.ResourceClass.nodes = nodes
+ api.tuskar.ResourceClass.list_racks = racks
+
+ api.tuskar.ResourceClass.list(
+ IsA(http.HttpRequest)).\
+ AndReturn(all_resource_classes)
+ # ResourceClass stubs end
+
+ # Rack stubs
+ racks = self.tuskar_racks.list()
+
+ api.tuskar.Rack.list(IsA(http.HttpRequest)).AndReturn(racks)
+ # Rack stubs end
+
+ self.mox.ReplayAll()
+
+ url = reverse('horizon:infrastructure:resource_management:index')
+ res = self.client.get(url)
+ self.assertTemplateUsed(
+ res, 'infrastructure/resource_management/index.html')
+
+ # FlavorTemplate asserts
+ self.assertItemsEqual(res.context['flavors_table'].data, flavors)
+ # FlavorTemplate asserts end
+
+ # ResourceClass asserts
+ self.assertItemsEqual(res.context['resource_classes_table'].data,
+ all_resource_classes)
+ # ResourceClass asserts end
+
+ # Rack asserts
+ self.assertItemsEqual(res.context['racks_table'].data, racks)
+ # Rack asserts end
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/urls.py b/openstack_dashboard/dashboards/infrastructure/resource_management/urls.py
new file mode 100644
index 00000000..89f462b8
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/urls.py
@@ -0,0 +1,32 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from django.conf.urls.defaults import patterns, url, include
+
+from .flavors import urls as flavor_urls
+from .resource_classes import urls as resource_classes_urls
+from .racks import urls as rack_urls
+from .nodes import urls as node_urls
+from .views import IndexView
+
+urlpatterns = patterns('',
+ url(r'^$', IndexView.as_view(), name='index'),
+ url(r'flavors/', include(flavor_urls, namespace='flavors')),
+ url(r'racks/', include(rack_urls, namespace='racks')),
+ url(r'resource_classes/',
+ include(resource_classes_urls, namespace='resource_classes')),
+ url(r'nodes/', include(node_urls, namespace='nodes')),
+)
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/views.py b/openstack_dashboard/dashboards/infrastructure/resource_management/views.py
new file mode 100644
index 00000000..c77b5517
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/views.py
@@ -0,0 +1,29 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Red Hat, 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.
+
+"""
+Views for Resource Management.
+"""
+
+
+from horizon import tabs
+
+from .tabs import ResourceManagementTabs
+
+
+class IndexView(tabs.TabbedTableView):
+ tab_group_class = ResourceManagementTabs
+ template_name = 'infrastructure/resource_management/index.html'
diff --git a/openstack_dashboard/dashboards/infrastructure/service_management/__init__.py b/openstack_dashboard/dashboards/infrastructure/service_management/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/service_management/__init__.py
diff --git a/openstack_dashboard/dashboards/infrastructure/service_management/panel.py b/openstack_dashboard/dashboards/infrastructure/service_management/panel.py
new file mode 100644
index 00000000..e9cc5411
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/service_management/panel.py
@@ -0,0 +1,29 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from django.utils.translation import ugettext_lazy as _
+
+import horizon
+
+from openstack_dashboard.dashboards.infrastructure import dashboard
+
+
+class Service_Management(horizon.Panel):
+ name = _("Service Management")
+ slug = "service_management"
+
+
+#dashboard.Infrastructure.register(Service_Management)
diff --git a/openstack_dashboard/dashboards/infrastructure/service_management/templates/service_management/index.html b/openstack_dashboard/dashboards/infrastructure/service_management/templates/service_management/index.html
new file mode 100644
index 00000000..7de2a028
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/service_management/templates/service_management/index.html
@@ -0,0 +1,12 @@
+{% extends 'infrastructure/base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Service Management" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Service Management") %}
+{% endblock page_header %}
+
+{% block infrastructure_main %}
+{% endblock %}
+
+
diff --git a/openstack_dashboard/dashboards/infrastructure/service_management/tests.py b/openstack_dashboard/dashboards/infrastructure/service_management/tests.py
new file mode 100644
index 00000000..1aee8f31
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/service_management/tests.py
@@ -0,0 +1,23 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from openstack_dashboard.test import helpers as test
+
+
+class Service_ManagementTests(test.TestCase):
+ # Unit tests for service_management.
+ def test_me(self):
+ self.assertTrue(1 + 1 == 2)
diff --git a/openstack_dashboard/dashboards/infrastructure/service_management/urls.py b/openstack_dashboard/dashboards/infrastructure/service_management/urls.py
new file mode 100644
index 00000000..d6e4d2d5
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/service_management/urls.py
@@ -0,0 +1,24 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from django.conf.urls.defaults import patterns, url
+
+from .views import IndexView
+
+
+urlpatterns = patterns('',
+ url(r'^$', IndexView.as_view(), name='index'),
+)
diff --git a/openstack_dashboard/dashboards/infrastructure/service_management/views.py b/openstack_dashboard/dashboards/infrastructure/service_management/views.py
new file mode 100644
index 00000000..758dc527
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/service_management/views.py
@@ -0,0 +1,26 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from horizon import views
+
+
+class IndexView(views.APIView):
+ # A very simple class-based view...
+ template_name = 'infrastructure/service_management/index.html'
+
+ def get_data(self, request, context, *args, **kwargs):
+ # Add data to the context here...
+ return context
diff --git a/openstack_dashboard/dashboards/infrastructure/static/infrastructure/less/infrastructure.less b/openstack_dashboard/dashboards/infrastructure/static/infrastructure/less/infrastructure.less
new file mode 100644
index 00000000..0e5257a1
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/static/infrastructure/less/infrastructure.less
@@ -0,0 +1,497 @@
+/* Additional CSS for infrastructure. */
+
+// global layout
+html,
+body {
+ width: 100%;
+ height: 100%;
+}
+
+// sidebar
+.sidebar {
+ font-size: 95%;
+ position: fixed;
+ top: 0;
+ left: 0;
+ height: 100%;
+ width: 240px;
+ border-right: 1px solid rgb(210,210,210);
+ background: rgb(235,235,235);
+
+ h1.brand {
+ text-align: center;
+
+ a {
+ display: inline-block;
+ width: 80px;
+ height: 85px;
+ background-size: 80px 85px;
+ float: none;
+ margin: 20px 0;
+ }
+ }
+
+ .nav-tabs {
+ margin-top: -30px;
+ border-bottom: 1px solid rgb(200,200,200);
+
+ a {
+ position: relative;
+ top: 1px;
+ padding: 7px 10px;
+ }
+
+ li.active a,
+ li a:hover {
+ background: rgb(235,235,235);
+ border: 1px solid rgb(200,200,200);
+ border-bottom: 0 none;
+ }
+ }
+
+ .main_nav {
+ text-align: right;
+ margin-right: 0;
+ width: 100%;
+
+ li {
+ display: inline-block;
+ text-align: left;
+ }
+
+ a {
+ margin: 0;
+ position: relative;
+ right: -1px;
+ border: 1px solid rgb(200,200,200);
+ border-right: 0 none;
+ border-radius: 4px 0 0 4px;
+ }
+ }
+}
+
+// right part
+#main_content {
+ padding: 15px 0 25px 0;
+ margin-left: 240px;
+
+ // topbar
+ .topbar {
+ padding: 0 30px 0 30px;
+ margin: 0;
+ background: white;
+ color: rgb(180, 180, 180);
+ border: 0 none;
+
+ .page-header {
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ }
+
+ h2 {
+ font-weight: lighter;
+ font-size: 19px;
+ color: rgb(180, 180, 180);
+ line-height: 24px;
+ }
+
+ #user_info {
+ position: relative;
+ color: inherit;
+
+ a {
+ color: rgb(130, 190, 245);
+ }
+ }
+ }
+
+ .row-fluid {
+ margin: 15px 0 40px;
+ }
+
+ // content area
+ & > .row-fluid {
+ padding: 0 30px;
+ margin: 0;
+ width: auto;
+ }
+}
+
+// navigations
+#main_content {
+ .nav-tabs {
+ margin: 0 0 20px;
+ border: 0 none;
+ border-top: 1px solid rgb(230,230,230);
+
+ li a {
+ position: relative;
+ top: -1px;
+ border-radius: 0 0 2px 2px;
+ text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75);
+ border-top-width: 0;
+ }
+
+ li.active a {
+ border: 1px solid rgb(220, 220, 220);
+ background: rgb(110, 110, 110);
+ color: white;
+ text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
+ }
+
+ li:not(.active) a:hover {
+ border: 1px solid rgb(240, 240, 240);
+ border-top-width: 0;
+ background: rgb(244, 244, 244);
+ }
+ }
+
+ .tab-content {
+ padding: 0;
+ border: 0 none;
+ }
+}
+
+// index pages layout
+#main_content {
+ #resource_management_tabs {
+ font-size: 130%;
+ border: 0 none;
+ border-bottom: 1px solid rgb(200, 200, 200);
+ font-weight: lighter;
+
+ a {
+ padding: 13px 18px;
+ border-top-width: 1px;
+ border-bottom-width: 0;
+ border-radius: 2px 2px 0 0;
+ }
+
+ li.active a,
+ li:not(.active) a:hover {
+ }
+ }
+}
+
+// tables
+table.table {
+ th.table_header {
+ padding: 10px 0;
+ background: none;
+ }
+
+ th {
+ background: linear-gradient(rgb(244,244,244), rgb(235,235,235));
+ font-size: 85%;
+ text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75);
+ }
+
+ tfoot {
+ color: rgb(180,180,180);
+ text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75);
+
+ tr td {
+ background: linear-gradient(rgb(244,244,244), rgb(235,235,235));
+ padding: 7px 12px;
+ height: auto;
+ }
+ }
+
+ .table_actions {
+ width: 100%;
+ text-align: right;
+ }
+
+ .table_title {
+ display: none;
+ }
+
+ .table_search {
+ float: left;
+ }
+
+ tr td {
+ padding: 0px 10px;
+ line-height: 1;
+ height: 40px;
+ vertical-align: middle;
+
+ &.anchor {
+ padding: 0;
+
+ a {
+ padding: 4px 10px;
+ }
+ }
+ }
+
+ .table_search input {
+ padding-left: 10px;
+ }
+
+ .table-bordered tr.table_caption + tr th:first-child {
+ border-top-left-radius: 2px;
+ }
+
+ .table-bordered tr.table_caption + tr th:last-child {
+ border-top-right-radius: 2px;
+ }
+
+ .table-bordered tfoot tr td:first-child {
+ border-bottom-left-radius: 2px;
+ }
+
+ .table-bordered tfoot tr td:last-child {
+ border-bottom-right-radius: 2px;
+ }
+}
+
+// detail pages
+#main_content {
+ h3 {
+ font-weight: lighter;
+ font-size: 26px;
+ color: rgb(80,80,80);
+ margin-bottom: 3px;
+ margin: 0 0 3px 0;
+ }
+
+ h4 {
+ font-weight: lighter;
+ font-size: 18px;
+ color: rgb(160,160,160);
+ margin-bottom: 3px;
+ }
+
+ dl {
+ dt {
+ width: 130px;
+ float: left;
+ font-weight: normal;
+ color: rgb(160,160,160);
+ line-height: 1.7;
+
+ &:after {
+ content: ":";
+ display: inline;
+ }
+ }
+
+ dd {
+ margin-left: 130px;
+ line-height: 1.7;
+ }
+ }
+}
+
+
+// capacities
+table.capacities {
+ &.overall_usage {
+ margin-top: 5px;
+ }
+
+ td {
+ padding: 3px;
+
+ &.capacity_label {
+ width: 60px;
+ padding-right: 5px;
+ color: rgb(160,160,160);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ & div.capacity_bar {
+ line-height: 0;
+ width: 120px;
+ }
+ }
+}
+
+// flavor usages
+div.flavor_usage_bar {
+ max-width: 550px;
+}
+
+table.flavor_usages {
+ width: 100%;
+ max-width: 550px;
+
+ td {
+ text-align: center;
+ padding: 3px;
+
+ &.flavor_usage_label {
+ width: 60px;
+ text-align: center;
+ }
+
+ &.flavor_usage_text {
+ width: 60px;
+ text-align: center;
+ line-height: 1;
+ }
+
+ & div.flavor_usage_bar {
+ width: auto;
+ text-align: center;
+ line-height: 0;
+ height: 120px;
+ }
+ }
+}
+
+#interval_selector {
+ position: absolute;
+ right: 15px;
+ top: 15px;
+
+ li {
+ display: inline;
+ list-style-type: none;
+ padding-right: 5px;
+
+ &.active {
+ font-weight: bold;
+
+ a {
+ color: black;
+
+ &:hover {
+ text-decoration:none;
+ }
+ }
+ }
+ }
+}
+
+svg {
+ .axis {
+ path, line {
+ fill: none;
+ stroke: #000;
+ shape-rendering: crispEdges;
+ }
+ }
+}
+
+.communication_chart_wrapper {
+ display:inline-block;
+ vertical-align: middle;
+ width: 38%;
+}
+
+.communication_chart_connection {
+ display:inline-block;
+ width: 60px;
+ height: 30px;
+ vertical-align: middle;
+ background: url('/static/dashboard/img/communication_flow.png') no-repeat 50% 50%;
+ background-size: 40px 20px;
+}
+
+.circles_chart_time_picker {
+ float: right;
+}
+
+.csv_rack_table {
+ padding-top: 40px;
+}
+
+// breadcrumbs
+
+.breadcrumbs {
+ font-size: 85%;
+ margin: 0 0 25px 0;
+
+ a {
+ color: rgb(170, 170, 170);
+ text-decoration: underline;
+
+ &:hover {
+ text-decoration: none;
+ color: rgb(100, 100, 100);
+ }
+ }
+
+ .separator {
+ color: rgb(200, 200, 200);
+
+ &:before {
+ display: inline;
+ content: ">>";
+ margin: 0 7px;
+ }
+
+ &:last-child:after {
+ display: inline;
+ content: "...";
+ }
+ }
+}
+
+// others
+.btn,
+input {
+ border-radius: 2px;
+}
+
+.btn-group .btn:first-child {
+ border-radius: 2px 0 0 2px;
+}
+
+.btn-group .btn:last-child {
+ border-radius: 0 2px 2px 0;
+}
+
+.btn {
+ background: rgb(243, 243, 243);
+
+ &:hover {
+ background: rgb( 235, 235, 235);
+ }
+
+ &.btn-primary {
+ background: #0088cc;
+
+ &:hover {
+ background: #006Dcc;
+ }
+ }
+}
+
+.btn-toolbar.pull-right {
+ margin: 0;
+}
+
+.alerts .icon-warning-sign {
+ padding-right: 5px;
+}
+
+.well {
+ background: rgb(249, 249, 249);
+ border-width: 1px;
+ box-shadow: none;
+}
+
+// forms
+.modal {
+ .help_text,
+ .right {
+ display: none;
+ }
+}
+
+// rack creation
+#id_resource_class_flavors_table {
+ .table tr td {
+ line-height: 2;
+ }
+
+ .number_input_slim {
+ width: 3em;
+ padding: 2px 5px;
+ margin: 0;
+ }
+}
diff --git a/openstack_dashboard/dashboards/infrastructure/templates/infrastructure/base.html b/openstack_dashboard/dashboards/infrastructure/templates/infrastructure/base.html
new file mode 100644
index 00000000..ef27a68f
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/templates/infrastructure/base.html
@@ -0,0 +1,18 @@
+{% extends 'base.html' %}
+
+{% block css %}
+ {% include "_stylesheets.html" %}
+
+ {% load compress %}
+ {% compress css %}
+ <link href='{{ STATIC_URL }}infrastructure/less/infrastructure.less' type='text/less' media='screen' rel='stylesheet' />
+ {% endcompress %}
+{% endblock %}
+
+{% block js %}
+ <script src='{{ STATIC_URL }}horizon/js/horizon.capacity.js' type='text/javascript' charset='utf-8'></script>
+ <script src='{{ STATIC_URL }}horizon/js/horizon.d3modallinechart.js' type='text/javascript' charset='utf-8'></script>
+ <script src='{{ STATIC_URL }}horizon/js/horizon.d3circleschart.js' type='text/javascript' charset='utf-8'></script>
+ <script src='{{ STATIC_URL }}horizon/js/horizon.d3linechart.js' type='text/javascript' charset='utf-8'></script>
+ <script src='{{ STATIC_URL }}horizon/js/horizon.d3singlebarchart.js' type='text/javascript' charset='utf-8'></script>
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/infrastructure/templates/infrastructure/base_detail.html b/openstack_dashboard/dashboards/infrastructure/templates/infrastructure/base_detail.html
new file mode 100644
index 00000000..f7274ea7
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/templates/infrastructure/base_detail.html
@@ -0,0 +1,27 @@
+{% extends 'infrastructure/base.html' %}
+
+{% block main %}
+
+<div class="row-fluid">
+ <div class="span12">
+ {% block breadcrumbs %}{% endblock %}
+
+ <div class="pull-right btn-toolbar">
+ {% block actions %}{% endblock %}
+ </div>
+
+ <h3>{% block name %}{% endblock %}</h3>
+ </div>
+</div>
+
+<div class="row-fluid">
+ <div class="span12">
+ <div class="pull-right">
+ {% block overall_usage %}{% endblock %}
+ </div>
+
+ {{ tab_group.render }}
+ </div>
+</div>
+
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/infrastructure/templatetags/__init__.py b/openstack_dashboard/dashboards/infrastructure/templatetags/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/templatetags/__init__.py
diff --git a/openstack_dashboard/dashboards/infrastructure/templatetags/chart_helpers.py b/openstack_dashboard/dashboards/infrastructure/templatetags/chart_helpers.py
new file mode 100644
index 00000000..8eefaf10
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/templatetags/chart_helpers.py
@@ -0,0 +1,40 @@
+from django import template
+from django.utils import simplejson
+
+register = template.Library()
+
+
+@register.filter()
+def remaining_capacity_by_flavors(obj):
+ flavors = obj.list_flavors
+
+ decorated_obj = " ".join(
+ [("<p><strong>{0}</strong> {1}</p>").format(
+ str(flavor.used_instances),
+ flavor.name)
+ for flavor in flavors])
+
+ decorated_obj = ("<p>Capacity remaining by flavors: </p>" +
+ decorated_obj)
+
+ return decorated_obj
+
+
+@register.filter()
+def all_used_instances(obj):
+ flavors = obj.list_flavors
+
+ all_used_instances_info = []
+ for flavor in flavors:
+ info = {}
+ info['popup_used'] = (
+ '<p> {0}% total,'
+ ' <strong> {1} instances</strong> of {2}</p>'.format(
+ flavor.used_instances,
+ flavor.used_instances,
+ flavor.name))
+ info['used_instances'] = str(flavor.used_instances)
+
+ all_used_instances_info.append(info)
+
+ return simplejson.dumps(all_used_instances_info)
diff --git a/openstack_dashboard/exceptions.py b/openstack_dashboard/exceptions.py
index e5dc97d8..958e075a 100644
--- a/openstack_dashboard/exceptions.py
+++ b/openstack_dashboard/exceptions.py
@@ -18,6 +18,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+from django.core.exceptions import ObjectDoesNotExist
+
from cinderclient import exceptions as cinderclient
from glanceclient.common import exceptions as glanceclient
from heatclient import exc as heatclient
@@ -45,7 +47,10 @@ NOT_FOUND = (keystoneclient.NotFound,
glanceclient.NotFound,
neutronclient.NetworkNotFoundClient,
neutronclient.PortNotFoundClient,
- heatclient.HTTPNotFound)
+ heatclient.HTTPNotFound,
+ # FIXME: this exception and the related import should be replaced
+ # by the one thrown by the tuskar api client
+ ObjectDoesNotExist)
# NOTE(gabriel): This is very broad, and may need to be dialed in.
RECOVERABLE = (keystoneclient.ClientException,
diff --git a/openstack_dashboard/local/local_settings.py.example b/openstack_dashboard/local/local_settings.py.example
index 4494b695..fed29015 100644
--- a/openstack_dashboard/local/local_settings.py.example
+++ b/openstack_dashboard/local/local_settings.py.example
@@ -352,3 +352,16 @@ SECURITY_GROUP_RULES = {
'to_port': '3389',
},
}
+
+# FIXME: this will eventually be unneeded, as it will be retrieved from Keystone
+#TUSKAR_ENDPOINT_URL = "http://127.0.0.1:6385"
+#NOVA_BAREMETAL_CREDS = {
+# 'user': 'admin',
+# 'password': 'admin_password_here',
+# 'tenant': 'admin',
+# 'auth_url': 'http://localhost:5001/v2.0/',
+# 'bypass_url': 'http://localhost:9774/v2/692567cd99f84f5d8f26ec23ff0ba460'
+#}
+#OVERCLOUD_AUTH_URL = 'http://127.0.0.1:5000/v2.0'
+#OVERCLOUD_USERNAME = 'admin'
+#OVERCLOUD_PASSWORD = 'password'
diff --git a/openstack_dashboard/settings.py b/openstack_dashboard/settings.py
index b05cd98c..e7fb4a5a 100644
--- a/openstack_dashboard/settings.py
+++ b/openstack_dashboard/settings.py
@@ -54,7 +54,7 @@ STATIC_URL = '/static/'
ROOT_URLCONF = 'openstack_dashboard.urls'
HORIZON_CONFIG = {
- 'dashboards': ('project', 'admin', 'settings',),
+ 'dashboards': ('project', 'admin', 'infrastructure', 'settings',),
'default_dashboard': 'project',
'user_home': 'openstack_dashboard.views.get_user_home',
'ajax_queue_limit': 10,
@@ -138,6 +138,7 @@ INSTALLED_APPS = (
'horizon',
'openstack_dashboard.dashboards.project',
'openstack_dashboard.dashboards.admin',
+ 'openstack_dashboard.dashboards.infrastructure',
'openstack_dashboard.dashboards.settings',
'openstack_auth',
)
@@ -191,3 +192,16 @@ COMPRESS_OFFLINE_CONTEXT = {
if DEBUG:
logging.basicConfig(level=logging.DEBUG)
+
+# FIXME: configuration for dummy data
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': 'openstack_dashboard/dummydb.sqlite',
+ }
+}
+
+# FIXME: configuration for dummy data
+FIXTURE_DIRS = (
+ 'openstack_dashboard/dashboards/infrastructure/fixtures/',
+)
diff --git a/openstack_dashboard/static/dashboard/img/communication_flow.png b/openstack_dashboard/static/dashboard/img/communication_flow.png
new file mode 100644
index 00000000..44e6b642
--- /dev/null
+++ b/openstack_dashboard/static/dashboard/img/communication_flow.png
Binary files differ
diff --git a/openstack_dashboard/static/dashboard/img/horizontal_loader.gif b/openstack_dashboard/static/dashboard/img/horizontal_loader.gif
new file mode 100644
index 00000000..f2d08339
--- /dev/null
+++ b/openstack_dashboard/static/dashboard/img/horizontal_loader.gif
Binary files differ
diff --git a/openstack_dashboard/test/api_tests/tuskar_tests.py b/openstack_dashboard/test/api_tests/tuskar_tests.py
new file mode 100644
index 00000000..4730c44b
--- /dev/null
+++ b/openstack_dashboard/test/api_tests/tuskar_tests.py
@@ -0,0 +1,109 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from __future__ import absolute_import
+
+from django import http
+from django.conf import settings
+from django.test.utils import override_settings
+
+from mox import IsA
+
+from openstack_dashboard import api
+from openstack_dashboard.test import helpers as test
+import openstack_dashboard.dashboards.infrastructure.models as dummymodels
+
+
+class TuskarApiTests(test.APITestCase):
+ def test_resource_class_list(self):
+ rcs = self.tuskar_resource_classes.list()
+
+ tuskarclient = self.stub_tuskarclient()
+ tuskarclient.resource_classes = self.mox.CreateMockAnything()
+ tuskarclient.resource_classes.list().AndReturn(rcs)
+ self.mox.ReplayAll()
+
+ ret_val = api.tuskar.ResourceClass.list(self.request)
+ for rc in ret_val:
+ self.assertIsInstance(rc, api.tuskar.ResourceClass)
+
+ def test_resource_class_get(self):
+ rc = self.tuskar_resource_classes.first()
+
+ tuskarclient = self.stub_tuskarclient()
+ tuskarclient.resource_classes = self.mox.CreateMockAnything()
+ tuskarclient.resource_classes.get(rc.id).AndReturn(rc)
+ self.mox.ReplayAll()
+
+ ret_val = api.tuskar.ResourceClass.get(self.request, rc.id)
+ self.assertIsInstance(ret_val, api.tuskar.ResourceClass)
+
+ def test_resource_class_flavor_counts(self):
+ rc = self.tuskar_resource_classes.first()
+ flavors = self.tuskar_flavors.list()
+
+ tuskarclient = self.stub_tuskarclient()
+ tuskarclient.flavors = self.mox.CreateMockAnything()
+ tuskarclient.flavors.list(rc.id).AndReturn(flavors)
+ self.mox.ReplayAll()
+
+ for f in rc.list_flavors:
+ self.assertIsInstance(f, api.tuskar.Flavor)
+ self.assertEquals(2, len(rc.list_flavors))
+
+ def test_resource_class_racks(self):
+ rc = self.tuskar_resource_classes.first()
+ r = self.tuskar_racks.first()
+
+ tuskarclient = self.stub_tuskarclient()
+ tuskarclient.racks = self.mox.CreateMockAnything()
+ tuskarclient.racks.get(r.id).AndReturn(r)
+ self.mox.ReplayAll()
+
+ for rack in rc.list_racks:
+ self.assertIsInstance(rack, api.tuskar.Rack)
+ self.assertEquals(1, len(rc.list_racks))
+
+ ## FIXME: we need to stub out the bare metal client, will
+ ## be easier once the client is separated out a bit
+ def test_resource_class_nodes(self):
+ rc = self.tuskar_resource_classes.first()
+ r = self.tuskar_racks.first()
+ n = self.nodes.first()
+
+ tuskarclient = self.stub_tuskarclient()
+ tuskarclient.racks = self.mox.CreateMockAnything()
+ tuskarclient.racks.get(r.id).AndReturn(r)
+ self.mox.ReplayAll()
+
+ for node in rc.nodes:
+ self.assertIsInstance(node, api.tuskar.Node)
+ self.assertEquals(4, len(rc.nodes))
+
+ # TODO: create, delete operations
+
+ def test_flavor_template_list(self):
+ templates = api.tuskar.FlavorTemplate.list(self.request)
+ self.assertEquals(7, len(templates))
+ for t in templates:
+ self.assertIsInstance(t, api.tuskar.FlavorTemplate)
+
+ def test_flavor_template_get(self):
+ test_template = self.tuskar_flavor_templates.first()
+ template = api.tuskar.FlavorTemplate.get(self.request,
+ test_template.id)
+ self.assertIsInstance(template, api.tuskar.FlavorTemplate)
+ self.assertEquals(template.name, test_template.name)
diff --git a/openstack_dashboard/test/helpers.py b/openstack_dashboard/test/helpers.py
index 79d201fd..1a6ad870 100644
--- a/openstack_dashboard/test/helpers.py
+++ b/openstack_dashboard/test/helpers.py
@@ -37,6 +37,7 @@ from keystoneclient.v2_0 import client as keystone_client
from neutronclient.v2_0 import client as neutron_client
from novaclient.v1_1 import client as nova_client
from swiftclient import client as swift_client
+from tuskarclient.v1 import client as tuskar_client
import httplib2
import mox
@@ -261,6 +262,7 @@ class APITestCase(TestCase):
self._original_neutronclient = api.neutron.neutronclient
self._original_cinderclient = api.cinder.cinderclient
self._original_heatclient = api.heat.heatclient
+ self._original_tuskarclient = api.tuskar.tuskarclient
# Replace the clients with our stubs.
api.glance.glanceclient = lambda request: self.stub_glanceclient()
@@ -269,6 +271,7 @@ class APITestCase(TestCase):
api.neutron.neutronclient = lambda request: self.stub_neutronclient()
api.cinder.cinderclient = lambda request: self.stub_cinderclient()
api.heat.heatclient = lambda request: self.stub_heatclient()
+ api.tuskar.tuskarclient = lambda request: self.stub_tuskarclient()
def tearDown(self):
super(APITestCase, self).tearDown()
@@ -278,6 +281,7 @@ class APITestCase(TestCase):
api.neutron.neutronclient = self._original_neutronclient
api.cinder.cinderclient = self._original_cinderclient
api.heat.heatclient = self._original_heatclient
+ api.tuskar.tuskarclient = self._original_tuskarclient
def stub_novaclient(self):
if not hasattr(self, "novaclient"):
@@ -335,6 +339,12 @@ class APITestCase(TestCase):
self.heatclient = self.mox.CreateMock(heat_client.Client)
return self.heatclient
+ def stub_tuskarclient(self):
+ if not hasattr(self, "tuskarclient"):
+ self.mox.StubOutWithMock(tuskar_client, 'Client')
+ self.tuskarclient = self.mox.CreateMock(tuskar_client.Client)
+ return self.tuskarclient
+
@unittest.skipUnless(os.environ.get('WITH_SELENIUM', False),
"The WITH_SELENIUM env variable is not set.")
diff --git a/openstack_dashboard/test/settings.py b/openstack_dashboard/test/settings.py
index dfcaa22c..a4ee5ea6 100644
--- a/openstack_dashboard/test/settings.py
+++ b/openstack_dashboard/test/settings.py
@@ -38,6 +38,7 @@ INSTALLED_APPS = (
'openstack_dashboard',
'openstack_dashboard.dashboards.project',
'openstack_dashboard.dashboards.admin',
+ 'openstack_dashboard.dashboards.infrastructure',
'openstack_dashboard.dashboards.settings',
)
@@ -46,7 +47,7 @@ AUTHENTICATION_BACKENDS = ('openstack_auth.backend.KeystoneBackend',)
SITE_BRANDING = 'OpenStack'
HORIZON_CONFIG = {
- 'dashboards': ('project', 'admin', 'settings'),
+ 'dashboards': ('project', 'admin', 'infrastructure', 'settings'),
'default_dashboard': 'project',
"password_validator": {
"regex": '^.{8,18}$',
@@ -121,3 +122,17 @@ NOSE_ARGS = ['--nocapture',
'--cover-package=openstack_dashboard',
'--cover-inclusive',
'--all-modules']
+
+# FIXME: this will eventually be unneeded when the parameter is removed
+# from local settings and api/tuskar.py
+TUSKAR_ENDPOINT_URL = "http://127.0.0.1:6385"
+NOVA_BAREMETAL_CREDS = {
+ 'user': 'admin',
+ 'password': 'admin_password_here',
+ 'tenant': 'admin',
+ 'auth_url': 'http://localhost:5001/v2.0/',
+ 'bypass_url': 'http://localhost:9774/v2/692567cd99f84f5d8f26ec23ff0ba460'
+}
+OVERCLOUD_AUTH_URL = 'http://127.0.0.1:5000/v2.0'
+OVERCLOUD_USERNAME = 'admin'
+OVERCLOUD_PASSWORD = 'password'
diff --git a/openstack_dashboard/test/test_data/tuskar_data.py b/openstack_dashboard/test/test_data/tuskar_data.py
new file mode 100644
index 00000000..08c42da7
--- /dev/null
+++ b/openstack_dashboard/test/test_data/tuskar_data.py
@@ -0,0 +1,167 @@
+# 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.api.tuskar import (
+ Flavor, FlavorTemplate, ResourceClass, Node,
+ Rack, Capacity)
+from collections import namedtuple
+
+import openstack_dashboard.dashboards.infrastructure.models as dummymodels
+
+from .utils import TestDataContainer
+
+
+def data(TEST):
+ FlavorStruct = namedtuple('FlavorStruct', 'id name\
+ capacities')
+ CapacityStruct = namedtuple('CapacityStruct', 'name value unit')
+ TEST.tuskar_flavor_templates = TestDataContainer()
+ flavor_template_1 = FlavorTemplate(FlavorStruct(
+ id="1",
+ name='nano',
+ capacities=[
+ Capacity(CapacityStruct(
+ name='cpu',
+ unit='',
+ value='1')),
+ Capacity(CapacityStruct(
+ name='memory',
+ unit='MB',
+ value='64')),
+ Capacity(CapacityStruct(
+ name='storage',
+ unit='MB',
+ value='128')),
+ Capacity(CapacityStruct(
+ name='ephemeral_disk',
+ unit='GB',
+ value='0')),
+ Capacity(CapacityStruct(
+ name='swap_disk',
+ unit='GB',
+ value='0'))]))
+ flavor_template_2 = FlavorTemplate(FlavorStruct(
+ id="2",
+ name='large',
+ capacities=[]))
+ TEST.tuskar_flavor_templates.add(flavor_template_1, flavor_template_2)
+
+ # Flavors
+ TEST.tuskar_flavors = TestDataContainer()
+ flavor_1 = Flavor(FlavorStruct(
+ id="1",
+ name='nano',
+ capacities=[]))
+ flavor_2 = Flavor(FlavorStruct(
+ id="2",
+ name='large',
+ capacities=[]))
+ TEST.tuskar_flavors.add(flavor_1, flavor_2)
+
+ # Resource Classes
+ TEST.tuskar_resource_classes = TestDataContainer()
+
+ ResourceClassStruct = namedtuple('ResourceClassStruct', 'id service_type\
+ name racks')
+ resource_class_1 = ResourceClass(ResourceClassStruct(
+ id="1",
+ service_type="compute",
+ racks=[{'id': 1}],
+ name="rclass1"))
+
+ resource_class_2 = ResourceClass(ResourceClassStruct(
+ id="2",
+ service_type="compute",
+ racks=[],
+ name="rclass2"))
+
+ """
+ # FIXME to make code below work, every @property has to have
+ # setter defined in API model
+ flavors = []
+ all_flavors = []
+ resources = []
+ all_resources = []
+
+ @resources.setter
+ def resources(self, value):
+ self._resources = value
+ resource_class_1.resources = resources
+ resource_class_2.resources = resources
+
+ resource_class_1.all_resources = all_resources
+ resource_class_2.all_resources = all_resources
+
+ resource_class_1.flavors = flavors
+ resource_class_2.flavors = flavors
+
+ resource_class_1.all_flavors = all_flavors
+ resource_class_2.all_flavors = all_flavors
+ """
+
+ TEST.tuskar_resource_classes.add(resource_class_1, resource_class_2)
+
+ #Racks
+ TEST.tuskar_racks = TestDataContainer()
+ # FIXME: Struct is used to provide similar object-like behaviour
+ # as is provided by tuskarclient
+ RackStruct = namedtuple('RackStruct', 'id name nodes resource_class\
+ location subnet state')
+ rack_1 = Rack(RackStruct(
+ id="1",
+ name='rack1',
+ location='location',
+ subnet='192.168.1.0/24',
+ state='provisioned',
+ nodes=[{'id': '1'}, {'id': '2'}, {'id': '3'}, {'id': '4'}],
+ resource_class={'id': '1'}))
+
+ TEST.tuskar_racks.add(rack_1)
+
+ # Nodes
+ TEST.nodes = TestDataContainer()
+ TEST.unracked_nodes = TestDataContainer()
+
+ node_1 = Node(dummymodels.Node(id="1",
+ name="node1",
+ rack_id=1,
+ mac_address="00-B0-D0-86-AB-F7",
+ ip_address="192.168.191.11",
+ status="active",
+ usage="20"))
+ node_2 = Node(dummymodels.Node(id="2",
+ name="node2",
+ rack_id=1,
+ mac_address="00-B0-D0-86-AB-F8",
+ ip_address="192.168.191.12",
+ status="active",
+ usage="20"))
+ node_3 = Node(dummymodels.Node(id="3",
+ name="node3",
+ rack_id=1,
+ mac_address="00-B0-D0-86-AB-F9",
+ ip_address="192.168.191.13",
+ status="active",
+ usage="20"))
+ node_4 = Node(dummymodels.Node(id="4",
+ name="node4",
+ rack_id=1,
+ mac_address="00-B0-D0-86-AB-F0",
+ ip_address="192.168.191.14",
+ status="active",
+ usage="20"))
+ node_5 = Node(dummymodels.Node(id="5",
+ name="node5",
+ mac_address="00-B0-D0-86-AB-F1"))
+
+ TEST.nodes.add(node_1, node_2, node_3, node_4)
+ TEST.unracked_nodes.add(node_5)
diff --git a/openstack_dashboard/test/test_data/utils.py b/openstack_dashboard/test/test_data/utils.py
index ceb6baed..3509cd65 100644
--- a/openstack_dashboard/test/test_data/utils.py
+++ b/openstack_dashboard/test/test_data/utils.py
@@ -22,6 +22,7 @@ def load_test_data(load_onto=None):
from openstack_dashboard.test.test_data import neutron_data
from openstack_dashboard.test.test_data import nova_data
from openstack_dashboard.test.test_data import swift_data
+ from openstack_dashboard.test.test_data import tuskar_data
# The order of these loaders matters, some depend on others.
loaders = (exceptions.data,
@@ -31,7 +32,8 @@ def load_test_data(load_onto=None):
cinder_data.data,
neutron_data.data,
swift_data.data,
- heat_data.data)
+ heat_data.data,
+ tuskar_data.data)
if load_onto:
for data_func in loaders:
data_func(load_onto)
diff --git a/requirements.txt b/requirements.txt
index b5ef7e67..66f18471 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -20,3 +20,5 @@ pytz>=2010h
# Horizon Utility Requirements
# for SECURE_KEY generation
lockfile>=0.8
+
+-e git://github.com/tuskar/python-tuskarclient.git#egg=python-tuskarclient