ironic-ui 2.0.0 release

meta:version: 2.0.0
 meta:diff-start: -
 meta:series: newton
 meta:release-type: release
 meta:announce: openstack-announce@lists.openstack.org
 meta:pypi: yes
 meta:first: yes
 meta:release:Author: Elizabeth Elwell <e.r.elwell@gmail.com>
 meta:release:Commit: Elizabeth Elwell <e.r.elwell@gmail.com>
 meta:release:Change-Id: I7aca3d9fe3d97e1ab725dc52ace9a6a0de3ccf88
 meta:release:Code-Review+1: Jim Rollenhagen <jim@jimrollenhagen.com>
 meta:release:Code-Review+2: Doug Hellmann <doug@doughellmann.com>
 meta:release:Workflow+1: Doug Hellmann <doug@doughellmann.com>
 -----BEGIN PGP SIGNATURE-----
 Comment: GPGTools - http://gpgtools.org
 
 iQEcBAABAgAGBQJXtfb1AAoJEDttBqDEKEN6W2AH/3g8hGg9t4AIH0IcxydBjCXE
 bvVzO8/c6Ib42z45+XUoZTLOu9tjivQ3hQpIEytwpaNnKWcELk1tEaQQdurMXcUn
 aPal7ljXTD1EbD1gmIuDPh9XgoDPHLg/1BbeYnCbE0TGbxect/jfWYfdyBGql793
 cduPkA3yXFzhdHU3LFdofGoz+vnrzMJmLiiH4DLcZeuJBnoRWWipi8tlaanVNO9t
 jQIJfmMuSSO+IbYL3i4GdshcfJRn8roZDqY295tnZlOy5Nc2Dw3GAuliB68/X6sy
 dXGhQ5mncR2D6IO8RIQ0l260ZB+9e6KT9D8pXTDcqjk3TWyEWoQPYma8C/dxBZk=
 =bxCb
 -----END PGP SIGNATURE-----

Merge tag '2.0.0' into debian/newton

ironic-ui 2.0.0 release

  * New upstream release.

Change-Id: If0ce5982efe48af6841e91c95b1520162727ac85
This commit is contained in:
Thomas Goirand 2016-09-28 17:04:48 +02:00
commit ca8a9eef3d
55 changed files with 4991 additions and 542 deletions

3
.gitignore vendored
View File

@ -60,3 +60,6 @@ ChangeLog
package/
node_modules/
npm-debug.log
# release notes build
releasenotes/build

View File

@ -1,5 +1,5 @@
[gerrit]
host=review.openstack.org
port=29418
project=openstack/ironic-ui.git
defaultbranch=stable/mitaka
project=openstack/deb-ironic-ui.git
defaultbranch=debian/newton

View File

@ -34,7 +34,7 @@ installation please see http://docs.openstack.org/developer/horizon/quickstart.h
`source .venv/bin/activate`
3. Copy the _2200_ironic.py file from ironic-ui/enabled directory to
3. Copy the _2200_ironic.py file from ironic_ui/enabled directory to
horizon/openstack_dashboard/local/enabled
4. Change into the ironic-ui repository and package the plugin:

14
debian/changelog vendored
View File

@ -1,13 +1,5 @@
ironic-ui (1.1.1-2) UNRELEASED; urgency=medium
ironic-ui (2.0.0-1) experimental; urgency=medium
* Standards-Version is 3.9.8 now (no change)
* d/rules: Removed UPSTREAM_GIT with default value
* d/copyright: Changed source URL to https protocol
* Initial release. (Closes: #839076)
-- Ondřej Nový <novy@ondrej.org> Sat, 09 Apr 2016 19:23:10 +0200
ironic-ui (1.1.1-1) unstable; urgency=medium
* Initial release. (Closes: #XXXXXX)
-- Thomas Goirand <zigo@debian.org> Mon, 21 Mar 2016 22:26:27 +0000
-- Thomas Goirand <zigo@debian.org> Wed, 28 Sep 2016 17:06:05 +0200

20
debian/control vendored
View File

@ -10,15 +10,27 @@ Build-Depends: debhelper (>= 9),
python-pbr (>= 1.8),
python-setuptools,
python-sphinx,
Build-Depends-Indep: openstack-dashboard (>= 2:9.0.0~rc1),
Build-Depends-Indep: openstack-dashboard (>= 3:10.0.0~rc2),
python-coverage,
python-django-nose (>= 1.2),
python-hacking,
python-ironicclient (>= 1.1.0),
python-oslosphinx (>= 2.5.0),
python-oslotest (>= 1:1.10.0),
python-subunit,
python-testscenarios,
python-testtools (>= 1.4.0),
subunit,
testrepository,
Standards-Version: 3.9.8
Vcs-Git: https://anonscm.debian.org/git/openstack/ironic-ui.git
Vcs-Browser: https://anonscm.debian.org/gitweb/?p=openstack/ironic-ui.git;a=summary
Vcs-Browser: https://git.openstack.org/cgit/openstack/deb-ironic-ui?h=debian%2Fnewton
Vcs-Git: https://git.openstack.org/openstack/deb-ironic-ui -b debian/newton
Homepage: https://github.com/openstack/ironic-ui
Package: python-ironic-ui
Architecture: all
Depends: openstack-dashboard (>= 2:9.0.0~rc1),
Depends: openstack-dashboard (>= 3:10.0.0~rc2),
python-ironicclient (>= 1.1.0),
python-pbr (>= 1.8),
python-six (>= 1.7.0),
${misc:Depends},

3
debian/source.lintian-overrides vendored Normal file
View File

@ -0,0 +1,3 @@
# This is a false positive from Lintian: this really is a source file
# and not a minimized .js file: it just happens to have a long line.
ironic-ui source: source-is-missing ironic_ui/static/dashboard/admin/ironic/enroll-node/enroll-node.service.js line length is 1173 characters (>512)

2
debian/source/options vendored Normal file
View File

@ -0,0 +1,2 @@
extend-diff-ignore = "^[^/]*[.]egg-info/"
extend-diff-ignore = "^[.]gitreview$"

View File

@ -1,4 +1,50 @@
============
Contributing
============
.. include:: ../../CONTRIBUTING.rst
.. _contributing:
=========================
Contributing to Ironic UI
=========================
If you're interested in contributing to the Ironic UI project,
the following will help get you started.
Contributor License Agreement
-----------------------------
.. index::
single: license; agreement
In order to contribute to the Ironic project, you need to have
signed OpenStack's contributor's agreement.
.. seealso::
* http://docs.openstack.org/infra/manual/developers.html
* http://wiki.openstack.org/CLA
LaunchPad Project
-----------------
Most of the tools used for OpenStack depend on a launchpad.net ID for
authentication.
.. seealso::
* https://launchpad.net
* https://launchpad.net/ironic
Project Hosting Details
-------------------------
Bug tracker
http://launchpad.net/ironic-ui
Mailing list (prefix subjects with ``[ironic-ui]`` for faster responses)
http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-dev
Code Hosting
https://github.com/openstack/ironic-ui
Code Review
https://review.openstack.org/#/q/status:open+project:openstack/ironic-ui,n,z

View File

@ -1,25 +1,33 @@
.. ironic-ui documentation master file, created by
sphinx-quickstart on Tue Jul 9 22:26:36 2013.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
===============================================
Welcome to Ironic UI's developer documentation!
===============================================
Welcome to ironic-ui's documentation!
========================================================
Introduction
============
Contents:
The ironic UI is an OpenStack Horizon plugin that will allow users to view and
manage their ironic bare metal nodes, ports and drivers.
The documentation provided here is continually kept up-to-date based
on the latest code that has been committed, and may not represent the state of
the project at any specific prior release.
For information on any current or prior version of Ironic, see `the release
notes`_.
.. _the release notes: http://docs.openstack.org/releasenotes/ironic-ui/
For more information on ironic, see `the ironic documentation`_.
.. _the ironic documentation: http://docs.openstack.org/developer/ironic/
Administrator's Guide
=====================
.. toctree::
:maxdepth: 2
readme
installation
usage
contributing
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
:maxdepth: 1
Introduction to ironic UI <readme>
Introduction to ironic <http://docs.openstack.org/developer/ironic/deploy/user-guide.html>
Installing the ironic UI <installation>
Contributing <contributing>

View File

@ -1,12 +1,40 @@
============
Installation
============
.. _installation:
At the command line::
======================
Ironic-UI Installation
======================
$ pip install ironic-ui
Please note that the following instructions assume that you have an existing
installation of the OpenStack Horizon dashboard application. For Horizon
installation please see http://docs.openstack.org/developer/horizon/quickstart.html
Or, if you have virtualenvwrapper installed::
1. Clone Ironic UI repository:
$ mkvirtualenv ironic-ui
$ pip install ironic-ui
`git clone https://git.openstack.org/openstack/ironic-ui`
2. Change into the root directory of your horizon installation and run the venv.
NOTE: this has been preinstalled when horizon was setup with ./run_tests.sh -
do not reinstall venv
`source .venv/bin/activate`
3. Copy the _2200_ironic.py file from ironic_ui/enabled directory to
horizon/openstack_dashboard/local/enabled
4. Change into the ironic-ui repository and package the plugin:
`pip install -r requirements.txt -e .`
This will build and install the ironic-ui plugin into the active virtual
environment associated with your horizon installation. The plugin is installed
in "editable" mode as a link back to your ironic-ui plugin directory.
5. Change back into the horizon repository and bring up your environment:
`./run_tests.sh --runserver`
The Bare Metal service should now be visible in the Horizon navigation.
To uninstall, use pip uninstall (find the name of the package to uninstall by
running pip list from inside the horizon .venv). You will also need to remove
the enabled file from the openstack_dashboard/enabled folder.

View File

@ -117,3 +117,73 @@ def node_set_maintenance(request, node_id, state, maint_reason=None):
node_id,
state,
maint_reason=maint_reason)
def node_create(request, params):
"""Create a node
:param request: HTTP request.
:param params: Dictionary of node parameters
"""
node_manager = ironicclient(request).node
node = node_manager.create(**params)
field_list = ['chassis_uuid',
'driver',
'driver_info',
'properties',
'extra',
'uuid',
'name']
return dict([(f, getattr(node, f, '')) for f in field_list])
def node_delete(request, node_id):
"""Delete a node from inventory.
:param request: HTTP request.
:param node_id: The UUID of the node.
:return: node.
http://docs.openstack.org/developer/python-ironicclient/api/ironicclient.v1.node.html#ironicclient.v1.node.NodeManager.delete
"""
return ironicclient(request).node.delete(node_id)
def driver_list(request):
"""Retrieve a list of drivers.
:param request: HTTP request.
:return: A list of drivers.
"""
return ironicclient(request).driver.list()
def driver_properties(request, driver_name):
"""Retrieve the properties of a specified driver
:param request: HTTP request
:param driver_name: Name of the driver
:return: Property list
"""
return ironicclient(request).driver.properties(driver_name)
def port_create(request, params):
"""Create network port
:param request: HTTP request
:param params: Port creation parameters
:return: Port
"""
port_manager = ironicclient(request).port
return port_manager.create(**params)
def port_delete(request, port_uuid):
"""Delete a network port
:param request: HTTP request
:param port_uuid: Port uuid
:return: Port
"""
return ironicclient(request).port.delete(port_uuid)

View File

@ -40,6 +40,24 @@ class Nodes(generic.View):
'items': [i.to_dict() for i in items],
}
@rest_utils.ajax(data_required=True)
def post(self, request):
"""Create an Ironic node
:param request: HTTP request
"""
params = request.DATA.get('node')
return ironic.node_create(request, params)
@rest_utils.ajax(data_required=True)
def delete(self, request):
"""Delete an Ironic node from inventory
:param request: HTTP request
"""
params = request.DATA.get('node')
return ironic.node_delete(request, params)
@urls.register
class Node(generic.View):
@ -75,6 +93,25 @@ class Ports(generic.View):
'items': [i.to_dict() for i in items],
}
@rest_utils.ajax(data_required=True)
def post(self, request):
"""Create a network port
:param request: HTTP request
:return: Port
"""
port = request.DATA.get('port')
return ironic.port_create(request, port).to_dict()
@rest_utils.ajax(data_required=True)
def delete(self, request):
"""Delete a network port
:param request: HTTP request
"""
params = request.DATA.get('port_uuid')
return ironic.port_delete(request, params)
@urls.register
class StatesPower(generic.View):
@ -122,3 +159,37 @@ class Maintenance(generic.View):
:return: Return code
"""
return ironic.node_set_maintenance(request, node_id, 'off')
@urls.register
class Drivers(generic.View):
url_regex = r'ironic/drivers/$'
@rest_utils.ajax()
def get(self, request):
"""Get the list of drivers
:param request: HTTP request
:return: drivers
"""
items = ironic.driver_list(request)
return {
'items': [i.to_dict() for i in items]
}
@urls.register
class DriverProperties(generic.View):
url_regex = r'ironic/drivers/(?P<driver_name>[0-9a-zA-Z_-]+)/properties$'
@rest_utils.ajax()
def get(self, request, driver_name):
"""Get the properties associated with a specified driver
:param request: HTTP request
:param driver_name: Driver name
:return: Dictionary of properties
"""
return ironic.driver_properties(request, driver_name)

View File

@ -17,7 +17,27 @@ from django.utils.translation import ugettext_lazy as _
import horizon
from openstack_dashboard.api import base
from openstack_dashboard.dashboards.admin import dashboard
class Ironic(horizon.Panel):
name = _("Ironic Bare Metal Provisioning")
slug = 'ironic'
permissions = ('openstack.roles.admin',)
def allowed(self, context):
request = context['request']
if not base.is_service_enabled(request, 'baremetal'):
return False
else:
return super(Ironic, self).allowed(context)
def nav(self, context):
request = context['request']
if not base.is_service_enabled(request, 'baremetal'):
return False
else:
return True
dashboard.Admin.register(Ironic)

View File

@ -2,6 +2,14 @@
{% load i18n %}
{% block title %}{% trans "Ironic Bare Metal Provisioning" %}{% endblock %}
{% block breadcrumb_nav %}
<ol class="breadcrumb">
<li>{% trans "Admin" %}</li>
<li>{% trans "System" %}</li>
<li class="active">{% trans "Ironic Bare Metal Provisioning" %}</li>
</ol>
{% endblock %}
{% block page_header %}
<hz-page-header header="{% trans "Ironic Bare Metal Provisioning" %}"></hz-page-header>
{% endblock %}

View File

@ -2,6 +2,15 @@
{% load i18n %}
{% block title %}{% trans "Node Details" %}{% endblock %}
{% block breadcrumb_nav %}
<ol class="breadcrumb">
<li>{% trans "Admin" %}</li>
<li>{% trans "System" %}</li>
<li>{% trans "Ironic Bare Metal Provisioning" %}</li>
<li class="active">{% trans "Node Details" %}</li>
</ol>
{% endblock %}
{% block page_header %}
<hz-page-header header="{% trans "Node Details" %}"></hz-page-header>
{% endblock %}

View File

@ -0,0 +1,18 @@
# Andi Chandler <andi@gowling.com>, 2016. #zanata
msgid ""
msgstr ""
"Project-Id-Version: ironic-ui 1.1.1.dev28\n"
"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n"
"POT-Creation-Date: 2016-07-13 21:09+0000\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"PO-Revision-Date: 2016-06-29 01:16+0000\n"
"Last-Translator: Andi Chandler <andi@gowling.com>\n"
"Language-Team: English (United Kingdom)\n"
"Language: en-GB\n"
"X-Generator: Zanata 3.7.3\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
msgid "Ironic Bare Metal Provisioning"
msgstr "Ironic Bare Metal Provisioning"

View File

@ -0,0 +1,311 @@
# OpenStack Infra <zanata@openstack.org>, 2015. #zanata
# Rob Cresswell <robert.cresswell@outlook.com>, 2015. #zanata
# Andi Chandler <andi@gowling.com>, 2016. #zanata
# Andreas Jaeger <jaegerandi@gmail.com>, 2016. #zanata
# Rob Cresswell <robert.cresswell@outlook.com>, 2016. #zanata
msgid ""
msgstr ""
"Project-Id-Version: ironic-ui 1.1.1.dev28\n"
"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n"
"POT-Creation-Date: 2016-07-13 21:09+0000\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"PO-Revision-Date: 2016-06-29 01:25+0000\n"
"Last-Translator: Andi Chandler <andi@gowling.com>\n"
"Language-Team: English (United Kingdom)\n"
"Language: en-GB\n"
"X-Generator: Zanata 3.7.3\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
msgid "A unique node name. Optional."
msgstr "A unique node name. Optional."
msgid "Actions"
msgstr "Actions"
msgid "Add Extra:"
msgstr "Add Extra:"
msgid "Add New Property:"
msgstr "Add New Property:"
#, python-format
msgid ""
"Are you sure you want to delete node \"%s\"? This action cannot be undone."
msgstr ""
"Are you sure you want to delete node \"%s\"? This action cannot be undone."
#, python-format
msgid ""
"Are you sure you want to delete nodes \"%s\"? This action cannot be undone."
msgstr ""
"Are you sure you want to delete nodes \"%s\"? This action cannot be undone."
msgid "Cancel"
msgstr "Cancel"
msgid "Capabilities"
msgstr "Capabilities"
msgid "Chassis ID"
msgstr "Chassis ID"
msgid "Choose an Image"
msgstr "Choose an Image"
msgid "Configuration"
msgstr "Configuration"
msgid "Console Enabled"
msgstr "Console Enabled"
msgid "Created At"
msgstr "Created At"
msgid "Delete Node"
msgstr "Delete Node"
msgid "Delete Nodes"
msgstr "Delete Nodes"
msgid "Delete node"
msgstr "Delete node"
msgid "Delete nodes"
msgstr "Delete nodes"
msgid "Deploy Kernel"
msgstr "Deploy Kernel"
msgid "Deploy Ramdisk"
msgstr "Deploy Ramdisk"
msgid "Driver"
msgstr "Driver"
msgid "Driver Details"
msgstr "Driver Details"
msgid "Driver Info"
msgstr "Driver Info"
msgid "Enroll Node"
msgstr "Enroll Node"
#, python-format
msgid "Error deleting nodes \"%s\""
msgstr "Error deleting nodes \"%s\""
msgid "Extra"
msgstr "Extra"
msgid "Extra Property Name"
msgstr "Extra Property Name"
msgid "Extras"
msgstr "Extras"
msgid "General"
msgstr "General"
msgid "Inspection Finished At"
msgstr "Inspection Finished At"
msgid "Inspection Started At"
msgstr "Inspection Started At"
msgid "Instance ID"
msgstr "Instance ID"
msgid "Instance Info"
msgstr "Instance Info"
msgid "Instance Name"
msgstr "Instance Name"
msgid "Kernel"
msgstr "Kernel"
msgid "Last Error"
msgstr "Last Error"
msgid "Maintenance"
msgstr "Maintenance"
msgid "Maintenance Reason"
msgstr "Maintenance Reason"
msgid "Maintenance off"
msgstr "Maintenance off"
msgid "Maintenance on"
msgstr "Maintenance on"
msgid "Name"
msgstr "Name"
msgid "No Instance"
msgstr "No Instance"
msgid "No maintenance reason given."
msgstr "No maintenance reason given."
#, python-format
msgid "Node %s is already in maintenance mode."
msgstr "Node %s is already in maintenance mode."
#, python-format
msgid "Node %s is not in maintenance mode."
msgstr "Node %s is not in maintenance mode."
#, python-format
msgid "Node %s is not powered off."
msgstr "Node %s is not powered off."
#, python-format
msgid "Node %s is not powered on."
msgstr "Node %s is not powered on."
msgid "Node Driver"
msgstr "Node Driver"
msgid "Node ID"
msgstr "Node ID"
msgid "Node Info"
msgstr "Node Info"
msgid "Node Name"
msgstr "Node Name"
msgid "Overview"
msgstr "Overview"
msgid "Ports"
msgstr "Ports"
msgid "Power State"
msgstr "Power State"
msgid "Power off"
msgstr "Power off"
msgid "Power on"
msgstr "Power on"
msgid "Properties"
msgstr "Properties"
msgid "Property Name"
msgstr "Property Name"
msgid ""
"Provide a reason for why you are putting the selected node(s) into "
"maintenance mode (optional)"
msgstr ""
"Provide a reason for why you are putting the selected node(s) into "
"maintenance mode (optional)"
msgid "Provision State"
msgstr "Provision State"
msgid "Provisioning State"
msgstr "Provisioning State"
msgid "Provisioning Status"
msgstr "Provisioning Status"
msgid "Put Node(s) Into Maintenance Mode"
msgstr "Put Node(s) Into Maintenance Mode"
msgid "Ramdisk"
msgstr "Ramdisk"
msgid "Refresh page to see updated power status"
msgstr "Refresh page to see updated power status"
msgid "Required"
msgstr "Required"
msgid "Reservation"
msgstr "Reservation"
msgid "SSH Port"
msgstr "SSH Port"
msgid "SSH Username"
msgstr "SSH Username"
msgid "Select a Driver"
msgstr "Select a Driver"
#, python-format
msgid "Successfully deleted node \"%s\""
msgstr "Successfully deleted node \"%s\""
#, python-format
msgid "Successfully deleted nodes \"%s\""
msgstr "Successfully deleted nodes \"%s\""
msgid "Target Power State"
msgstr "Target Power State"
msgid "Target Provision State"
msgstr "Target Provision State"
msgid "UUID"
msgstr "UUID"
#, python-format
msgid "Unable to create node: %s"
msgstr "Unable to create node: %s"
#, python-format
msgid "Unable to delete node \"%s\""
msgstr "Unable to delete node \"%s\""
#, python-format
msgid "Unable to delete node %s: %s"
msgstr "Unable to delete node %s: %s"
#, python-format
msgid "Unable to power off the node: %s"
msgstr "Unable to power off the node: %s"
#, python-format
msgid "Unable to power on the node: %s"
msgstr "Unable to power on the node: %s"
#, python-format
msgid "Unable to put the Ironic node in maintenance mode: %s"
msgstr "Unable to put the Ironic node in maintenance mode: %s"
#, python-format
msgid "Unable to remove the Ironic node from maintenance mode: %s"
msgstr "Unable to remove the Ironic node from maintenance mode: %s"
#, python-format
msgid "Unable to retrieve Ironic drivers: %s"
msgstr "Unable to retrieve Ironic drivers: %s"
msgid "Unable to retrieve Ironic nodes."
msgstr "Unable to retrieve Ironic nodes."
#, python-format
msgid "Unable to retrieve driver properties: %s"
msgstr "Unable to retrieve driver properties: %s"
#, python-format
msgid "Unable to retrieve the Ironic node ports: %s"
msgstr "Unable to retrieve the Ironic node ports: %s"
#, python-format
msgid "Unable to retrieve the Ironic node: %s"
msgstr "Unable to retrieve the Ironic node: %s"
msgid "Updated At"
msgstr "Updated At"
msgid "{$ property.getDescription() $}"
msgstr "{$ property.getDescription() $}"

View File

@ -0,0 +1,18 @@
# Nicolas Fournier <nicolas.fournier3@gmail.com>, 2016. #zanata
msgid ""
msgstr ""
"Project-Id-Version: ironic-ui 1.1.1.dev9\n"
"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n"
"POT-Creation-Date: 2016-05-05 12:53+0000\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"PO-Revision-Date: 2016-04-30 05:13+0000\n"
"Last-Translator: Nicolas Fournier <nicolas.fournier3@gmail.com>\n"
"Language-Team: French\n"
"Language: fr\n"
"X-Generator: Zanata 3.7.3\n"
"Plural-Forms: nplurals=2; plural=(n > 1)\n"
msgid "Ironic Bare Metal Provisioning"
msgstr "Déploiement Bare Metal Ironic"

View File

@ -0,0 +1,173 @@
# Gael Rehault <gael01@gmail.com>, 2015. #zanata
# OpenStack Infra <zanata@openstack.org>, 2015. #zanata
# Andreas Jaeger <jaegerandi@gmail.com>, 2016. #zanata
# Martine Marin <mmarin@fr.ibm.com>, 2016. #zanata
# Nicolas Fournier <nicolas.fournier3@gmail.com>, 2016. #zanata
msgid ""
msgstr ""
"Project-Id-Version: ironic-ui 1.1.1.dev13\n"
"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n"
"POT-Creation-Date: 2016-05-23 20:35+0000\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"PO-Revision-Date: 2016-04-30 05:12+0000\n"
"Last-Translator: Nicolas Fournier <nicolas.fournier3@gmail.com>\n"
"Language-Team: French\n"
"Language: fr\n"
"X-Generator: Zanata 3.7.3\n"
"Plural-Forms: nplurals=2; plural=(n > 1)\n"
msgid "Actions"
msgstr "Actions"
msgid "Cancel"
msgstr "Annuler"
msgid "Capabilities"
msgstr "Capacités"
msgid "Chassis ID"
msgstr "ID du chassis"
msgid "Configuration"
msgstr "Configuration"
msgid "Console Enabled"
msgstr "Console Activée"
msgid "Created At"
msgstr "Créé le"
msgid "Deploy Kernel"
msgstr "Kernel de déploiement"
msgid "Deploy Ramdisk"
msgstr "Ramdisk de déploiement"
msgid "Driver"
msgstr "Driver"
msgid "Driver Info"
msgstr "Info du Driver"
msgid "Extra"
msgstr "Extra"
msgid "General"
msgstr "Général"
msgid "Inspection Finished At"
msgstr "Inspection terminée à"
msgid "Inspection Started At"
msgstr "Inspection démarrée à"
msgid "Instance ID"
msgstr "ID de l'instance"
msgid "Instance Info"
msgstr "Infos de l'instance"
msgid "Instance Name"
msgstr "Nom de l'instance"
msgid "Kernel"
msgstr "Noyau"
msgid "Last Error"
msgstr "Dernière Erreur"
msgid "Maintenance"
msgstr "Maintenance"
msgid "Maintenance Reason"
msgstr "Raison de la maintenance"
msgid "Maintenance off"
msgstr "Maintenance désactivée"
msgid "Maintenance on"
msgstr "Maintenance activée"
msgid "Name"
msgstr "Nom"
msgid "No Instance"
msgstr "Pas d'instance"
msgid "No maintenance reason given."
msgstr "Aucune raison de maintenance fournie."
msgid "Node ID"
msgstr "ID du noeud"
msgid "Node Name"
msgstr "Nom du noeud"
msgid "Overview"
msgstr "Vue d'ensemble"
msgid "Ports"
msgstr "Ports"
msgid "Power State"
msgstr "État de l'alimentation"
msgid "Power off"
msgstr "Éteindre"
msgid "Power on"
msgstr "Allumer"
msgid "Properties"
msgstr "Propriétés"
msgid ""
"Provide a reason for why you are putting the selected node(s) into "
"maintenance mode (optional)"
msgstr ""
"Fournir une raison pour avoir mis le(s) noeud(s) sélectionné(s) en mode "
"maintenance (optionnel)"
msgid "Provision State"
msgstr "État de déploiement"
msgid "Provisioning State"
msgstr "État de déploiement"
msgid "Provisioning Status"
msgstr "Statut de Déploiement"
msgid "Put Node(s) Into Maintenance Mode"
msgstr "Mettre le(s) noeud(s) en mode maintenance"
msgid "Ramdisk"
msgstr "Ramdisk"
msgid "Refresh page to see updated power status"
msgstr "Rafraichir la page pour voir les statuts d'alimentation à jour"
msgid "Reservation"
msgstr "Réservation"
msgid "SSH Port"
msgstr "Port SSH"
msgid "SSH Username"
msgstr "Utilisateur SSH"
msgid "Target Power State"
msgstr "État de l'alimentation de la cible"
msgid "Target Provision State"
msgstr "État de déploiement de la cible"
msgid "UUID"
msgstr "UUID"
msgid "Unable to retrieve Ironic nodes."
msgstr "Impossible de récupérer les noeud Ironic."
msgid "Updated At"
msgstr "Mis à jour à"

View File

@ -0,0 +1,18 @@
# Andreas Jaeger <jaegerandi@gmail.com>, 2016. #zanata
msgid ""
msgstr ""
"Project-Id-Version: ironic-ui 1.1.1.dev9\n"
"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n"
"POT-Creation-Date: 2016-05-05 12:53+0000\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"PO-Revision-Date: 2016-04-01 04:04+0000\n"
"Last-Translator: Shu Muto <shu-mutou@rf.jp.nec.com>\n"
"Language-Team: Japanese\n"
"Language: ja\n"
"X-Generator: Zanata 3.7.3\n"
"Plural-Forms: nplurals=1; plural=0\n"
msgid "Ironic Bare Metal Provisioning"
msgstr "Ironic ベアメタルプロビジョニング"

View File

@ -0,0 +1,297 @@
# Andreas Jaeger <jaegerandi@gmail.com>, 2016. #zanata
# Shu Muto <shu-mutou@rf.jp.nec.com>, 2016. #zanata
# Yoshiki Eguchi <yoshiki.eguchi@gmail.com>, 2016. #zanata
msgid ""
msgstr ""
"Project-Id-Version: ironic-ui 1.1.1.dev33\n"
"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n"
"POT-Creation-Date: 2016-08-15 22:06+0000\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"PO-Revision-Date: 2016-08-06 02:49+0000\n"
"Last-Translator: Yoshiki Eguchi <yoshiki.eguchi@gmail.com>\n"
"Language-Team: Japanese\n"
"Language: ja\n"
"X-Generator: Zanata 3.7.3\n"
"Plural-Forms: nplurals=1; plural=0\n"
msgid "A unique node name. Optional."
msgstr "一意のノード名。オプション。"
msgid "Actions"
msgstr "アクション"
msgid "Add New Property:"
msgstr "新しいプロパティーの追加:"
#, python-format
msgid ""
"Are you sure you want to delete node \"%s\"? This action cannot be undone."
msgstr "ノード「%s」 を削除してよろしいですか?この操作は取り消せません。"
#, python-format
msgid ""
"Are you sure you want to delete nodes \"%s\"? This action cannot be undone."
msgstr "ノード「%s」 を削除してよろしいですか?この操作は取り消せません。"
msgid "Cancel"
msgstr "取り消し"
msgid "Capabilities"
msgstr "機能"
msgid "Chassis ID"
msgstr "シャーシ ID"
msgid "Choose an Image"
msgstr "イメージの選択"
msgid "Configuration"
msgstr "設定"
msgid "Console Enabled"
msgstr "コンソールの有効化"
msgid "Created At"
msgstr "作成時刻"
msgid "Delete Node"
msgstr "ノードの削除"
msgid "Delete Nodes"
msgstr "ノードの削除"
msgid "Delete node"
msgstr "ノードの削除"
msgid "Delete nodes"
msgstr "ノードの削除"
msgid "Deploy Kernel"
msgstr "カーネルのデプロイ"
msgid "Deploy Ramdisk"
msgstr "RAM ディスクのデプロイ"
msgid "Driver"
msgstr "ドライバー"
msgid "Driver Details"
msgstr "ドライバー詳細"
msgid "Driver Info"
msgstr "ドライバー情報"
msgid "Enroll Node"
msgstr "ノードの登録"
#, python-format
msgid "Error deleting nodes \"%s\""
msgstr "ノード「%s」の削除中にエラーが発生しました"
msgid "Extra"
msgstr "拡張"
msgid "General"
msgstr "一般"
msgid "Inspection Finished At"
msgstr "検査終了時刻"
msgid "Inspection Started At"
msgstr "検査開始時刻"
msgid "Instance ID"
msgstr "インスタンス ID"
msgid "Instance Info"
msgstr "インスタンス情報"
msgid "Instance Name"
msgstr "インスタンス名"
msgid "Kernel"
msgstr "カーネル"
msgid "Last Error"
msgstr "最後のエラー"
msgid "Maintenance"
msgstr "メンテナンス"
msgid "Maintenance Reason"
msgstr "メンテナンスの理由"
msgid "Maintenance off"
msgstr "メンテナンスオフ"
msgid "Maintenance on"
msgstr "メンテナンスオン"
msgid "Name"
msgstr "名前"
msgid "No Instance"
msgstr "インスタンスなし"
msgid "No maintenance reason given."
msgstr "メンテナンスの理由がありません。"
#, python-format
msgid "Node %s is already in maintenance mode."
msgstr "ノード %s は既にメンテナンスモードです。"
#, python-format
msgid "Node %s is not in maintenance mode."
msgstr "ノード %s はメンテナンスモードになっていません。"
#, python-format
msgid "Node %s is not powered off."
msgstr "ノード %s の電源がオフになっていません。"
#, python-format
msgid "Node %s is not powered on."
msgstr "ノード %s の電源がオンになっていません。"
msgid "Node Driver"
msgstr "ノードドライバー"
msgid "Node ID"
msgstr "ノード ID"
msgid "Node Info"
msgstr "ノード情報"
msgid "Node Name"
msgstr "ノード名"
msgid "Overview"
msgstr "概要"
msgid "Ports"
msgstr "ポート"
msgid "Power State"
msgstr "電源状態"
msgid "Power off"
msgstr "電源オフ"
msgid "Power on"
msgstr "電源オン"
msgid "Properties"
msgstr "プロパティー"
msgid "Property Name"
msgstr "プロパティ名"
msgid ""
"Provide a reason for why you are putting the selected node(s) into "
"maintenance mode (optional)"
msgstr ""
"選択したノードをメンテナンスモードにする理由を設定してください(オプション)"
msgid "Provision State"
msgstr "プロビジョニング状態"
msgid "Provisioning State"
msgstr "プロビジョニング状態"
msgid "Provisioning Status"
msgstr "プロビジョニング状態"
msgid "Put Node(s) Into Maintenance Mode"
msgstr "ノードをメンテナンスモードにします"
msgid "Ramdisk"
msgstr "RAM ディスク"
msgid "Refresh page to see updated power status"
msgstr "電源状態を更新するにはページをリフレッシュしてください"
msgid "Required"
msgstr "必須"
msgid "Reservation"
msgstr "予約"
msgid "SSH Port"
msgstr "SSH ポート"
msgid "SSH Username"
msgstr "SSH ユーザー名"
msgid "Select a Driver"
msgstr "ドライバーを選択してください"
#, python-format
msgid "Successfully deleted node \"%s\""
msgstr "ノード「%s」を正常に削除しました"
#, python-format
msgid "Successfully deleted nodes \"%s\""
msgstr "ノード「%s」を正常に削除しました"
msgid "Target Power State"
msgstr "ターゲット電源状態"
msgid "Target Provision State"
msgstr "ターゲットプロビジョニング状態"
msgid "UUID"
msgstr "UUID"
#, python-format
msgid "Unable to create node: %s"
msgstr "ノードを作成できません: %s"
#, python-format
msgid "Unable to delete node \"%s\""
msgstr "ノード 「%s」を削除できません"
#, python-format
msgid "Unable to delete node %s: %s"
msgstr "ノード %s を削除できません: %s"
#, python-format
msgid "Unable to power off the node: %s"
msgstr "ードを電源OFFにできません: %s"
#, python-format
msgid "Unable to power on the node: %s"
msgstr "ノードを電源オンにできません: %s"
#, python-format
msgid "Unable to put the Ironic node in maintenance mode: %s"
msgstr "Ironic ノードをメンテナンスモードにできません: %s"
#, python-format
msgid "Unable to remove the Ironic node from maintenance mode: %s"
msgstr "Ironic ノードをメンテナンスモードから削除できません: %s"
#, python-format
msgid "Unable to retrieve Ironic drivers: %s"
msgstr "Ironic ドライバーの一覧を取得できません: %s"
msgid "Unable to retrieve Ironic nodes."
msgstr "Ironic ノードの一覧を取得できません"
#, python-format
msgid "Unable to retrieve driver properties: %s"
msgstr "ドライバープロパティーの一覧を取得できません: %s"
#, python-format
msgid "Unable to retrieve the Ironic node ports: %s"
msgstr "Ironic ノードのポート一覧を取得できません: %s"
#, python-format
msgid "Unable to retrieve the Ironic node: %s"
msgstr "Ironic ノードを取得できません: %s"
msgid "Updated At"
msgstr "最終更新"
msgid "{$ property.getDescription() $}"
msgstr "{$ property.getDescription() $}"

View File

@ -0,0 +1,18 @@
# Shengjing Zhu <zsj950618@gmail.com>, 2016. #zanata
msgid ""
msgstr ""
"Project-Id-Version: ironic-ui 1.1.1.dev33\n"
"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n"
"POT-Creation-Date: 2016-08-15 22:06+0000\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"PO-Revision-Date: 2016-08-09 08:49+0000\n"
"Last-Translator: Shengjing Zhu <zsj950618@gmail.com>\n"
"Language-Team: Chinese (China)\n"
"Language: zh-CN\n"
"X-Generator: Zanata 3.7.3\n"
"Plural-Forms: nplurals=1; plural=0\n"
msgid "Ironic Bare Metal Provisioning"
msgstr "裸金属 Ironic 配置向导"

View File

@ -0,0 +1,323 @@
# OpenStack Infra <zanata@openstack.org>, 2015. #zanata
# zhangjingwen <zhangjingwen@cn.fujitsu.com>, 2015. #zanata
# Andreas Jaeger <jaegerandi@gmail.com>, 2016. #zanata
# Shengjing Zhu <zsj950618@gmail.com>, 2016. #zanata
# sunanchen <KF.sunanchen@h3c.com>, 2016. #zanata
# vuuv <froms2008@gmail.com>, 2016. #zanata
msgid ""
msgstr ""
"Project-Id-Version: ironic-ui 1.1.1.dev33\n"
"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n"
"POT-Creation-Date: 2016-08-15 22:06+0000\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"PO-Revision-Date: 2016-08-09 08:52+0000\n"
"Last-Translator: Shengjing Zhu <zsj950618@gmail.com>\n"
"Language-Team: Chinese (China)\n"
"Language: zh-CN\n"
"X-Generator: Zanata 3.7.3\n"
"Plural-Forms: nplurals=1; plural=0\n"
msgid " ([^\" ]+|\"[^\"]+\") \\(Default\\)"
msgstr "([^\" ]+|\"[^\"]+\") \\(Default标签\\)"
msgid "(?:[Oo]ne of )(?!this)((?:(?:\"[^\"]+\"|[^,\\. ]+)(?:, |\\.))+)"
msgstr "(?:[Oo]ne of )(?!this)((?:(?:\"[^\"]+\"|[^,\\. ]+)(?:, |\\.))+)"
msgid "A unique node name. Optional."
msgstr "独一无二的节点名称。可选。"
msgid "Actions"
msgstr "动作"
msgid "Add Extra:"
msgstr "增加额外信息"
msgid "Add New Property:"
msgstr "增加新的属性"
#, python-format
msgid ""
"Are you sure you want to delete node \"%s\"? This action cannot be undone."
msgstr "你确认要删除节点\"%s\"嘛?此操作将不可恢复"
#, python-format
msgid ""
"Are you sure you want to delete nodes \"%s\"? This action cannot be undone."
msgstr "你确认要删除节点\"%s\"嘛?此操作将不可恢复"
msgid "Cancel"
msgstr "取消"
msgid "Capabilities"
msgstr "能力"
msgid "Chassis ID"
msgstr "机架ID"
msgid "Choose an Image"
msgstr "选择一个镜像"
msgid "Configuration"
msgstr "配置"
msgid "Console Enabled"
msgstr "允许控制台"
msgid "Created At"
msgstr "创建于"
msgid "Defaults to ([^\"\\. ]+|\"[^\"]+\")"
msgstr "默认为 ([^\"\\. ]+|\"[^\"]+\")"
msgid "Delete Node"
msgstr "删除节点"
msgid "Delete Nodes"
msgstr "删除多个节点"
msgid "Delete node"
msgstr "删除节点"
msgid "Delete nodes"
msgstr "删除多个节点"
msgid "Deploy Kernel"
msgstr "部署内核"
msgid "Deploy Ramdisk"
msgstr "部署虚拟内存盘"
msgid "Driver"
msgstr "驱动"
msgid "Driver Details"
msgstr "驱动详情"
msgid "Driver Info"
msgstr "驱动信息"
msgid "Enroll Node"
msgstr "注册节点"
#, python-format
msgid "Error deleting nodes \"%s\""
msgstr "删除多个节点\"%s\"错误"
msgid "Extra"
msgstr "额外信息"
msgid "Extra Property Name"
msgstr "额外属性名称"
msgid "Extras"
msgstr "额外信息"
msgid "General"
msgstr "概要信息"
msgid "Inspection Finished At"
msgstr "检查结束于"
msgid "Inspection Started At"
msgstr "检查开始于"
msgid "Instance ID"
msgstr "云主机ID"
msgid "Instance Info"
msgstr "实例信息"
msgid "Instance Name"
msgstr "实例名字"
msgid "Kernel"
msgstr "内核"
msgid "Last Error"
msgstr "最近的一次错误"
msgid "Maintenance"
msgstr "维护"
msgid "Maintenance Reason"
msgstr "维护原因"
msgid "Maintenance off"
msgstr "退出维护模式"
msgid "Maintenance on"
msgstr "处于维护状态"
msgid "Name"
msgstr "名称"
msgid "No Instance"
msgstr "没有实例"
msgid "No maintenance reason given."
msgstr "缺少提供维护原因"
#, python-format
msgid "Node %s is already in maintenance mode."
msgstr "节点\"%s\"已经处于维护模式"
#, python-format
msgid "Node %s is not in maintenance mode."
msgstr "节点\"%s\"没有处于维护模式"
#, python-format
msgid "Node %s is not powered off."
msgstr "节点\"%s\"电源没有关闭"
#, python-format
msgid "Node %s is not powered on."
msgstr "节点\"%s\"电源没有开启"
msgid "Node Driver"
msgstr "节点驱动"
msgid "Node ID"
msgstr "节点ID"
msgid "Node Info"
msgstr "节点信息"
msgid "Node Name"
msgstr "节点名称"
msgid "One of this, (.*) must be specified\\."
msgstr "必须指定其中的一个 (.*)"
msgid "Overview"
msgstr "概览"
msgid "Ports"
msgstr "端口"
msgid "Power State"
msgstr "电源状态"
msgid "Power off"
msgstr "关闭电源"
msgid "Power on"
msgstr "打开电源"
msgid "Properties"
msgstr "属性"
msgid "Property Name"
msgstr "属性名称"
msgid ""
"Provide a reason for why you are putting the selected node(s) into "
"maintenance mode (optional)"
msgstr "提供一个你为什么选择将节点置于维护模式的原因(可选)"
msgid "Provision State"
msgstr "配置状态"
msgid "Provisioning State"
msgstr "配置状态"
msgid "Provisioning Status"
msgstr "配置状态"
msgid "Put Node(s) Into Maintenance Mode"
msgstr "将节点置于维护模式"
msgid "Ramdisk"
msgstr "内存盘"
msgid "Refresh page to see updated power status"
msgstr "刷新页面来确认电源状态是否更新"
msgid "Required"
msgstr "必需的"
msgid "Reservation"
msgstr "预留"
msgid "SSH Port"
msgstr "SSH端口"
msgid "SSH Username"
msgstr "SSH用户名"
msgid "Select a Driver"
msgstr "选择一种驱动"
#, python-format
msgid "Successfully deleted node \"%s\""
msgstr "成功删除节点\"%s\""
#, python-format
msgid "Successfully deleted nodes \"%s\""
msgstr "成功删除多个节点\"%s\""
msgid "Target Power State"
msgstr "标记电源状态"
msgid "Target Provision State"
msgstr "标记配置状态"
msgid "UUID"
msgstr "UUID"
#, python-format
msgid "Unable to create node: %s"
msgstr "无法创建Ironic节点: %s"
#, python-format
msgid "Unable to delete node \"%s\""
msgstr "无法删除节点\"%s\""
#, python-format
msgid "Unable to delete node %s: %s"
msgstr "无法删除Ironic节点\"%s\": %s"
#, python-format
msgid "Unable to power off the node: %s"
msgstr "无法关闭节点电源: %s"
#, python-format
msgid "Unable to power on the node: %s"
msgstr "无法启动节点电源: %s"
#, python-format
msgid "Unable to put the Ironic node in maintenance mode: %s"
msgstr "无法将Ironic节点置于维护模式: %s"
#, python-format
msgid "Unable to remove the Ironic node from maintenance mode: %s"
msgstr "无法将Ironic节点退出维护模式: %s"
#, python-format
msgid "Unable to retrieve Ironic drivers: %s"
msgstr "无法获取Ironic驱动: %s"
msgid "Unable to retrieve Ironic nodes."
msgstr "无法获取Ironic节点信息"
#, python-format
msgid "Unable to retrieve driver properties: %s"
msgstr "无法获取驱动属性: %s"
#, python-format
msgid "Unable to retrieve the Ironic node ports: %s"
msgstr "无法获取Ironic节点端口信息: %s"
#, python-format
msgid "Unable to retrieve the Ironic node: %s"
msgstr "无法获取Ironic节点信息: %s"
msgid "Updated At"
msgstr "已更新于"
msgid "default (?:value )?is ([^\"\\. ]+|\"[^\"]+\")"
msgstr "默认值 (?:value )?是 ([^\"\\. ]+|\"[^\"]+\")"
msgid "{$ property.getDescription() $}"
msgstr "{$ property.getDescription() $}"

View File

@ -0,0 +1,35 @@
/*
* Copyright 2016 Cray 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.
*/
(function () {
'use strict';
angular
.module('horizon.dashboard.admin.ironic')
.directive('autoFocus', AutoFocus);
AutoFocus.$inject = ['$timeout'];
function AutoFocus($timeout) {
return {
restrict: 'AC',
link: function(scope, elem) {
$timeout(function() {
elem[0].focus();
}, 1000);
}
};
}
})();

View File

@ -0,0 +1,95 @@
/*
* Copyright 2016 Cray 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.
*/
(function() {
'use strict';
/**
* Controller used to create a network port on a specified node
*/
angular
.module('horizon.dashboard.admin.ironic')
.controller('CreatePortController', CreatePortController);
CreatePortController.$inject = [
'$rootScope',
'$modalInstance',
'horizon.app.core.openstack-service-api.ironic',
'horizon.dashboard.admin.ironic.events',
'$log',
'node'
];
function CreatePortController($rootScope,
$modalInstance,
ironic,
ironicEvents,
$log,
node) {
var ctrl = this;
// Paramater object that defines the port to be created
ctrl.port = {
node_uuid: node.id,
address: null,
extra: {}
};
/**
* Cancel the port creation process
*
* @return {void}
*/
ctrl.cancel = function() {
$modalInstance.dismiss('cancel');
};
/**
* Create the defined port
*
* @return {void}
*/
ctrl.createPort = function() {
ironic.createPort(ctrl.port).then(
function() {
$modalInstance.close();
$rootScope.$emit(ironicEvents.CREATE_PORT_SUCCESS);
},
function() {
});
};
/**
* Delete a port metadata property
*
* @param {string} propertyName - Name of the property
* @return {void}
*/
ctrl.deleteExtra = function(propertyName) {
delete ctrl.port.extra[propertyName];
};
/**
* Check whether the specified port metadata property already exists
*
* @param {string} propertyName - Name of the metadata property
* @return {boolean} True if the property already exists,
* otherwise false
*/
ctrl.checkExtraUnique = function(propertyName) {
return !(propertyName in ctrl.port.extra);
};
}
})();

View File

@ -0,0 +1,90 @@
<div class="modal-header" modal-draggable>
<button type="button"
class="close"
ng-click="$dismiss()"
aria-hidden="true"
aria-label="Close">
<span aria-hidden="true" class="fa fa-times"></span>
</button>
<h3 class="modal-title" translate>Create Port</h3>
</div>
<div class="modal-body">
<form id="CreatePortForm" name="CreatePortForm">
<div class="form-group"
ng-class="{'has-error': CreatePortForm.macAddress.$invalid &&
CreatePortForm.macAddress.$dirty}">
<label for="macAddress"
class="control-label"
translate>MAC address</label>
<div>
<input type="text"
class="form-control"
ng-model="ctrl.port.address"
id="macAddress"
name="macAddress"
ng-required="true"
ng-pattern="'([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})'"
auto-focus
placeholder="{$ 'MAC address for this port. Required.' | translate $}"/>
</div>
</div>
</form>
<form id="AddExtraForm" name="AddExtraForm" style="margin-bottom:10px;">
<label for="extras" class="control-label" translate>Extras</label>
<div class="input-group input-group-sm">
<span class="input-group-addon"
style="width:25%;text-align:right">
Add Extra:</span>
<input class="form-control"
type="text"
ng-model="extraName"
validate-unique="ctrl.checkExtraUnique"
placeholder="{$ 'Property Name' | translate $}"/>
<span class="input-group-btn">
<button class="btn btn-primary"
type="button"
ng-disabled="!extraName || AddExtraForm.$invalid"
ng-click="ctrl.port.extra[extraName] = null;
extraName = null">
<span class="fa fa-plus"> </span>
</button>
</span>
</div>
</form>
<form class="form-horizontal" id="ExtraForm" name="ExtraForm">
<div class="input-group input-group-sm"
ng-repeat="(propertyName, propertyValue) in ctrl.port.extra">
<span class="input-group-addon"
style="width:25%;text-align:right">
{$ propertyName $}
</span>
<input class="form-control"
type="text"
name="{$ propertyName $}"
ng-model="ctrl.port.extra[propertyName]"
ng-required="true"/>
<div class="input-group-btn">
<a class="btn btn-default"
ng-click="ctrl.deleteExtra(propertyName)">
<span class="fa fa-minus"> </span>
</a>
</div>
</div>
</form>
</div>
<!--modal footer-->
<div class="modal-footer ng-scope">
<button class="btn btn-default" ng-click="ctrl.cancel()">
<span class="fa fa-close"></span>
<span class="ng-scope" translate>Cancel</span>
</button>
<button type="submit"
ng-disabled="CreatePortForm.$invalid || ExtraForm.$invalid"
ng-click="ctrl.createPort()"
class="btn btn-primary"
translate>
Create Port
</button>
</div>

View File

@ -0,0 +1,49 @@
/*
* Copyright 2016 Cray 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.
*/
(function() {
'use strict';
angular
.module('horizon.dashboard.admin.ironic')
.factory('horizon.dashboard.admin.ironic.create-port.service',
createPortService);
createPortService.$inject = [
'$modal',
'horizon.dashboard.admin.basePath'
];
function createPortService($modal, basePath) {
var service = {
modal: modal
};
return service;
function modal(node) {
var options = {
controller: 'CreatePortController as ctrl',
backdrop: 'static',
resolve: {
node: function() {
return node;
}
},
templateUrl: basePath + '/ironic/create-port/create-port.html'
};
return $modal.open(options);
}
}
})();

View File

@ -0,0 +1,37 @@
/*
* Copyright 2016 Cray 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.
*/
(function () {
'use strict';
angular
.module('horizon.dashboard.admin.ironic')
.directive('emptyToPristine', EmptyToPristine);
function EmptyToPristine() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elem, attrs, ctrl) {
ctrl.$parsers.push(function(viewValue) {
if (viewValue === "") {
ctrl.$setPristine();
}
return viewValue;
});
}
};
}
})();

View File

@ -0,0 +1,343 @@
/*
* Copyright 2016 Cray 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.
*/
(function() {
'use strict';
/**
* Controller used to enroll a node in the Ironic database
*/
angular
.module('horizon.dashboard.admin.ironic')
.controller('EnrollNodeController', EnrollNodeController);
EnrollNodeController.$inject = [
'$rootScope',
'$modalInstance',
'horizon.app.core.openstack-service-api.ironic',
'horizon.dashboard.admin.ironic.events',
'horizon.app.core.openstack-service-api.glance',
'horizon.dashboard.admin.ironic.enroll-node.service',
'horizon.dashboard.admin.ironic.validHostNamePattern',
'$log'
];
function EnrollNodeController($rootScope,
$modalInstance,
ironic,
ironicEvents,
glance,
enrollNodeService,
validHostNamePattern,
$log) {
var ctrl = this;
ctrl.validHostNameRegex = new RegExp(validHostNamePattern);
ctrl.drivers = null;
ctrl.images = null;
ctrl.loadingDriverProperties = false;
// Object containing the set of properties associated with the currently
// selected driver
ctrl.driverProperties = null;
ctrl.driverPropertyGroups = null;
// Parameter object that defines the node to be enrolled
ctrl.node = {
name: null,
driver: null,
driver_info: {},
properties: {},
extra: {}
};
init();
function init() {
loadDrivers();
getImages();
}
/**
* @description Get the list of currently active Ironic drivers
*
* @return {void}
*/
function loadDrivers() {
ironic.getDrivers().then(function(response) {
ctrl.drivers = response.data.items;
});
}
/**
* @description Get the list of images from Glance
*
* @return {void}
*/
function getImages() {
glance.getImages().then(function(response) {
ctrl.images = response.data.items;
});
}
/**
* @description Check whether a group contains required properties
*
* @param {DriverProperty[]} group - Property group
* @return {boolean} Return true if the group contains required
* properties, false otherwise
*/
function driverPropertyGroupHasRequired(group) {
var hasRequired = false;
for (var i = 0; i < group.length; i++) {
if (group[i].required) {
hasRequired = true;
break;
}
}
return hasRequired;
}
/**
* @description Convert array of driver property groups to a string
*
* @param {array[]} groups - Array for driver property groups
* @return {string} Output string
*/
function driverPropertyGroupsToString(groups) {
var output = [];
angular.forEach(groups, function(group) {
var groupStr = [];
angular.forEach(group, function(property) {
groupStr.push(property.name);
});
groupStr = groupStr.join(", ");
output.push(['[', groupStr, ']'].join(""));
});
output = output.join(", ");
return ['[', output, ']'].join("");
}
/**
* @description Comaprison function used to sort driver property groups
*
* @param {DriverProperty[]} group1 - First group
* @param {DriverProperty[]} group2 - Second group
* @return {integer} Return:
* < 0 if group1 should precede group2 in an ascending ordering
* > 0 if group2 should precede group1
* 0 if group1 and group2 are considered equal from ordering perpsective
*/
function compareDriverPropertyGroups(group1, group2) {
var group1HasRequired = driverPropertyGroupHasRequired(group1);
var group2HasRequired = driverPropertyGroupHasRequired(group2);
if (group1HasRequired === group2HasRequired) {
if (group1.length === group2.length) {
return group1[0].name.localeCompare(group2[0].name);
} else {
return group1.length - group2.length;
}
} else {
return group1HasRequired ? -1 : 1;
}
return 0;
}
/**
* @description Order driver properties in the form using the following
* rules:
*
* (1) Properties that are related to one another should occupy adjacent
* locations in the form
*
* (2) Required properties with no dependents should be located at the
* top of the form
*
* @return {void}
*/
ctrl._sortDriverProperties = function() {
// Build dependency graph between driver properties
var graph = new enrollNodeService.Graph();
// Create vertices
angular.forEach(ctrl.driverProperties, function(property, name) {
graph.addVertex(name, property);
});
/* eslint-disable no-unused-vars */
// Create edges
angular.forEach(ctrl.driverProperties,
function(property, name) {
var activators = property.getActivators();
if (activators) {
angular.forEach(activators,
function(unused, activatorName) {
graph.addEdge(name, activatorName);
});
}
});
/* eslint-enable no-unused-vars */
// Perform depth-first-search to find groups of related properties
var groups = [];
graph.dfs(
function(vertexList, components) {
// Sort properties so that those with the largest number of
// immediate dependents are the top of the list
vertexList.sort(function(vertex1, vertex2) {
return vertex2.adjacents.length - vertex1.adjacents.length;
});
// Build component and add to list
var component = new Array(vertexList.length);
angular.forEach(vertexList, function(vertex, index) {
component[index] = vertex.data;
});
components.push(component);
},
groups);
groups.sort(compareDriverPropertyGroups);
$log.debug("Found the following property groups: " +
driverPropertyGroupsToString(groups));
return groups;
};
/**
* @description Get the properties associated with a specified driver
*
* @param {string} driverName - Name of driver
* @return {void}
*/
ctrl.loadDriverProperties = function(driverName) {
ctrl.node.driver = driverName;
ctrl.node.driver_info = {};
ctrl.loadingDriverProperties = true;
ctrl.driverProperties = null;
ctrl.driverPropertyGroups = null;
ironic.getDriverProperties(driverName).then(function(response) {
ctrl.driverProperties = {};
angular.forEach(response.data, function(desc, property) {
ctrl.driverProperties[property] =
new enrollNodeService.DriverProperty(property,
desc,
ctrl.driverProperties);
});
ctrl.driverPropertyGroups = ctrl._sortDriverProperties();
ctrl.loadingDriverProperties = false;
});
};
/**
* @description Cancel the node enrollment process
*
* @return {void}
*/
ctrl.cancel = function() {
$modalInstance.dismiss('cancel');
};
/**
* @description Enroll the defined node
*
* @return {void}
*/
ctrl.enroll = function() {
$log.debug(">> EnrollNodeController.enroll()");
angular.forEach(ctrl.driverProperties, function(property, name) {
$log.debug(name +
", required = " + property.isRequired() +
", active = " + property.isActive() +
", input-value = " + property.getInputValue() +
", default-value = " + property.getDefaultValue());
if (property.isActive() &&
property.getInputValue() &&
property.getInputValue() !== property.getDefaultValue()) {
$log.debug("Setting driver property " + name + " to " +
property.inputValue);
ctrl.node.driver_info[name] = property.inputValue;
}
});
ironic.createNode(ctrl.node).then(
function() {
$modalInstance.close();
$rootScope.$emit(ironicEvents.ENROLL_NODE_SUCCESS);
},
function() {
// No additional error processing for now
});
$log.debug("<< EnrollNodeController.enroll()");
};
/**
* @desription Delete a node property
*
* @param {string} propertyName - Name of the property
* @return {void}
*/
ctrl.deleteProperty = function(propertyName) {
delete ctrl.node.properties[propertyName];
};
/**
* @description Check whether the specified node property already exists
*
* @param {string} propertyName - Name of the property
* @return {boolean} True if the property already exists,
* otherwise false
*/
ctrl.checkPropertyUnique = function(propertyName) {
return !(propertyName in ctrl.node.properties);
};
/**
* @description Delete a node metadata property
*
* @param {string} propertyName - Name of the property
* @return {void}
*/
ctrl.deleteExtra = function(propertyName) {
delete ctrl.node.extra[propertyName];
};
/**
* @description Check whether the specified node metadata property
* already exists
*
* @param {string} propertyName - Name of the metadata property
* @return {boolean} True if the property already exists,
* otherwise false
*/
ctrl.checkExtraUnique = function(propertyName) {
return !(propertyName in ctrl.node.extra);
};
/**
* @description Check whether a specified driver property is
* currently active
*
* @param {string} property - Driver property
* @return {boolean} True if the property is active, false otherwise
*/
ctrl.isDriverPropertyActive = function(property) {
return property.isActive();
};
}
})();

View File

@ -0,0 +1,278 @@
<div class="modal-header" modal-draggable>
<button type="button"
class="close"
ng-click="$dismiss()"
aria-hidden="true"
aria-label="Close">
<span aria-hidden="true" class="fa fa-times"></span>
</button>
<h3 class="modal-title" translate>Enroll Node</h3>
</div>
<!-- begin general node info modal -->
<div class="modal-body">
<div class="tabbable"> <!-- Only required for left/right tabs -->
<ul class="nav nav-tabs">
<li class="required active">
<a href=""
data-target="#nodeInfo"
data-toggle="tab"
translate>Node Info</a></li>
<li ng-if="!ctrl.driverProperties"
class="disabled">
<a data-target="#driverDetails"
translate>Driver Details</a></li>
<li ng-if="ctrl.driverProperties">
<a href=""
data-target="#driverDetails"
data-toggle="tab"
translate>Driver Details</a></li>
</ul>
<!--enroll node form-->
<form id="enrollNodeForm"
name="enrollNodeForm">
<!--tabbed content-->
<div class="tab-content">
<!-- node info tab-->
<div class="tab-pane active" id="nodeInfo">
<!--node name-->
<div class="form-group"
ng-class="{'has-error': enrollNodeForm.name.$invalid &&
enrollNodeForm.name.$dirty}">
<label for="name"
class="control-label"
translate>Node Name</label>
<div>
<input type="text"
class="form-control"
ng-model="ctrl.node.name"
id="name"
name="name"
ng-pattern="ctrl.validHostNameRegex"
placeholder="{$ 'A unique node name. Optional.' | translate $}"/>
</div>
</div>
<!--node driver-->
<div class="form-group required">
<label for="driver"
class="control-label"
translate>Node Driver</label>
<span class="hz-icon-required fa fa-asterisk"></span>
<div>
<select id="driver"
class="form-control"
ng-options="driver as driver.name for driver in ctrl.drivers"
ng-model="ctrl.selectedDriver"
ng-change="ctrl.loadDriverProperties(ctrl.selectedDriver.name)">
<option value="" disabled selected translate>Select a Driver</option>
</select>
</div>
</div>
<!--properties-->
<div class="form-group">
<label for="properties"
class="control-label"
translate>Properties</label>
<div class="input-group input-group-sm">
<span class="input-group-addon"
style="width:25%;text-align:right"
translate>
Add New Property:</span>
<input class="form-control"
id="properties"
type="text"
ng-model="propertyName"
validate-unique="ctrl.checkPropertyUnique"
placeholder="{$ 'Property Name' | translate $}"/>
<span class="input-group-btn">
<button class="btn btn-primary"
type="button"
ng-disabled="!propertyName || AddPropertyForm.$invalid"
ng-click="ctrl.node.properties[propertyName] = null;
propertyName = null">
<span class="fa fa-plus"> </span>
</button>
</span>
</div>
</div>
<!--properties list-->
<div class="form-group">
<div class="input-group input-group-sm"
ng-repeat="(propertyName, propertyValue) in ctrl.node.properties">
<span class="input-group-addon"
style="width:25%;text-align:right">
{$ propertyName $}
</span>
<input class="form-control"
type="text"
name="{$ propertyName $}"
ng-model="ctrl.node.properties[propertyName]"
ng-required="true"/>
<div class="input-group-btn">
<a class="btn btn-default"
ng-click="ctrl.deleteProperty(propertyName)">
<span class="fa fa-minus"> </span>
</a>
</div>
</div>
</div>
<!--extras-->
<div class="form-group">
<label for="extras"
class="control-label"
translate>Extras</label>
<div class="input-group input-group-sm">
<span class="input-group-addon"
style="width:25%;text-align:right"
translate>
Add Extra:</span>
<input class="form-control"
id="extras"
type="text"
ng-model="extraName"
validate-unique="ctrl.checkExtraUnique"
placeholder="{$ 'Extra Property Name' | translate $}"/>
<span class="input-group-btn">
<button class="btn btn-primary"
type="button"
ng-disabled="!extraName || AddExtraForm.$invalid"
ng-click="ctrl.node.extra[extraName] = null;
extraName = null">
<span class="fa fa-plus"> </span>
</button>
</span>
</div>
</div>
<!--extras list-->
<div class="input-group input-group-sm"
ng-repeat="(propertyName, propertyValue) in ctrl.node.extra">
<span class="input-group-addon"
style="width:25%;text-align:right">
{$ propertyName $}
</span>
<input class="form-control"
type="text"
name="{$ propertyName $}"
ng-model="ctrl.node.extra[propertyName]"
ng-required="true"/>
<div class="input-group-btn">
<a class="btn btn-default"
ng-click="ctrl.deleteExtra(propertyName)">
<span class="fa fa-minus"> </span>
</a>
</div>
</div>
</div>
<!--end node info tab-->
<!--driver details tab-->
<div class="tab-pane" id="driverDetails">
<p class="text-center"
ng-if="ctrl.loadingDriverProperties">
<small><em><i class="fa fa-spin fa-refresh"></i></em></small>
</p>
<div ng-repeat="propertyGroup in ctrl.driverPropertyGroups"
ng-class="{'well': propertyGroup.length > 1}">
<div class="form-group"
ng-repeat="property in propertyGroup | filter:ctrl.isDriverPropertyActive"
ng-init="name = property.name;
selectOptions = property.getSelectOptions()"
ng-class="{'has-error': enrollNodeForm.{$ name $}.$invalid &&
enrollNodeForm.{$ name $}.$dirty}">
<label for="{$ name $}"
class="control-label"
style="white-space: nowrap">
{$ name $}
<span ng-if="property.isRequired()"
class="hz-icon-required fa fa-asterisk"></span>
<span class="help-icon"
data-container="body"
title=""
data-toggle="tooltip"
data-original-title="{$ property.getDescription() | translate $}">
<span class="fa fa-question-circle"></span>
</span>
</label>
<div ng-if="!selectOptions"
ng-class="{'input-group': name === 'deploy_kernel' ||
name === 'deploy_ramdisk'}">
<input type="text"
class="form-control"
id="{$ name $}"
name="{$ name $}"
ng-model="property.inputValue"
ng-pattern="property.getValidValueRegex()"
placeholder="{$ property.defaultValue !== undefined ?
property.defaultValue :
property.getDescription() $}"
ng-required="property.isRequired()"
empty-to-pristine/>
<div ng-if="name === 'deploy_kernel' ||
name === 'deploy_ramdisk'"
class="input-group-btn">
<button type="button"
class="btn btn-primary dropdown-toggle"
data-toggle="dropdown"
translate>
Choose an Image
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li>
<a class="dropdown-item"
ng-repeat="imageObj in ctrl.images"
href="#"
ng-click="property.inputValue = imageObj.id">{$ imageObj.name + ' [' + imageObj.id + ']' $}</a>
</li>
</ul>
</div>
</div>
<div ng-if="selectOptions" class="">
<select ng-if="selectOptions.length > 4"
id="{$ name $}"
class="form-control"
ng-options="opt for opt in selectOptions"
ng-model="property.inputValue"
ng-required="property.isRequired()">
<option ng-if="property.defaultValue === undefined"
value=""
disabled
selected
translate>{$ property.getDescription() $}</option>
</select>
<div ng-if="selectOptions.length <= 4"
class="btn-group">
<label class="btn btn-default"
ng-repeat="opt in selectOptions"
ng-model="property.inputValue"
btn-radio="opt">{$ opt $}</label>
</div>
</div>
</div>
</div>
</div>
<!--end driver details tab-->
</div>
<!--end tabbed content-->
</form>
<!--end enroll node form-->
</div>
</div>
<!--modal footer-->
<div class="modal-footer ng-scope">
<button class="btn btn-default"
ng-click="ctrl.cancel()">
<span class="fa fa-close"></span>
<span class="ng-scope" translate>Cancel</span>
</button>
<button type="submit"
ng-disabled="!ctrl.driverProperties ||
enrollNodeForm.$invalid"
ng-click="ctrl.enroll()"
class="btn btn-primary"
translate>
Enroll Node
</button>
</div>

View File

@ -0,0 +1,685 @@
/*
* Copyright 2016 Cray 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.
*/
(function() {
'use strict';
var REQUIRED = " " + gettext("Required") + ".";
var SELECT_OPTIONS_REGEX =
new RegExp(
gettext('(?:[Oo]ne of )(?!this)((?:(?:"[^"]+"|[^,\\. ]+)(?:, |\\.))+)'));
var DEFAULT_IS_REGEX =
new RegExp(gettext('default (?:value )?is ([^"\\. ]+|"[^"]+")'));
var DEFAULTS_TO_REGEX =
new RegExp(gettext('Defaults to ([^"\\. ]+|"[^"]+")'));
var DEFAULT_IN_PARENS_REGEX =
new RegExp(gettext(' ([^" ]+|"[^"]+") \\(Default\\)'));
var DEFAULT_REGEX_LIST = [DEFAULT_IS_REGEX,
DEFAULTS_TO_REGEX,
DEFAULT_IN_PARENS_REGEX];
var ONE_OF_REGEX =
new RegExp(gettext('One of this, (.*) must be specified\\.'));
var NOT_INSIDE_MATCH = -1;
var VALID_PORT_REGEX = new RegExp('^\\d+$');
var VALID_IPV4_ADDRESS = "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"; // eslint-disable-line max-len
var VALID_IPV6_ADDRESS = "^\\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?\\s*$"; // eslint-disable-line max-len
angular
.module('horizon.dashboard.admin.ironic')
.factory('horizon.dashboard.admin.ironic.enroll-node.service',
enrollNodeService);
enrollNodeService.$inject = [
'$modal',
'horizon.dashboard.admin.basePath',
'$log',
'horizon.dashboard.admin.ironic.validHostNamePattern',
'horizon.dashboard.admin.ironic.validUuidPattern'
];
function enrollNodeService($modal,
basePath,
$log,
validHostNamePattern,
validUuidPattern) {
var service = {
modal: modal,
DriverProperty: DriverProperty,
Graph: Graph
};
var VALID_ADDRESS_HOSTNAME_REGEX = new RegExp(VALID_IPV4_ADDRESS + "|" +
VALID_IPV6_ADDRESS + "|" +
validHostNamePattern);
var VALID_IMAGE_REGEX = new RegExp(validUuidPattern + "|" +
"^(https?|file)://.+$");
function modal() {
var options = {
controller: 'EnrollNodeController as ctrl',
backdrop: 'static',
templateUrl: basePath + '/ironic/enroll-node/enroll-node.html'
};
return $modal.open(options);
}
/**
The DriverProperty class is used to represent an ironic driver
property. It is currently used by the enroll-node form to
support property display, value assignment and validation.
The following rules are used to extract information about a property
from the description returned by the driver.
1. If the description ends with " Required." a value must be
supplied for the property.
2. The following syntax is used to extract default values
from property descriptions.
Default is <value>(<space>|.)
default is <value>
default value is <value>(<space>|.)
default value is <value>
Defaults to <value>(<space>|.)
Defaults to <value>
<value> (Default)
3. The following syntax is used to determine whether a property
is considered active. In the example below if the user specifies
a value for <property-name-1>, properties 2 to n will be tagged
inactive, and hidden from view. All properties are considered
to be required.
One of this, <property-name-1>, <property-name-2>, , or
<property-name-n> must be specified.
4. The following syntax is used to determine whether a property
is restricted to a set of enumerated values. The property will
be displayed as an HTML select element.
[Oo]ne of <value-1>, "<value-2>", , <value-n>.
5. The following syntax is used to determine whether a property is
active and required based on the value of another property.
If the property is not active it will not be displayed.
Required|Used only if <property-name> is set to <value-1>
(or "<value-2>")*.
Notes:
1. The properties "deploy_kernel" and "deploy_ramdisk" are
assumed to accept Glance image uuids as valid values.
2. Property names ending in _port are assumed to only accept
postive integer values
3. Property names ending in _address are assumed to only accept
valid IPv4 and IPv6 addresses; and hostnames
*/
/**
* @description Construct a new driver property
*
* @class DriverProperty
* @param {string} name - Name of property
* @param {string} desc - Description of property
* @param {object} propertySet - Set of properties to which this one belongs
*
* @property {string} defaultValue - Default value of the property
* @property {string[]} selectOptions - If the property is limited to a
* set of enumerated values then selectOptions will be an array of those
* values, otherwise null
* @property {boolean} required - Boolean value indicating whether a value
* must be supplied for this property if it is active
* @property {PostfixExpr} isActiveExpr - Null if this property is always
* active; otherwise, a boolean expression that when evaluated will
* return whether this variable is active. A property is considered
* active if its role is not eliminated by the values of other
* properties in the property-set.
* @property {string} inputValue - User assigned value for this property
* @property {regexp} validValueRegex - Regular expression used to
* determine whether an input value is valid.
* @returns {object} Driver property
*/
function DriverProperty(name, desc, propertySet) {
this.name = name;
this.desc = desc;
this.propertySet = propertySet;
// Determine whether this property should be presented as a selection
this.selectOptions = this._analyzeSelectOptions();
this.required = null; // Initialize to unknown
// Expression to be evaluated to determine whether property is active.
// By default the property is considered active.
this.isActiveExpr = null;
var result = this._analyzeRequiredOnlyDependencies();
if (result) {
this.required = result[0];
this.isActiveExpr = result[1];
}
if (!this.isActiveExpr) {
result = this._analyzeOneOfDependencies();
if (result) {
this.required = result[0];
this.isActiveExpr = result[1];
}
}
if (this.required === null) {
this.required = desc.endsWith(REQUIRED);
}
this.defaultValue = this._getDefaultValue();
this.inputValue = this.defaultValue;
// Infer that property is a boolean that can be represented as a
// True/False selection
if (this.selectOptions === null &&
(this.defaultValue === "True" || this.defaultValue === "False")) {
this.selectOptions = ["True", "False"];
}
this.validValueRegex = _determineValidValueRegex(this.name);
}
/**
* @description Return a regular expression that can be used to
* validate the value of a specified property
*
* @param {string} propertyName - Name of property
* @return {regexp} Regular expression object or undefined
*/
function _determineValidValueRegex(propertyName) {
var regex;
if (propertyName.endsWith("_port")) {
regex = VALID_PORT_REGEX;
} else if (propertyName.endsWith("_address")) {
regex = VALID_ADDRESS_HOSTNAME_REGEX;
} else if (propertyName === "deploy_kernel") {
regex = VALID_IMAGE_REGEX;
} else if (propertyName === "deploy_ramdisk") {
regex = VALID_IMAGE_REGEX;
}
return regex;
}
DriverProperty.prototype.isActive = function() {
if (!this.isActiveExpr) {
return true;
}
var ret = this.isActiveExpr.evaluate(this.propertySet);
return ret[0] === PostfixExpr.status.OK &&
typeof ret[1] === "boolean" ? ret[1] : true;
};
/**
* @description Get a regular expression object that can be used to
* determine whether a value is valid for this property
*
* @return {regexp} Regular expression object or undefined
*/
DriverProperty.prototype.getValidValueRegex = function() {
return this.validValueRegex;
};
/**
* @description Must a value be provided for this property
*
* @return {boolean} True if a value must be provided for this property
*/
DriverProperty.prototype.isRequired = function() {
return this.required;
};
DriverProperty.prototype._analyzeSelectOptions = function() {
var match = this.desc.match(SELECT_OPTIONS_REGEX);
if (!match) {
return null;
}
var matches = match[1].substring(0, match[1].length - 1).split(", ");
var options = [];
angular.forEach(matches, function(match) {
options.push(trimQuotes(match));
});
return options;
};
/**
* @description Get the list of select options for this property
*
* @return {string[]} null if this property is not selectable; else,
* an array of selectable options
*/
DriverProperty.prototype.getSelectOptions = function() {
return this.selectOptions;
};
/**
* @description Remove leading/trailing double-quotes from a string
*
* @param {string} str - String to be trimmed
* @return {string} trim'd string
*/
function trimQuotes(str) {
return str.charAt(0) === '"'
? str.substring(1, str.length - 1) : str;
}
/**
* @description Get the default value of this property
*
* @return {string} Default value of this property
*/
DriverProperty.prototype._getDefaultValue = function() {
var value;
for (var i = 0; i < DEFAULT_REGEX_LIST.length; i++) {
var match = this.desc.match(DEFAULT_REGEX_LIST[i]);
if (match) {
value = trimQuotes(match[1]);
break;
}
}
$log.debug("_getDefaultValue | " + this.desc + " | " + value);
return value;
};
/**
* @description Get the input value of this property
*
* @return {string} the input value of this property
*/
DriverProperty.prototype.getInputValue = function() {
return this.inputValue;
};
/**
* @description Get the default value of this property
*
* @return {string} the default value of this property
*/
DriverProperty.prototype.getDefaultValue = function() {
return this.defaultValue;
};
/**
* @description Get the description of this property
*
* @return {string} Description of this property
*/
DriverProperty.prototype.getDescription = function() {
return this.desc;
};
/**
* @description Use the property description to build an expression
* that will evaluate to a boolean result indicating whether the
* property is active
*
* @return {array} null if this property is not dependent on any others;
* otherwise,
* [0] boolean indicating whether if active a value must be
* supplied for this property.
* [1] an expression that when evaluated will return a boolean
* result indicating whether this property is active
*/
DriverProperty.prototype._analyzeRequiredOnlyDependencies = function() {
var re = /(Required|Used) only if ([^ ]+) is set to /g;
var match = re.exec(this.desc);
if (!match) {
return null;
}
// Build logical expression to describe under what conditions this
// property is active
var expr = new PostfixExpr();
var numAdds = 0;
var i = NOT_INSIDE_MATCH;
var j = re.lastIndex;
while (j < this.desc.length) {
if (i === NOT_INSIDE_MATCH && this.desc.charAt(j) === ".") {
break;
}
if (this.desc.charAt(j) === '"') {
if (i === NOT_INSIDE_MATCH) {
i = j + 1;
} else {
expr.addProperty(match[2]);
expr.addValue(this.desc.substring(i, j));
expr.addOperator(PostfixExpr.op.EQ);
numAdds++;
if (numAdds > 1) {
expr.addOperator(PostfixExpr.op.OR);
}
i = NOT_INSIDE_MATCH;
}
}
j++;
}
$log.debug("_analyzeRequiredOnlyDependencies | " +
this.desc + " | " +
match[2] + ", " +
JSON.stringify(expr));
return [match[1] === "Required", expr];
};
DriverProperty.prototype._analyzeOneOfDependencies = function() {
var match = this.desc.match(ONE_OF_REGEX);
if (!match) {
return null;
}
// Build logical expression to describe under what conditions this
// property is active
var expr = new PostfixExpr();
var parts = match[1].split(", or ");
expr.addProperty(parts[1]);
expr.addValue(undefined);
expr.addOperator(PostfixExpr.op.EQ);
parts = parts[0].split(", ");
for (var i = 0; i < parts.length; i++) {
expr.addProperty(parts[i]);
expr.addValue(undefined);
expr.addOperator(PostfixExpr.op.EQ);
expr.addOperator(PostfixExpr.op.AND);
}
$log.debug("_analyzeOneOfDependencies | " +
this.desc + " | " +
JSON.stringify(match) + ", " +
JSON.stringify(expr));
return [true, expr];
};
/**
* @description Get the names of the driver-properties whose values
* determine whether this property is active
*
* @return {object} Object the properties of which are names of
* activating driver-properties or null
*/
DriverProperty.prototype.getActivators = function() {
return this.isActiveExpr ? this.isActiveExpr.getProperties() : null;
};
/**
* PostFixExpr is a class primarily developed to support the
* evaluation of boolean expressions that determine whether a
* particular property is active.
*
* The expression is stored as a postfix sequence of operands and
* operators. Operands are currently limited to the literal values
* and the values of properties in a specified set. Currently
* supported operands are ==, or, and.
*
* @return {void}
*/
function PostfixExpr() {
this.elem = [];
}
PostfixExpr.op = {
EQ: "==",
AND: "and",
OR: "or"
};
PostfixExpr.UNDEFINED = undefined;
PostfixExpr.status = {
OK: 0,
ERROR: 1,
BAD_ARG: 2,
UNKNOWN_OP: 3,
MALFORMED: 4
};
/**
* @description Add a property to the expression
*
* @param {string} propertyName - Property name
*
* @return {void}
*/
PostfixExpr.prototype.addProperty = function(propertyName) {
this.elem.push({name: propertyName});
};
/**
* @description Add a value to the expression
*
* @param {object} value - value
*
* @return {void}
*/
PostfixExpr.prototype.addValue = function(value) {
this.elem.push({value: value});
};
/**
* @description Add an operator to the expression
*
* @param {PostfixExpr.op} opId - operator
*
* @return {void}
*/
PostfixExpr.prototype.addOperator = function(opId) {
this.elem.push({op: opId});
};
/**
* @description Get a list of property names referenced by this
* expression
*
* @return {object} An object each property of which corresponds to
* a property in the expression
*/
PostfixExpr.prototype.getProperties = function() {
var properties = {};
angular.forEach(this.elem, function(elem) {
if (angular.isDefined(elem.name)) {
properties[elem.name] = true;
}
});
return properties;
};
/**
* @description Evaluate a boolean binary operation
*
* @param {array} valStack - Stack of values to operate on
* @param {string} opId - operator id
*
* @return {integer} Return code
*/
function _evaluateBoolBinaryOp(valStack, opId) {
var retCode = PostfixExpr.status.OK;
var val1 = valStack.pop();
var val2 = valStack.pop();
if (typeof val1 === "boolean" &&
typeof val2 === "boolean") {
switch (opId) {
case PostfixExpr.op.AND:
valStack.push(val1 && val2);
break;
case PostfixExpr.op.OR:
valStack.push(val1 || val2);
break;
default:
retCode = PostfixExpr.status.UNKNOWN_OP;
}
} else {
retCode = PostfixExpr.status.BAD_ARG;
}
return retCode;
}
/**
* @description Evaluate the experssion using property values from
* a specified set
*
* @param {object} propertySet - Dictionary of DriverProperty instances
*
* @return {array} Return code and Value of the expression
*/
PostfixExpr.prototype.evaluate = function(propertySet) {
var resultStack = [];
for (var i = 0, len = this.elem.length; i < len; i++) {
var elem = this.elem[i];
if (elem.hasOwnProperty("name")) {
resultStack.push(propertySet[elem.name].getInputValue());
} else if (elem.hasOwnProperty("value")) {
resultStack.push(elem.value);
} else if (elem.hasOwnProperty("op")) {
if (elem.op === PostfixExpr.op.EQ) {
var val1 = resultStack.pop();
var val2 = resultStack.pop();
resultStack.push(val1 === val2);
} else {
var ret = _evaluateBoolBinaryOp(resultStack, elem.op);
if (ret !== PostfixExpr.status.OK) {
return [ret, PostfixExpr.UNDEFINED];
}
}
} else {
return [PostfixExpr.status.UNKNOWN_ELEMENT, PostfixExpr.UNDEFINED];
}
}
return resultStack.length === 1
? [PostfixExpr.status.OK, resultStack.pop()]
: [PostfixExpr.status.MALFORMED, PostfixExpr.UNDEFINED];
};
/**
* @description Class for representing and manipulating undirected
* graphs
*
* @property {object} vertices - Associative array of vertex objects
* indexed by property name
* @return {object} Graph
*/
function Graph() {
this.vertices = {};
}
Graph.prototype.getVertex = function(vertexName) {
var vertex = null;
if (this.vertices.hasOwnProperty(vertexName)) {
vertex = this.vertices[vertexName];
}
return vertex;
};
/**
* @description Add a vertex to this graph
*
* @param {string} name - Vertex name
* @param {object} data - Vertex data
* @returns {object} - Newly created vertex
*/
Graph.prototype.addVertex = function(name, data) {
var vertex = {name: name, data: data, adjacents: []};
this.vertices[name] = vertex;
return vertex;
};
/**
* @description Add an undirected edge between two vertices
*
* @param {string} vertexName1 - Name of first vertex
* @param {string} vertexName2 - Name of second vertex
* @returns {void}
*/
Graph.prototype.addEdge = function(vertexName1, vertexName2) {
this.vertices[vertexName1].adjacents.push(vertexName2);
this.vertices[vertexName2].adjacents.push(vertexName1);
};
/**
* @description Depth-first-search graph traversal utility function
*
* @param {object} vertex - Root vertex from which traveral will begin.
* It is assumed that this vertex has not alreday been visited as part
* of this traversal.
* @param {object} visited - Associative array. Each named property
* corresponds to a vertex with the same name, and has boolean value
* indicating whether the vertex has been alreday visited.
* @param {object[]} component - Array of vertices that define a strongly
* connected component.
* @returns {void}
*/
Graph.prototype._dfsTraverse = function(vertex, visited, component) {
var graph = this;
visited[vertex.name] = true;
component.push(vertex);
/* eslint-disable no-unused-vars */
angular.forEach(vertex.adjacents, function(vertexName) {
if (!visited[vertexName]) {
graph._dfsTraverse(graph.vertices[vertexName], visited, component);
}
});
/* eslint-enable no-unused-vars */
};
/**
* @description Perform a depth-first-search on a specified graph to
* find strongly connected components. A user provided function will
* be called to process each component.
*
* @param {function} componentFunc - Function called on each strongly
* connected component. Accepts aruments: array of vertex objects, and
* user-provided extra data that can be used in processing the component.
* @param {object} extra - Extra data that is passed into the component
* processing function.
* @returns {void}
*/
Graph.prototype.dfs = function(componentFunc, extra) {
var graph = this;
var visited = {};
angular.forEach(
graph.vertices,
function(unused, name) {
visited[name] = false;
});
angular.forEach(this.vertices, function(vertex, vertexName) {
if (!visited[vertexName]) {
var component = [];
graph._dfsTraverse(vertex, visited, component);
componentFunc(component, extra);
}
});
};
return service;
}
})();

View File

@ -0,0 +1,115 @@
/**
* Copyright 2016 Cray 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.
*/
(function() {
"use strict";
describe(
'horizon.dashboard.admin.ironic.enroll-node.service',
function() {
var service;
beforeEach(module('horizon.dashboard.admin'));
beforeEach(module('horizon.dashboard.admin.ironic'));
beforeEach(module(function($provide) {
$provide.value('$modal', jasmine.createSpy());
}));
beforeEach(inject(function($injector) {
service =
$injector.get('horizon.dashboard.admin.ironic.enroll-node.service');
}));
it('defines the service', function() {
expect(service).toBeDefined();
});
describe('DriverProperty', function() {
it('Base construction', function() {
var propertyName = 'propertyName';
var description = '';
var propertySet = [];
var property = new service.DriverProperty(propertyName,
description,
propertySet);
expect(property.name).toBe(propertyName);
expect(property.desc).toBe(description);
expect(property.propertySet).toBe(propertySet);
expect(property.getSelectOptions()).toBe(null);
expect(property.required).toBe(false);
expect(property.defaultValue).toBe(null);
expect(property.inputValue).toBe(null);
expect(property.getInputValue()).toBe(null);
expect(property.isActive()).toBe(true);
});
it('Required - ends with', function() {
var property = new service.DriverProperty('propertyName',
' Required.',
[]);
expect(property.required).toBe(true);
});
it('Not required - missing space', function() {
var property = new service.DriverProperty('propertyName',
'Required.',
[]);
expect(property.required).toBe(false);
});
it('Not required - missing period', function() {
var property = new service.DriverProperty('propertyName',
' Required',
[]);
expect(property.required).toBe(false);
});
it('Select options', function() {
var property = new service.DriverProperty(
'propertyName',
'One of "foo", bar.',
[]);
expect(property.getSelectOptions()).toEqual(['foo', 'bar']);
});
it('Select options - No single quotes', function() {
var property = new service.DriverProperty(
'propertyName',
"One of 'foo', bar.",
[]);
expect(property.getSelectOptions()).toEqual(["'foo'", 'bar']);
});
it('default - is string', function() {
var property = new service.DriverProperty(
'propertyName',
'default is "5.1".',
[]);
expect(property._getDefaultValue()).toEqual('5.1');
});
it('default - period processing', function() {
var property = new service.DriverProperty(
'propertyName',
'default is 5.1.',
[]);
expect(property._getDefaultValue()).toEqual('5');
});
});
});
})();

View File

@ -26,6 +26,21 @@
* to support and display Ironic related content.
*/
angular
.module('horizon.dashboard.admin.ironic', []);
.module('horizon.dashboard.admin.ironic', [])
.constant('horizon.dashboard.admin.ironic.validHostNamePattern',
'^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$') // eslint-disable-line max-len
.constant('horizon.dashboard.admin.ironic.validUuidPattern',
'^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$') // eslint-disable-line max-len
.constant('horizon.dashboard.admin.ironic.events', events());
function events() {
return {
ENROLL_NODE_SUCCESS:'horizon.dashboard.admin.ironic.ENROLL_NODE_SUCCESS',
DELETE_NODE_SUCCESS:'horizon.dashboard.admin.ironic.DELETE_NODE_SUCCESS',
CREATE_PORT_SUCCESS:'horizon.dashboard.admin.ironic.CREATE_PORT_SUCCESS',
DELETE_PORT_SUCCESS:'horizon.dashboard.admin.ironic.DELETE_PORT_SUCCESS'
};
}
})();

View File

@ -28,157 +28,295 @@
];
/**
* @ngdoc service
* @name horizon.app.core.openstack-service-api.ironic
* @description Provides access to Ironic API
* @description Service that provides access to the Ironic client API
*
* @param {object} apiService - HTTP service
* @param {object} toastService - User message service
* @return {object} Ironic API service
*/
function ironicAPI(apiService, toastService) {
var service = {
getNodes: getNodes,
createNode: createNode,
createPort: createPort,
deleteNode: deleteNode,
deletePort: deletePort,
getDrivers: getDrivers,
getDriverProperties: getDriverProperties,
getNode: getNode,
getNodes: getNodes,
getPortsWithNode: getPortsWithNode,
putNodeInMaintenanceMode: putNodeInMaintenanceMode,
removeNodeFromMaintenanceMode: removeNodeFromMaintenanceMode,
powerOffNode: powerOffNode,
powerOnNode: powerOnNode,
powerOffNode: powerOffNode
putNodeInMaintenanceMode: putNodeInMaintenanceMode,
removeNodeFromMaintenanceMode: removeNodeFromMaintenanceMode
};
return service;
///////////
/**
* @name horizon.app.core.openstack-service-api.ironic.getNodes
* @description Retrieve a list of nodes
* http://docs.openstack.org/developer/ironic/webapi/v1.html#get--v1-nodes
*
* @return Node collection in JSON
* @return {promise} Node collection in JSON
* http://docs.openstack.org/developer/ironic/webapi/v1.html#NodeCollection
*/
function getNodes() {
return apiService.get('/api/ironic/nodes/')
.error(function() {
toastService.add('error', gettext('Unable to retrieve Ironic nodes.'));
toastService.add('error',
gettext('Unable to retrieve Ironic nodes.'));
});
}
/**
* @name horizon.app.core.openstack-service-api.ironic.getNode
* @description Retrieve information about the given node.
*
* http://docs.openstack.org/developer/ironic/webapi/v1.html#get--v1-nodes-(node_ident)
* http://docs.openstack.org/developer/ironic/webapi/v1.html#get--v1-
* nodes-(node_ident)
*
* @param {string} uuid UUID or logical name of a node.
* @return {promise} Node
*/
function getNode(uuid) {
return apiService.get('/api/ironic/nodes/' + uuid).error(function() {
toastService.add('error', gettext('Unable to retrieve the Ironic node.'));
});
return apiService.get('/api/ironic/nodes/' + uuid)
.error(function(reason) {
var msg = gettext('Unable to retrieve the Ironic node: %s');
toastService.add('error', interpolate(msg, [reason], false));
});
}
/**
* @name horizon.app.core.openstack-service-api.ironic.getPortsWithNode
* @description Retrieve a list of ports associated with a node.
*
* http://docs.openstack.org/developer/ironic/webapi/v1.html#get--v1-ports
*
* @param {string} uuid UUID or logical name of a node.
* @return {promise} List of ports
*/
function getPortsWithNode(uuid) {
var config = {
params : {
node_id: uuid
}
};
return apiService.get('/api/ironic/ports/', config).error(function() {
toastService.add('error', gettext('Unable to retrieve the Ironic node ports.'));
});
}
/**
* @name horizon.app.core.openstack-service-api.ironic.putNodeInMaintenanceMode
* @description Put the node in maintenance mode.
*
* \href{http://docs.openstack.org/developer/ironic/webapi/v1.html#
* put--v1-nodes-(node_ident)-maintenance}
*
* @param {string} uuid UUID or logical name of a node.
*/
function putNodeInMaintenanceMode(uuid, reason) {
var data = {
maint_reason: reason ? reason : gettext("No maintenance reason given.")
};
return apiService.patch('/api/ironic/nodes/' + uuid + '/maintenance', data).error(function() {
toastService.add('error',
gettext('Unable to put the Ironic node in maintenance mode.'));
});
}
/**
* @name horizon.app.core.openstack-service-api.ironic.removeNodeFromMaintenanceMode
* @description Remove the node from maintenance mode.
*
* \href{http://docs.openstack.org/developer/ironic/webapi/v1.html#
* delete--v1-nodes-(node_ident)-maintenance}
*
* @param {string} uuid UUID or logical name of a node.
*/
function removeNodeFromMaintenanceMode(uuid) {
return apiService.delete('/api/ironic/nodes/' + uuid + '/maintenance').error(function() {
toastService.add('error',
gettext('Unable to remove the Ironic node from maintenance mode.'));
});
}
/**
* @name horizon.app.core.openstack-service-api.ironic.powerOnNode
* @description Set the power state of the node.
*
* \href{http://docs.openstack.org/developer/ironic/webapi/v1.html#
* put--v1-nodes-(node_ident)-states-power}
*
* @param {string} uuid UUID or logical name of a node.
*/
function powerOnNode(uuid) {
var data = {
state: 'on'
};
return apiService.patch('/api/ironic/nodes/' + uuid + '/states/power', data)
.success(function () {
toastService.add('success', gettext('Refresh page to see updated power status'));
})
.error(function () {
toastService.add('error', gettext('Unable to power on the node'));
return apiService.get('/api/ironic/ports/', config)
.error(function(reason) {
var msg = gettext(
'Unable to retrieve the Ironic node ports: %s');
toastService.add('error', interpolate(msg, [reason], false));
});
}
/**
* @name horizon.app.core.openstack-service-api.ironic.powerOffNode
* @description Set the power state of the node.
* @description Put the node in maintenance mode.
*
* \href{http://docs.openstack.org/developer/ironic/webapi/v1.html#
* put--v1-nodes-(node_ident)-states-power}
* http://docs.openstack.org/developer/ironic/webapi/v1.html#
* put--v1-nodes-(node_ident)-maintenance
*
* @param {string} uuid UUID or logical name of a node.
* @param {string} reason Reason for why node is being put into
* maintenance mode
* @return {promise} Promise
*/
function putNodeInMaintenanceMode(uuid, reason) {
var data = {
maint_reason: reason ? reason : gettext("No maintenance reason given.")
};
return apiService.patch('/api/ironic/nodes/' + uuid + '/maintenance',
data)
.error(function(reason) {
var msg = gettext(
'Unable to put the Ironic node in maintenance mode: %s');
toastService.add('error', interpolate(msg, [reason], false));
});
}
/**
* @description Remove the node from maintenance mode.
*
* http://docs.openstack.org/developer/ironic/webapi/v1.html#
* delete--v1-nodes-(node_ident)-maintenance
*
* @param {string} uuid UUID or logical name of a node.
* @return {promise} Promise
*/
function removeNodeFromMaintenanceMode(uuid) {
return apiService.delete('/api/ironic/nodes/' + uuid + '/maintenance')
.error(function(reason) {
var msg = gettext('Unable to remove the Ironic node ' +
'from maintenance mode: %s');
toastService.add('error', interpolate(msg, [reason], false));
});
}
/**
* @description Set the power state of the node.
*
* http://docs.openstack.org/developer/ironic/webapi/v1.html#
* put--v1-nodes-(node_ident)-states-power
*
* @param {string} uuid UUID or logical name of a node.
* @return {promise} Promise
*/
function powerOnNode(uuid) {
var data = {
state: 'on'
};
return apiService.patch('/api/ironic/nodes/' + uuid + '/states/power',
data)
.success(function() {
toastService.add('success',
gettext('Refresh page to see updated power status'));
})
.error(function(reason) {
var msg = gettext('Unable to power on the node: %s');
toastService.add('error', interpolate(msg, [reason], false));
});
}
/**
* @description Set the power state of the node.
*
* http://docs.openstack.org/developer/ironic/webapi/v1.html#
* put--v1-nodes-(node_ident)-states-power
*
* @param {string} uuid UUID or logical name of a node.
* @return {promise} Promise
*/
function powerOffNode(uuid) {
var data = {
state: 'off'
};
return apiService.patch('/api/ironic/nodes/' + uuid + '/states/power', data)
.success(function () {
toastService.add('success', gettext('Refresh page to see updated power status'));
return apiService.patch('/api/ironic/nodes/' + uuid + '/states/power',
data)
.success(function() {
toastService.add('success',
gettext('Refresh page to see updated power status'));
})
.error(function () {
toastService.add('error', gettext('Unable to power off the node'));
.error(function(reason) {
var msg = gettext('Unable to power off the node: %s');
toastService.add('error', interpolate(msg, [reason], false));
});
}
/**
* @description Create an Ironic node
*
* http://docs.openstack.org/developer/ironic/webapi/v1.html#post--v1-nodes
*
* @param {object} params Object containing parameters that define
* the node to be created
* @return {promise} Promise
*/
function createNode(params) {
var data = {
node: params
};
return apiService.post('/api/ironic/nodes/', data)
.success(function() {
})
.error(function(reason) {
var msg = gettext('Unable to create node: %s');
toastService.add('error', interpolate(msg, [reason], false));
});
}
/**
* @description Delete the specified node from inventory
*
* http://docs.openstack.org/developer/ironic/webapi/v1.html#
* delete--v1-nodes
*
* @param {string} nodeIdent UUID or logical name of a node.
* @return {promise} Promise
*/
function deleteNode(nodeIdent) {
var data = {
node: nodeIdent
};
return apiService.delete('/api/ironic/nodes/', data)
.success(function() {
})
.error(function(reason) {
var msg = gettext('Unable to delete node %s: %s');
toastService.add(
'error',
interpolate(msg, [nodeIdent, reason], false));
});
}
/**
* @description Retrieve the list of Ironic drivers
*
* http://docs.openstack.org/developer/ironic/webapi/v1.html#get--v1-drivers
*
* @return {promise} Driver collection in JSON
* http://docs.openstack.org/developer/ironic/webapi/v1.html#DriverList
*/
function getDrivers() {
return apiService.get('/api/ironic/drivers/').error(function(reason) {
var msg = gettext('Unable to retrieve Ironic drivers: %s');
toastService.add('error', interpolate(msg, [reason], false));
});
}
/**
* @description Retrieve properities of a specified driver
*
* http://docs.openstack.org/developer/ironic/webapi/v1.html#
* get--v1-drivers-properties
*
* @param {string} driverName - Driver name
* @returns {promise} Property list
*/
function getDriverProperties(driverName) {
return apiService.get(
'/api/ironic/drivers/' + driverName + '/properties').error(
function(reason) {
var msg = gettext(
'Unable to retrieve driver properties: %s');
toastService.add('error', interpolate(msg, [reason], false));
});
}
/**
* @description Create a network port
*
* @param {object} port Object containing parameters that define
* the port to be created
* @return {promise} Promise
*/
function createPort(port) {
var data = {
port: port
};
return apiService.post('/api/ironic/ports/', data)
.success(function() {
toastService.add('success',
gettext('Port successfully created'));
})
.error(function(reason) {
var msg = gettext('Unable to create port: %s');
toastService.add('error', interpolate(msg, [reason], false));
});
}
/**
* @description Delete a network port
*
* @param {string} portUuid UUID of the port to be deleted
* @return {promise} Promise
*/
function deletePort(portUuid) {
var data = {
port_uuid: portUuid
};
return apiService.delete('/api/ironic/ports/', data)
.success(function() {
})
.error(function(reason) {
var msg = gettext('Unable to delete port: %s');
toastService.add('error', interpolate(msg, [reason], false));
});
}
}

View File

@ -15,7 +15,7 @@
</div>
</div>
<div class="modal-footer">
<button class="btn btn-default secondary"
<button class="btn btn-default secondary"
type="button"
ng-click="ctrl.cancel()"
translate>
@ -28,3 +28,4 @@
Put Node(s) Into Maintenance Mode
</button>
</div>

View File

@ -0,0 +1,46 @@
/*
* Copyright 2016 Cray 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.
*/
(function () {
'use strict';
angular
.module('horizon.dashboard.admin.ironic')
.directive('modalDraggable', ModalDraggable);
ModalDraggable.$inject = ['$document', '$log'];
function ModalDraggable($document, $log) {
return function (scope, element) {
var modalContent = null;
while (element) {
if (element.hasClass("modal-content")) {
modalContent = element;
break;
}
element = element.parent();
}
if (modalContent) {
modalContent.draggable({
handle: ".modal-header"
});
} else {
$log.error("Unable to find parent dialog");
}
};
}
})();

View File

@ -20,6 +20,34 @@
var POWER_STATE_ON = 'power on';
var POWER_STATE_OFF = 'power off';
var DELETE_NODE_TITLE = gettext("Delete Node");
var DELETE_NODE_MSG =
gettext('Are you sure you want to delete node "%s"? ' +
'This action cannot be undone.');
var DELETE_NODE_SUCCESS = gettext('Successfully deleted node "%s"');
var DELETE_NODE_ERROR = gettext('Unable to delete node "%s"');
var DELETE_NODES_TITLE = gettext("Delete Nodes");
var DELETE_NODES_MSG =
gettext('Are you sure you want to delete nodes "%s"? ' +
'This action cannot be undone.');
var DELETE_NODES_SUCCESS = gettext('Successfully deleted nodes "%s"');
var DELETE_NODES_ERROR = gettext('Error deleting nodes "%s"');
var DELETE_PORT_TITLE = gettext("Delete Port");
var DELETE_PORT_MSG =
gettext('Are you sure you want to delete port "%s"? ' +
'This action cannot be undone.');
var DELETE_PORT_SUCCESS = gettext('Successfully deleted port "%s"');
var DELETE_PORT_ERROR = gettext('Unable to delete port "%s"');
var DELETE_PORTS_TITLE = gettext("Delete Ports");
var DELETE_PORTS_MSG =
gettext('Are you sure you want to delete ports "%s"? ' +
'This action cannot be undone.');
var DELETE_PORTS_SUCCESS = gettext('Successfully deleted ports "%s"');
var DELETE_PORTS_ERROR = gettext('Error deleting ports "%s"');
angular
.module('horizon.dashboard.admin.ironic')
.factory('horizon.dashboard.admin.ironic.actions', actions);
@ -27,11 +55,26 @@
actions.$inject = [
'horizon.app.core.openstack-service-api.ironic',
'horizon.framework.widgets.toast.service',
'$q'
'horizon.dashboard.admin.ironic.events',
'horizon.framework.widgets.modal.deleteModalService',
'horizon.dashboard.admin.ironic.create-port.service',
'$q',
'$rootScope'
];
function actions(ironic, toastService, $q) {
function actions(ironic,
toastService,
ironicEvents,
deleteModalService,
createPortService,
$q,
$rootScope) {
var service = {
createPort: createPort,
deleteNode: deleteNode,
deleteNodes: deleteNodes,
deletePort: deletePort,
deletePorts: deletePorts,
powerOn: powerOn,
powerOff: powerOff,
powerOnAll: powerOnNodes,
@ -44,33 +87,62 @@
return service;
function deleteNode(node) {
var labels = {
title: DELETE_NODE_TITLE,
message: DELETE_NODE_MSG,
submit: DELETE_NODE_TITLE,
success: DELETE_NODE_SUCCESS,
error: DELETE_NODE_ERROR
};
var context = {
labels: labels,
deleteEntity: ironic.deleteNode,
successEvent: ironicEvents.DELETE_NODE_SUCCESS
};
return deleteModalService.open($rootScope, [node], context);
}
function deleteNodes(nodes) {
var labels = {
title: DELETE_NODES_TITLE,
message: DELETE_NODES_MSG,
submit: DELETE_NODES_TITLE,
success: DELETE_NODES_SUCCESS,
error: DELETE_NODES_ERROR
};
var context = {
labels: labels,
deleteEntity: ironic.deleteNode,
successEvent: ironicEvents.DELETE_NODE_SUCCESS
};
return deleteModalService.open($rootScope, nodes, context);
}
// power state
function powerOn(node) {
if (node.power_state !== POWER_STATE_OFF) {
return $q.reject(gettext("Node is not powered off."));
var msg = gettext("Node %s is not powered off.");
return $q.reject(interpolate(msg, [node], false));
}
return ironic.powerOnNode(node.uuid).then(
function() {
// Set power state to be indeterminate
node.power_state = null;
},
function(reason) {
toastService.add('error', reason);
});
}
);
}
function powerOff(node) {
if (node.power_state !== POWER_STATE_ON) {
return $q.reject(gettext("Node is not powered on."));
var msg = gettext("Node %s is not powered on.");
return $q.reject(interpolate(msg, [node], false));
}
return ironic.powerOffNode(node.uuid).then(
function() {
// Set power state to be indeterminate
node.power_state = null;
},
function(reason) {
toastService.add('error', reason);
}
);
}
@ -87,30 +159,26 @@
function putInMaintenanceMode(node, maintReason) {
if (node.maintenance !== false) {
return $q.reject(gettext("Node is already in maintenance mode."));
var msg = gettext("Node %s is already in maintenance mode.");
return $q.reject(interpolate(msg, [node], false));
}
return ironic.putNodeInMaintenanceMode(node.uuid, maintReason).then(
function () {
node.maintenance = true;
node.maintenance_reason = maintReason;
},
function(reason) {
toastService.add('error', reason);
}
);
}
function removeFromMaintenanceMode(node) {
if (node.maintenance !== true) {
return $q.reject(gettext("Node is not in maintenance mode."));
var msg = gettext("Node %s is not in maintenance mode.");
return $q.reject(interpolate(msg, [node], false));
}
return ironic.removeNodeFromMaintenanceMode(node.uuid).then(
function () {
node.maintenance = false;
node.maintenance_reason = "";
},
function (reason) {
toastService.add('error', reason);
}
);
}
@ -123,6 +191,42 @@
return applyFuncToNodes(removeFromMaintenanceMode, nodes);
}
function createPort(node) {
return createPortService.modal(node);
}
function deletePort(port) {
var labels = {
title: DELETE_PORT_TITLE,
message: DELETE_PORT_MSG,
submit: DELETE_PORT_TITLE,
success: DELETE_PORT_SUCCESS,
error: DELETE_PORT_ERROR
};
var context = {
labels: labels,
deleteEntity: ironic.deletePort,
successEvent: ironicEvents.DELETE_PORT_SUCCESS
};
return deleteModalService.open($rootScope, [port], context);
}
function deletePorts(ports) {
var labels = {
title: DELETE_PORTS_TITLE,
message: DELETE_PORTS_MSG,
submit: DELETE_PORTS_TITLE,
success: DELETE_PORTS_SUCCESS,
error: DELETE_PORTS_ERROR
};
var context = {
labels: labels,
deleteEntity: ironic.deletePort,
successEvent: ironicEvents.DELETE_PORT_SUCCESS
};
return deleteModalService.open($rootScope, ports, context);
}
/*
* @name horizon.dashboard.admin.ironic.actions.applyFuncToNodes
* @description Apply a specified function to each member of a

View File

@ -23,21 +23,31 @@
IronicNodeDetailsController);
IronicNodeDetailsController.$inject = [
'$scope',
'$rootScope',
'$location',
'horizon.app.core.openstack-service-api.ironic',
'horizon.dashboard.admin.ironic.events',
'horizon.dashboard.admin.ironic.actions',
'horizon.dashboard.admin.basePath',
'horizon.dashboard.admin.ironic.maintenance.service'
'horizon.dashboard.admin.ironic.maintenance.service',
'horizon.dashboard.admin.ironic.validUuidPattern'
];
function IronicNodeDetailsController($location,
function IronicNodeDetailsController($scope,
$rootScope,
$location,
ironic,
ironicEvents,
actions,
basePath,
maintenanceService) {
maintenanceService,
validUuidPattern) {
var ctrl = this;
var path = basePath + 'ironic/node-details/sections/';
ctrl.noPortsText = gettext('No network ports have been defined');
ctrl.actions = actions;
ctrl.sections = [
@ -51,18 +61,42 @@
}
];
ctrl.ports = [];
ctrl.portsSrc = [];
ctrl.basePath = basePath;
ctrl.re_uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
ctrl.re_uuid = new RegExp(validUuidPattern);
ctrl.isUuid = isUuid;
ctrl.getVifPortId = getVifPortId;
ctrl.putNodeInMaintenanceMode = putNodeInMaintenanceMode;
ctrl.removeNodeFromMaintenanceMode = removeNodeFromMaintenanceMode;
ctrl.createPort = createPort;
ctrl.deletePort = deletePort;
ctrl.deletePorts = deletePorts;
var createPortHandler =
$rootScope.$on(ironicEvents.CREATE_PORT_SUCCESS,
function() {
init();
});
var deletePortHandler =
$rootScope.$on(ironicEvents.DELETE_PORT_SUCCESS,
function() {
init();
$scope.$broadcast('hzTable:clearSelected');
});
$scope.$on('$destroy', function() {
createPortHandler();
deletePortHandler();
});
init();
/**
* @name horizon.dashboard.admin.ironic.NodeDetailsController.init
* @description Initialize the controller instance based on the current page url.
* @description Initialize the controller instance based on the
* current page url.
*
* @return {void}
*/
@ -87,20 +121,24 @@
function retrieveNode(uuid) {
return ironic.getNode(uuid).then(function (response) {
ctrl.node = response.data;
ctrl.node.id = uuid;
});
}
/**
* @name horizon.dashboard.admin.ironic.NodeDetailsController.retrievePorts
* @description Retrieve the ports associated with a specified node, and store
* them in the controller instance.
* @description Retrieve the ports associated with a specified node,
* and store them in the controller instance.
*
* @param {string} nodeId Node name or UUID
* @return {void}
*/
function retrievePorts(nodeId) {
ironic.getPortsWithNode(nodeId).then(function (response) {
ctrl.ports = response.data.items;
ctrl.portsSrc = response.data.items;
ctrl.portsSrc.forEach(function(port) {
port.id = port.uuid;
});
});
}
@ -109,7 +147,8 @@
* @description Test whether a string is an OpenStack UUID
*
* @param {string} str string
* @return {boolean} True if the string is an OpenStack UUID, otherwise false
* @return {boolean} True if the string is an OpenStack UUID,
* otherwise false
*/
function isUuid(str) {
return !!str.match(ctrl.re_uuid);
@ -120,7 +159,8 @@
* @description Get the vif_port_id property of a specified port
*
* @param {object} port instance of port
* @return {string} Value of vif_port_id property or "" if the property does not exist
* @return {string} Value of vif_port_id property or
* "" if the property does not exist
*/
function getVifPortId(port) {
return angular.isDefined(port.extra) &&
@ -135,5 +175,42 @@
function removeNodeFromMaintenanceMode() {
maintenanceService.removeNodeFromMaintenanceMode(ctrl.node);
}
/**
* @name horizon.dashboard.admin.ironic.NodeDetailsController.createPort
* @description Initiate creation of a newtwork port for the current
* node
*
* @return {void}
*/
function createPort() {
ctrl.actions.createPort(ctrl.node);
}
/**
* @name horizon.dashboard.admin.ironic.NodeDetailsController.deletePort
* @description Delete a specified port
*
* @param {port []} port port to be deleted
* @return {void}
*/
function deletePort(port) {
ctrl.actions.deletePort({id: port.uuid, name: port.address});
}
/**
* @name horizon.dashboard.admin.ironic.NodeDetailsController.deletePorts
* @description Delete a specified list of ports
*
* @param {port []} ports list of ports to be deleted
* @return {void}
*/
function deletePorts(ports) {
var selectedPorts = [];
angular.forEach(ports, function(port) {
selectedPorts.push({id: port.uuid, name: port.address});
});
ctrl.actions.deletePorts(selectedPorts);
}
}
})();

View File

@ -56,8 +56,14 @@
beforeEach(module('horizon.dashboard.admin.ironic'));
beforeEach(module('horizon.app.core.openstack-service-api', function($provide) {
$provide.value('horizon.app.core.openstack-service-api.ironic', ironicAPI);
beforeEach(module(function($provide) {
$provide.value('horizon.app.core.openstack-service-api.ironic',
ironicAPI);
}));
beforeEach(module(function($provide) {
$provide.value('horizon.dashboard.admin.ironic.maintenance.service',
{});
}));
beforeEach(inject(function ($injector, _$rootScope_, _$location_) {
@ -67,11 +73,11 @@
var $location = _$location_;
$location.path('/admin/ironic/' + nodeUuid + '/');
ctrl = controller('horizon.dashboard.admin.ironic.NodeDetailsController', {
$scope: scope,
$location: $location,
'horizon.dashboard.admin.ironic.actions': {},
'horizon.dashboard.admin.basePath': '/static'});
ctrl = controller(
'horizon.dashboard.admin.ironic.NodeDetailsController',
{$location: $location,
'horizon.dashboard.admin.ironic.actions': {},
'horizon.dashboard.admin.basePath': '/static'});
scope.$apply();
}));

View File

@ -8,9 +8,9 @@
<dt translate>Node ID</dt>
<dd>{$ ctrl.node.uuid $}</dd>
<dt translate>Chassis ID</dt>
<dd>{$ ctrl.node.chassis_uuid $}</dd>
<dd>{$ ctrl.node.chassis_uuid | noValue $}</dd>
<dt translate>Created At</dt>
<dd>{$ ctrl.node.created_at $}</dd>
<dd>{$ ctrl.node.created_at | date:'medium' $}</dd>
<dt translate>Extra</dt>
<dd>{$ ctrl.node.extra $}</dd>
</dl>
@ -20,19 +20,89 @@
<div class="col-md-6 status detail">
<h4 translate>Ports</h4>
<hr class="header_rule">
<dl class="dl-horizontal">
<dt ng-repeat-start="port in ctrl.ports">
{$ 'MAC ' + (1 + $index) $}</dt>
<dd ng-if="vif_port_id = ctrl.getVifPortId(port)">
<a href="/admin/networks/ports/{$ vif_port_id $}/detail">
{$ port.address $}
</a>
</dd>
<dd ng-if="!vif_port_id">
{$ port.address $}
</dd>
<p ng-repeat-end></p>
</dl>
<table hz-table ng-cloak
st-table="ctrl.ports"
st-safe-src="ctrl.portsSrc"
class="table table-striped table-rsp table-detail">
<thead>
<tr>
<th colspan="4" class="action-col">
<action-list dropdown class="pull-right">
<action button-type="split-button"
action-classes="'btn btn-default btn-sm'"
callback="ctrl.createPort">
{$ 'Create port' | translate $}
</action>
<menu>
<action button-type="menu-item"
callback="ctrl.deletePorts"
item="tCtrl.selected"
disabled="tCtrl.selected.length === 0">
<span class="fa fa-trash"></span>
{$ 'Delete ports' | translate $}
</action>
</menu>
</action-list>
</th>
</tr>
<tr>
<th class="multi_select_column">
<input type="checkbox"
hz-select-all="ctrl.ports"/>
</th>
<th translate class="rsp-p1" style="white-space:nowrap">
MAC Address
</th>
<th translate class="rsp-p2" style="width:100%;">
Extra
</th>
<th translate class="actions_column">
Actions
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="port in ctrl.ports">
<td class="multi_select_column">
<input type="checkbox"
hz-select="port"
ng-model="tCtrl.selections[port.id].checked"/>
<td ng-if="vif_port_id = ctrl.getVifPortId(port)"
class="rsp-p1">
<a href="/dashboard/admin/networks/ports/{$ vif_port_id $}/detail">
{$ port.address $}
</a>
</td>
<td ng-if="!vif_port_id" class="rsp-p1">
{$ port.address $}
</td>
<td>
<dl class="dl-horizontal">
<dt style="width:auto;" ng-repeat-start="(id, value) in port.extra">
{$ id $}
</dt>
<dd>
{$ value $}
</dd>
<p ng-repeat-end></p>
</dl>
</td>
<td class="actions_column">
<action-list>
<action action-classes="'btn btn-default btn-sm'"
callback="ctrl.deletePort"
item="port">
<span class="fa fa-trash"></span>
</action>
</action-list>
</td>
</tr>
<tr hz-no-items
items="ctrl.ports"
message="ctrl.noPortsText">
</tr>
</tbody>
</table>
</div>
</div>
@ -42,14 +112,9 @@
<h4 translate>Properties</h4>
<hr class="header_rule">
<dl class="dl-horizontal">
<dt translate>Memory</dt>
<dd>{$ ctrl.node.properties.memory_mb + ' MB' $}</dd>
<dt translate>CPU Arch</dt>
<dd>{$ ctrl.node.properties.cpu_arch $}</dd>
<dt translate>Local GB</dt>
<dd>{$ ctrl.node.properties.local_gb $}</dd>
<dt translate>CPUs</dt>
<dd>{$ ctrl.node.properties.cpus $}</dd>
<dt ng-repeat-start="(propertyName, propertyValue) in ctrl.node.properties">
{$ propertyName $}</dt>
<dd ng-repeat-end>{$ propertyValue $}</dd>
</dl>
</div>
@ -68,7 +133,7 @@
<dt translate>Deploy Kernel</dt>
<dd>
<a ng-if="deploy_kernel_is_uuid = ctrl.isUuid(ctrl.node.driver_info.deploy_kernel)"
href="/admin/images/{$ ctrl.node.driver_info.deploy_kernel $}/detail">
href="/dashboard/admin/images/{$ ctrl.node.driver_info.deploy_kernel $}/detail">
{$ ctrl.node.driver_info.deploy_kernel $}
</a>
<span ng-if="!deploy_kernel_is_uuid">
@ -78,7 +143,7 @@
<dt translate>Deploy Ramdisk</dt>
<dd>
<a ng-if="deploy_ramdisk_is_uuid = ctrl.isUuid(ctrl.node.driver_info.deploy_ramdisk)"
href="/admin/images/{$ ctrl.node.driver_info.deploy_ramdisk $}/detail">
href="/dashboard/admin/images/{$ ctrl.node.driver_info.deploy_ramdisk $}/detail">
{$ ctrl.node.driver_info.deploy_ramdisk $}
</a>
<span ng-if="!deploy_ramdisk_is_uuid">
@ -107,22 +172,29 @@
<!-- Instance Info -->
<div class="col-md-6 status detail">
<h4 translate>Instance Info</h4>
<hr class="header_rule">
<dl class="dl-horizontal">
<dt translate>Instance Name</dt>
<dd>{$ ctrl.node.instance_info.display_name $}</dd>
<dt translate>Ramdisk</dt>
<dd>
<a href="/admin/images/{$ ctrl.node.instance_info.ramdisk $}/detail">
{$ ctrl.node.instance_info.ramdisk $}
</a>
</dd>
<dt translate>Kernel</dt>
<dd>
<a href="/admin/images/{$ ctrl.node.instance_info.kernel $}/detail">
{$ ctrl.node.instance_info.kernel $}
</a>
</dd>
</dl>
<hr class="header_rule"/>
<div ng-switch="ctrl.node.driver">
<dl ng-switch-when="pxe_ssh" class="dl-horizontal">
<dt translate>Instance Name</dt>
<dd>{$ ctrl.node.instance_info.display_name $}</dd>
<dt translate>Ramdisk</dt>
<dd>
<a href="/dashboard/admin/images/{$ ctrl.node.instance_info.ramdisk $}/detail">
{$ ctrl.node.instance_info.ramdisk $}
</a>
</dd>
<dt translate>Kernel</dt>
<dd>
<a href="/dashboard/admin/images/{$ ctrl.node.instance_info.kernel $}/detail">
{$ ctrl.node.instance_info.kernel $}
</a>
</dd>
</dl>
<dl ng-switch-default class="dl-horizontal">
<dt ng-repeat-start="(id, value) in ctrl.node.instance_info">{$ id $}</dt>
<dd ng-repeat-end>{$ value $}</dd>
</dl>
</div>
</div>
</div>

View File

@ -22,16 +22,22 @@
.controller('IronicNodeListController', IronicNodeListController);
IronicNodeListController.$inject = [
'$rootScope',
'horizon.app.core.openstack-service-api.ironic',
'horizon.dashboard.admin.ironic.events',
'horizon.dashboard.admin.ironic.actions',
'horizon.dashboard.admin.basePath',
'horizon.dashboard.admin.ironic.maintenance.service'
'horizon.dashboard.admin.ironic.maintenance.service',
'horizon.dashboard.admin.ironic.enroll-node.service'
];
function IronicNodeListController(ironic,
function IronicNodeListController($rootScope,
ironic,
ironicEvents,
actions,
basePath,
maintenanceService) {
maintenanceService,
enrollNodeService) {
var ctrl = this;
ctrl.nodes = [];
@ -43,6 +49,7 @@
ctrl.putNodesInMaintenanceMode = putNodesInMaintenanceMode;
ctrl.removeNodeFromMaintenanceMode = removeNodeFromMaintenanceMode;
ctrl.removeNodesFromMaintenanceMode = removeNodesFromMaintenanceMode;
ctrl.enrollNode = enrollNode;
/**
* Filtering - client-side MagicSearch
@ -81,6 +88,23 @@
}
];
// Listen for the creation of new nodes, and update the node list
$rootScope.$on(ironicEvents.ENROLL_NODE_SUCCESS, function() {
init();
});
$rootScope.$on(ironicEvents.DELETE_NODE_SUCCESS, function() {
init();
});
$rootScope.$on(ironicEvents.CREATE_PORT_SUCCESS, function() {
init();
});
$rootScope.$on(ironicEvents.DELETE_PORT_SUCCESS, function() {
init();
});
init();
// RETRIVE NODES AND PORTS
@ -124,6 +148,10 @@
function removeNodesFromMaintenanceMode(nodes) {
maintenanceService.removeNodesFromMaintenanceMode(nodes);
}
function enrollNode() {
enrollNodeService.modal();
}
}
})();

View File

@ -1,150 +1,176 @@
<table ng-controller="IronicNodeListController as table"
hz-table ng-cloak
st-table="table.nodes"
st-safe-src="table.nodesSrc"
default-sort="name"
default-sort-reverse="false"
class="table table-striped table-rsp table-detail">
<div ng-controller="IronicNodeListController as table">
<thead>
<tr>
<th colspan="100" class="search-header">
<hz-magic-search-bar group-classes="input-group-sm" icon-classes="fa-search"
filter-facets="table.nodeFacets">
</hz-magic-search-bar>
</th>
</tr>
<tr>
<th colspan="100" class="action-col">
<action-list dropdown class="pull-right">
<action button-type="split-button"
action-classes="'btn btn-default btn-sm'"
callback="table.actions.powerOnAll"
item="tCtrl.selected"
disabled="tCtrl.selected.length === 0">
{$ 'Power on' | translate $}
</action>
<menu>
<action button-type="menu-item"
callback="table.actions.powerOffAll"
item="tCtrl.selected"
disabled="tCtrl.selected.length === 0">
{$ 'Power off' | translate $}
</action>
<action button-type="menu-item"
callback="table.putNodesInMaintenanceMode"
item="tCtrl.selected"
disabled="tCtrl.selected.length === 0">
{$ 'Maintenance on' | translate $}
</action>
<action button-type="menu-item"
callback="table.removeNodesFromMaintenanceMode"
item="tCtrl.selected"
disabled="tCtrl.selected.length === 0">
{$ 'Maintenance off' | translate $}
</action>
</menu>
</action-list>
</th>
</tr>
<hz-magic-search-context filter-facets="table.nodeFacets">
<hz-magic-search-bar>
</hz-magic-search-bar>
<tr>
<th class="multi_select_column">
<input type="checkbox"
hz-select-all="table.nodes"/>
</th>
<th translate class="rsp-p1" st-sort="name" st-sort-default="name">
Node Name
</th>
<th translate class="rsp-p1" st-sort="instance_uuid">
Instance ID
</th>
<th translate class="rsp-p2" st-sort="power_state">
Power State
</th>
<th translate class="rsp-p2" st-sort="provision_state">
Provisioning State
</th>
<th translate class="rsp-p2" st-sort="maintenance">
Maintenance
</th>
<th translate class="rsp-p2" st-sort="ports">
Ports
</th>
<th translate class="rsp-p2" st-sort="driver">
Driver
</th>
<th translate class="actions_column">
Actions
</th>
</tr>
</thead>
<table st-magic-search
hz-table ng-cloak
st-table="table.nodes"
st-safe-src="table.nodesSrc"
default-sort="name"
default-sort-reverse="false"
class="table table-striped table-rsp table-detail">
<tbody>
<tr ng-repeat="node in table.nodes">
<td class="multi_select_column">
<input type="checkbox"
hz-select="node"
ng-model="tCtrl.selections[node.id].checked"/>
</td>
<td class="rsp-p1">
<a href="{$ node.uuid $}">
{$ node.name || node.uuid $}
</a>
</td>
<td class="rsp-p1">
<!-- multiple tags used purposefully to ensure that the link
is removed if node uuid == no instance. Using ng-attr-href
only removed the href and the <a> styling remained which could
cause confusion for users. -->
<a href="/admin/instances/{$ node.instance_uuid $}/detail"
ng-if="node.instance_uuid">
{$ node.instance_uuid $}
</a>
<span ng-if="!node.instance_uuid">{$ 'No Instance' | translate $}</span>
</td>
<td class="rsp-p2" >
<div ng-class="{'running': node['target_power_state']}">
{$ node.power_state $}
</div>
</td>
<td class="rsp-p2">{$ node.provision_state $}</td>
<td class="rsp-p2">{$ node.maintenance $}</td>
<td class="rsp-p2">{$ node.ports.length $}</td>
<td class="rsp-p2">{$ node.driver $}</td>
<td class="actions_column">
<action-list dropdown>
<action button-type="split-button"
action-classes="'btn btn-default btn-sm'"
callback="table.actions.powerOn"
disabled="node['power_state']!=='power off'"
item="node">
{$ 'Power on' | translate $}
</action>
<menu>
<action button-type="menu-item"
callback="table.actions.powerOff"
disabled="node['power_state']!=='power on'"
item="node">
{$ 'Power off' | translate $}
</action>
<action button-type="menu-item"
callback="table.putNodeInMaintenanceMode"
disabled="node['maintenance']"
item="node">
{$ 'Maintenance on' | translate $}
</action>
<action button-type="menu-item"
callback="table.removeNodeFromMaintenanceMode"
disabled="!node['maintenance']"
item="node">
{$ 'Maintenance off' | translate $}
</action>
</menu>
</action-list>
<thead>
<tr>
<th colspan="8">
<button class="btn btn-default btn-sm pull-right"
ng-click="table.enrollNode()">
<span class="fa fa-plus"></span>
<span translate>Enroll Node</span>
</button>
</th>
<th class="action-col">
<action-list dropdown class="pull-right">
<action button-type="split-button"
action-classes="'btn btn-default btn-sm'"
callback="table.actions.powerOnAll"
item="tCtrl.selected"
disabled="tCtrl.selected.length === 0">
{$ 'Power on' | translate $}
</action>
<menu>
<action button-type="menu-item"
callback="table.actions.powerOffAll"
item="tCtrl.selected"
disabled="tCtrl.selected.length === 0">
{$ 'Power off' | translate $}
</action>
<action button-type="menu-item"
callback="table.putNodesInMaintenanceMode"
item="tCtrl.selected"
disabled="tCtrl.selected.length === 0">
{$ 'Maintenance on' | translate $}
</action>
<action button-type="menu-item"
callback="table.removeNodesFromMaintenanceMode"
item="tCtrl.selected"
disabled="tCtrl.selected.length === 0">
{$ 'Maintenance off' | translate $}
</action>
<action button-type="menu-item"
callback="table.actions.deleteNodes"
item="tCtrl.selected"
disabled="tCtrl.selected.length === 0">
<span class="fa fa-trash"></span>
{$ 'Delete nodes' | translate $}
</action>
</menu>
</action-list>
</th>
</tr>
</td>
</tr>
</tbody>
<tfoot hz-table-footer items="table.nodes"></tfoot>
</table>
<tr>
<th class="multi_select_column">
<input type="checkbox"
hz-select-all="table.nodes"/>
</th>
<th translate class="rsp-p1" st-sort="name" st-sort-default="name">
Node Name
</th>
<th translate class="rsp-p1" st-sort="instance_uuid">
Instance ID
</th>
<th translate class="rsp-p2" st-sort="power_state">
Power State
</th>
<th translate class="rsp-p2" st-sort="provision_state">
Provisioning State
</th>
<th translate class="rsp-p2" st-sort="maintenance">
Maintenance
</th>
<th translate class="rsp-p2" st-sort="ports">
Ports
</th>
<th translate class="rsp-p2" st-sort="driver">
Driver
</th>
<th translate class="actions_column">
Actions
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="node in table.nodes">
<td class="multi_select_column">
<input type="checkbox"
hz-select="node"
ng-model="tCtrl.selections[node.id].checked"/>
</td>
<td class="rsp-p1">
<a href="{$ node.uuid $}">
{$ node.name || node.uuid $}
</a>
</td>
<td class="rsp-p1">
<!-- multiple tags used purposefully to ensure that the link
is removed if node uuid == no instance. Using ng-attr-href
only removed the href and the <a> styling remained which could
cause confusion for users. -->
<a href="/admin/instances/{$ node.instance_uuid $}/detail"
ng-if="node.instance_uuid">
{$ node.instance_uuid $}
</a>
<span ng-if="!node.instance_uuid">{$ 'No Instance' | translate $}</span>
</td>
<td class="rsp-p2" >
<div ng-class="{'running': node['target_power_state']}">
{$ node.power_state $}
</div>
</td>
<td class="rsp-p2">{$ node.provision_state $}</td>
<td class="rsp-p2">{$ node.maintenance | yesno $}</td>
<td class="rsp-p2">{$ node.ports.length $}</td>
<td class="rsp-p2">{$ node.driver $}</td>
<td class="actions_column">
<action-list dropdown>
<action button-type="split-button"
action-classes="'btn btn-default btn-sm'"
callback="table.actions.powerOn"
disabled="node['power_state']!=='power off'"
item="node">
{$ 'Power on' | translate $}
</action>
<menu>
<action button-type="menu-item"
callback="table.actions.powerOff"
disabled="node['power_state']!=='power on'"
item="node">
{$ 'Power off' | translate $}
</action>
<action button-type="menu-item"
callback="table.putNodeInMaintenanceMode"
disabled="node['maintenance']"
item="node">
{$ 'Maintenance on' | translate $}
</action>
<action button-type="menu-item"
callback="table.removeNodeFromMaintenanceMode"
disabled="!node['maintenance']"
item="node">
{$ 'Maintenance off' | translate $}
</action>
<action button-type="menu-item"
callback="table.actions.deleteNode"
disabled="!(node['provision_state']==='available' || node['provision_state']==='nostate' || node['provision_state']==='manageable' || node['provision_state']==='enroll')"
item="node">
<span class="fa fa-trash"></span>
{$ 'Delete node' | translate $}
</action>
<action button-type="menu-item"
callback="table.actions.createPort"
item="node">
{$ 'Create port' | translate $}
</action>
</menu>
</action-list>
</td>
</tr>
</tbody>
<tfoot hz-table-footer items="table.nodes"></tfoot>
</table>
</hz-magic-search-context>
</div>

View File

@ -13,177 +13,28 @@
# License for the specific language governing permissions and limitations
# under the License.
import importlib
import os
import six
# Default to Horizons test settings to avoid any missing keys
from horizon.test.settings import * # noqa
from horizon.utils import secret_key
from openstack_dashboard import exceptions
from openstack_dashboard.test.settings import * # noqa
DEBUG = True
TEMPLATE_DEBUG = DEBUG
# pop these keys to avoid log warnings about deprecation
# update_dashboards will populate them anyway
HORIZON_CONFIG.pop('dashboards', None)
HORIZON_CONFIG.pop('default_dashboard', None)
TEST_DIR = os.path.dirname(os.path.abspath(__file__))
ROOT_PATH = os.path.abspath(os.path.join(TEST_DIR, ".."))
MEDIA_ROOT = os.path.abspath(os.path.join(ROOT_PATH, '..', 'media'))
MEDIA_URL = '/media/'
STATIC_ROOT = os.path.abspath(os.path.join(ROOT_PATH, '..', 'static'))
STATIC_URL = '/static/'
SECRET_KEY = secret_key.generate_or_read_from_file(
os.path.join(TEST_DIR, '.secret_key_store'))
ROOT_URLCONF = 'ironic_ui.test.urls'
TEMPLATE_DIRS = (
os.path.join(TEST_DIR, 'templates'),
)
TEMPLATE_CONTEXT_PROCESSORS += (
'openstack_dashboard.context_processors.openstack',
)
INSTALLED_APPS = (
'django.contrib.contenttypes',
'django.contrib.auth',
'django.contrib.sessions',
'django.contrib.staticfiles',
'django.contrib.messages',
'django.contrib.humanize',
'django_nose',
'openstack_auth',
'compressor',
'horizon',
'openstack_dashboard',
'openstack_dashboard.dashboards',
)
AUTHENTICATION_BACKENDS = ('openstack_auth.backend.KeystoneBackend',)
SITE_BRANDING = 'OpenStack'
HORIZON_CONFIG = {
"password_validator": {
"regex": '^.{8,18}$',
"help_text": "Password must be between 8 and 18 characters."
},
'user_home': None,
'help_url': "http://docs.openstack.org",
'exceptions': {'recoverable': exceptions.RECOVERABLE,
'not_found': exceptions.NOT_FOUND,
'unauthorized': exceptions.UNAUTHORIZED},
'angular_modules': [],
'js_files': [],
}
# Load the pluggable dashboard settings
# Update the dashboards with ironic_ui
import ironic_ui.enabled
import openstack_dashboard.enabled
from openstack_dashboard.utils import settings
dashboard_module_names = [
'openstack_dashboard.enabled',
'openstack_dashboard.local.enabled',
'ironic_ui.enabled',
]
dashboard_modules = []
# All dashboards must be enabled for the namespace to get registered, which is
# needed by the unit tests.
for module_name in dashboard_module_names:
module = importlib.import_module(module_name)
dashboard_modules.append(module)
for submodule in six.itervalues(settings.import_submodules(module)):
if getattr(submodule, 'DISABLED', None):
delattr(submodule, 'DISABLED')
INSTALLED_APPS = list(INSTALLED_APPS) # Make sure it's mutable
settings.update_dashboards(dashboard_modules, HORIZON_CONFIG, INSTALLED_APPS)
settings.update_dashboards(
[
ironic_ui.enabled,
openstack_dashboard.enabled,
],
HORIZON_CONFIG,
INSTALLED_APPS
)
# Set to True to allow users to upload images to glance via Horizon server.
# When enabled, a file form field will appear on the create image form.
# See documentation for deployment considerations.
HORIZON_IMAGES_ALLOW_UPLOAD = True
AVAILABLE_REGIONS = [
('http://localhost:5000/v2.0', 'local'),
('http://remote:5000/v2.0', 'remote'),
]
OPENSTACK_API_VERSIONS = {
"identity": 3
}
OPENSTACK_KEYSTONE_URL = "http://localhost:5000/v2.0"
OPENSTACK_KEYSTONE_DEFAULT_ROLE = "_member_"
OPENSTACK_KEYSTONE_MULTIDOMAIN_SUPPORT = True
OPENSTACK_KEYSTONE_DEFAULT_DOMAIN = 'test_domain'
OPENSTACK_KEYSTONE_BACKEND = {
'name': 'native',
'can_edit_user': True,
'can_edit_group': True,
'can_edit_project': True,
'can_edit_domain': True,
'can_edit_role': True
}
OPENSTACK_CINDER_FEATURES = {
'enable_backup': True,
}
OPENSTACK_NEUTRON_NETWORK = {
'enable_lb': False,
'enable_firewall': False,
'enable_vpn': False,
}
OPENSTACK_HYPERVISOR_FEATURES = {
'can_set_mount_point': True,
# NOTE: as of Grizzly this is not yet supported in Nova so enabling this
# setting will not do anything useful
'can_encrypt_volumes': False
}
LOGGING['loggers']['openstack_dashboard'] = {
'handlers': ['test'],
'propagate': False,
}
LOGGING['loggers']['selenium'] = {
'handlers': ['test'],
'propagate': False,
}
LOGGING['loggers']['ironic_ui'] = {
'handlers': ['test'],
'propagate': False,
}
SECURITY_GROUP_RULES = {
'all_tcp': {
'name': 'ALL TCP',
'ip_protocol': 'tcp',
'from_port': '1',
'to_port': '65535',
},
'http': {
'name': 'HTTP',
'ip_protocol': 'tcp',
'from_port': '80',
'to_port': '80',
},
}
NOSE_ARGS = ['--nocapture',
'--nologcapture',
'--cover-package=openstack_dashboard',
'--cover-inclusive',
'--all-modules']
POLICY_FILES_PATH = os.path.join(ROOT_PATH, "conf")
POLICY_FILES = {
'identity': 'keystone_policy.json',
'compute': 'nova_policy.json'
}
# The openstack_auth.user.Token object isn't JSON-serializable ATM
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
# Ensure any duplicate apps are removed after the update_dashboards call
INSTALLED_APPS = list(set(INSTALLED_APPS))

View File

View File

@ -0,0 +1,21 @@
---
prelude: >
This release adds support for adding and deleting
nodes. Support has also been added for adding and
deleting ports. The panel will now be hidden if the
baremetal service is not present in the scenario where
the collection of running services differs between
multiple keystone regions.
features:
- Add and delete nodes
- Add and delete ports
- Panel hidden if baremetal service or admin rights are not present
- UX improvements across the interface
- Breadcrumbs have been added
issues:
- Currently it is not possible to edit a node via the UI
once it has been enrolled. Therefore, the enrollment must
be done accurately to ensure the node is enrolled accurately
and can then be made available. At present, any errors
made during enrollment can only be corrected by deleting
the node and enrolling it again.

View File

@ -0,0 +1,225 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " applehelp to make an Apple Help Book"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " epub3 to make an epub3"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
@echo " coverage to run coverage check of the documentation (if enabled)"
@echo " dummy to check syntax errors of document sources"
.PHONY: clean
clean:
rm -rf $(BUILDDIR)/*
.PHONY: html
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
.PHONY: dirhtml
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
.PHONY: singlehtml
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
.PHONY: pickle
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
.PHONY: json
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
.PHONY: htmlhelp
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
.PHONY: qthelp
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/ironic-ui.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/ironic-ui.qhc"
.PHONY: applehelp
applehelp:
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
@echo
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
@echo "N.B. You won't be able to view it unless you put it in" \
"~/Library/Documentation/Help or install it in your application" \
"bundle."
.PHONY: devhelp
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/ironic-ui"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/ironic-ui"
@echo "# devhelp"
.PHONY: epub
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
.PHONY: epub3
epub3:
$(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
@echo
@echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."
.PHONY: latex
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
.PHONY: latexpdf
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
.PHONY: latexpdfja
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
.PHONY: text
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
.PHONY: man
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
.PHONY: texinfo
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
.PHONY: info
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
.PHONY: gettext
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
.PHONY: changes
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
.PHONY: linkcheck
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
.PHONY: doctest
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
.PHONY: coverage
coverage:
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
@echo "Testing of coverage in the sources finished, look at the " \
"results in $(BUILDDIR)/coverage/python.txt."
.PHONY: xml
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
.PHONY: pseudoxml
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
.PHONY: dummy
dummy:
$(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
@echo
@echo "Build finished. Dummy builder generates no files."

View File

351
releasenotes/source/conf.py Normal file
View File

@ -0,0 +1,351 @@
# -*- coding: utf-8 -*-
#
# 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.
#
# ironic-ui documentation build configuration file, created by
# sphinx-quickstart on Tue Aug 16 16:29:30 2016.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'reno.sphinxext',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The encoding of source files.
#
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'ironic-ui'
copyright = u'2016, OpenStack'
author = u'OpenStack'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = u'2.0.0'
# The full version, including alpha/beta/rc tags.
release = u'2.0.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#
# today = ''
#
# Else, today_fmt is used as the format for a strftime call.
#
# today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The reST default role (used for this markup: `text`) to use for all
# documents.
#
# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#
# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#
# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
# keep_warnings = False
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'default'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []
# The name for this set of Sphinx documents.
# "<project> v<release> documentation" by default.
#
# html_title = u'ironic-ui v2.0.0'
# A shorter title for the navigation bar. Default is the same as html_title.
#
# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#
# html_logo = None
# The name of an image file (relative to this directory) to use as a favicon of
# the docs. This file should be a Windows icon file (.ico) being 16x16 or
# 32x32 pixels large.
#
# html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
#
# html_extra_path = []
# If not None, a 'Last updated on:' timestamp is inserted at every page
# bottom, using the given strftime format.
# The empty string is equivalent to '%b %d, %Y'.
#
# html_last_updated_fmt = None
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#
# html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#
# html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#
# html_additional_pages = {}
# If false, no module index is generated.
#
# html_domain_indices = True
# If false, no index is generated.
#
# html_use_index = True
# If true, the index is split into individual pages for each letter.
#
# html_split_index = False
# If true, links to the reST sources are added to the pages.
#
# html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#
# html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#
# html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#
# html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
# html_file_suffix = None
# Language to be used for generating the HTML full-text search index.
# Sphinx supports the following languages:
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh'
#
# html_search_language = 'en'
# A dictionary with options for the search language support, empty by default.
# 'ja' uses this config value.
# 'zh' user can custom change `jieba` dictionary path.
#
# html_search_options = {'type': 'default'}
# The name of a javascript file (relative to the configuration directory) that
# implements a search results scorer. If empty, the default will be used.
#
# html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder.
htmlhelp_basename = 'ironic-uidoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'ironic-ui.tex', u'ironic-ui Documentation',
u'OpenStack', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#
# latex_use_parts = False
# If true, show page references after internal links.
#
# latex_show_pagerefs = False
# If true, show URL addresses after external links.
#
# latex_show_urls = False
# Documents to append as an appendix to all manuals.
#
# latex_appendices = []
# It false, will not define \strong, \code, itleref, \crossref ... but only
# \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added
# packages.
#
# latex_keep_old_macro_names = True
# If false, no module index is generated.
#
# latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'ironic-ui', u'ironic-ui Documentation',
[author], 1)
]
# If true, show URL addresses after external links.
#
# man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'ironic-ui', u'ironic-ui Documentation',
author, 'ironic-ui', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#
# texinfo_appendices = []
# If false, no module index is generated.
#
# texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#
# texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#
# texinfo_no_detailmenu = False

View File

@ -0,0 +1,5 @@
============================
Current Series Release Notes
============================
.. release-notes::

View File

@ -0,0 +1,14 @@
========================
Ironic UI Release Notes
========================
.. toctree::
:maxdepth: 1
Current (unreleased) <current-series>
Newton (unreleased) <newton>
.. toctree::
:hidden:
unreleased

View File

@ -0,0 +1,281 @@
@ECHO OFF
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set BUILDDIR=_build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
set I18NSPHINXOPTS=%SPHINXOPTS% .
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
)
if "%1" == "" goto help
if "%1" == "help" (
:help
echo.Please use `make ^<target^>` where ^<target^> is one of
echo. html to make standalone HTML files
echo. dirhtml to make HTML files named index.html in directories
echo. singlehtml to make a single large HTML file
echo. pickle to make pickle files
echo. json to make JSON files
echo. htmlhelp to make HTML files and a HTML help project
echo. qthelp to make HTML files and a qthelp project
echo. devhelp to make HTML files and a Devhelp project
echo. epub to make an epub
echo. epub3 to make an epub3
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. text to make text files
echo. man to make manual pages
echo. texinfo to make Texinfo files
echo. gettext to make PO message catalogs
echo. changes to make an overview over all changed/added/deprecated items
echo. xml to make Docutils-native XML files
echo. pseudoxml to make pseudoxml-XML files for display purposes
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
echo. coverage to run coverage check of the documentation if enabled
echo. dummy to check syntax errors of document sources
goto end
)
if "%1" == "clean" (
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
del /q /s %BUILDDIR%\*
goto end
)
REM Check if sphinx-build is available and fallback to Python version if any
%SPHINXBUILD% 1>NUL 2>NUL
if errorlevel 9009 goto sphinx_python
goto sphinx_ok
:sphinx_python
set SPHINXBUILD=python -m sphinx.__init__
%SPHINXBUILD% 2> nul
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
:sphinx_ok
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
if "%1" == "singlehtml" (
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\ironic-ui.qhcp
echo.To view the help file:
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\ironic-ui.ghc
goto end
)
if "%1" == "devhelp" (
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished.
goto end
)
if "%1" == "epub" (
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub file is in %BUILDDIR%/epub.
goto end
)
if "%1" == "epub3" (
%SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub3 file is in %BUILDDIR%/epub3.
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
if errorlevel 1 exit /b 1
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "latexpdf" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
cd %BUILDDIR%/latex
make all-pdf
cd %~dp0
echo.
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "latexpdfja" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
cd %BUILDDIR%/latex
make all-pdf-ja
cd %~dp0
echo.
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "text" (
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The text files are in %BUILDDIR%/text.
goto end
)
if "%1" == "man" (
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The manual pages are in %BUILDDIR%/man.
goto end
)
if "%1" == "texinfo" (
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
goto end
)
if "%1" == "gettext" (
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
if errorlevel 1 exit /b 1
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
if errorlevel 1 exit /b 1
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
if "%1" == "coverage" (
%SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage
if errorlevel 1 exit /b 1
echo.
echo.Testing of coverage in the sources finished, look at the ^
results in %BUILDDIR%/coverage/python.txt.
goto end
)
if "%1" == "xml" (
%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The XML files are in %BUILDDIR%/xml.
goto end
)
if "%1" == "pseudoxml" (
%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
goto end
)
if "%1" == "dummy" (
%SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy
if errorlevel 1 exit /b 1
echo.
echo.Build finished. Dummy builder generates no files.
goto end
)
:end

View File

@ -0,0 +1,6 @@
===========================
Newton Series Release Notes
===========================
.. release-notes::
:branch: origin/master

View File

@ -0,0 +1 @@
.. include:: current-series.rst

View File

@ -10,9 +10,12 @@ hacking>=0.10.2,<0.11 # Apache-2.0
coverage>=3.6 # Apache-2.0
django-nose>=1.2 # BSD
python-subunit>=0.0.18 # Apache-2.0/BSD
sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 # BSD
oslosphinx>=2.5.0,!=3.4.0 # Apache-2.0
oslotest>=1.10.0 # Apache-2.0
testrepository>=0.0.18 # Apache-2.0/BSD
testscenarios>=0.4 # Apache-2.0/BSD
testtools>=1.4.0 # MIT
# this is required for the docs build jobs
sphinx>=1.2.1,!=1.3b1,<1.3 # BSD
oslosphinx>=2.5.0,!=3.4.0 # Apache-2.0
reno>=1.8.0 # Apache2

View File

@ -59,3 +59,6 @@ commands = oslo_debug_helper {posargs}
show-source = True
builtins = _
exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build
[testenv:releasenotes]
commands = sphinx-build -a -W -E -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html