ironic-ui 2.1.0 release

meta:version: 2.1.0
 meta:diff-start: -
 meta:series: newton
 meta:release-type: release
 meta:announce: openstack-announce@lists.openstack.org
 meta:pypi: yes
 meta:first: no
 meta:release:Author: Elizabeth Elwell <e.r.elwell@gmail.com>
 meta:release:Commit: Elizabeth Elwell <e.r.elwell@gmail.com>
 meta:release:Change-Id: Id98fb9332b79c21c0b8b4d990685be79df5fbd9a
 meta:release:Code-Review+2: Thierry Carrez <thierry@openstack.org>
 meta:release:Code-Review+1: Jim Rollenhagen <jim@jimrollenhagen.com>
 meta:release:Code-Review+2: Davanum Srinivas (dims) <davanum@gmail.com>
 meta:release:Workflow+1: Davanum Srinivas (dims) <davanum@gmail.com>
 -----BEGIN PGP SIGNATURE-----
 Version: GnuPG v1
 
 iQEcBAABAgAGBQJX7Q2kAAoJENljH+rwzGInk78H/AqNTeFEosNrUJYhiwszX49E
 cMn1IAVFhrpGoHPiBlqLwFxs3n2XwgrwXrAqF3EgGNWhAarVIlk+SGkKcKQyW/Yq
 OYIOULO/5PAAj8fTy1xqTe7qL4dsaC/S3qisyJEVdYDHA3OhSddCSQIbHvhjDy4S
 +GhJ3bG5KENtTK3XS9AJlkmg+W6yAk6qUqx01wTUbAE6DWPZjQPpj0zaCRF7wjz3
 jLTa6Xse4eAYaXDFdyUe8Sdx6AEVzRRs2rg1fUqe5d212nhgfxIQld1UJU17GUbF
 pgBT2sMnQ9d6lj3oIHUq4cAOSUyFYGutLBJHyIM512GfByQOMZT9Ql614wphEnU=
 =eifa
 -----END PGP SIGNATURE-----

Merge tag '2.1.0' into debian/newton

ironic-ui 2.1.0 release

  * New upstream release.
  * Correctly run collectstatic and compress after install.
  * Add ironic_ui/static/dashboard/admin/ironic/base-node/base-node.service.js
    to lintian-overrides.

Change-Id: I4d797570a60d68da1c10b5b8a4a2d183e0ee6c03
This commit is contained in:
Thomas Goirand 2016-09-30 11:08:16 +02:00
commit a4d2bb674e
35 changed files with 2781 additions and 1312 deletions

9
debian/changelog vendored
View File

@ -1,3 +1,12 @@
ironic-ui (2.1.0-1) experimental; urgency=medium
* New upstream release.
* Correctly run collectstatic and compress after install.
* Add ironic_ui/static/dashboard/admin/ironic/base-node/base-node.service.js
to lintian-overrides.
-- Thomas Goirand <zigo@debian.org> Fri, 30 Sep 2016 11:09:28 +0200
ironic-ui (2.0.0-1) experimental; urgency=medium
* Initial release. (Closes: #839076)

14
debian/python-ironic-ui.postinst vendored Normal file
View File

@ -0,0 +1,14 @@
#!/bin/sh
set -e
if [ "${1}" = "configure" ] ; then
/usr/share/openstack-dashboard/manage.py collectstatic --clear --noinput
/usr/share/openstack-dashboard/manage.py compress --force
if [ -f /var/lib/openstack-dashboard/secret-key/.secret_key_store ]; then
rm /var/lib/openstack-dashboard/secret-key/.secret_key_store
fi
chown -R www-data /var/lib/openstack-dashboard/secret-key /var/lib/openstack-dashboard/static
fi
#DEBHELPER#

14
debian/python-ironic-ui.postrm vendored Normal file
View File

@ -0,0 +1,14 @@
#!/bin/sh
set -e
if [ "$1" = "remove" ] || [ "$1" = "disappear" ] [ "$1" = "purge" ] ; then
/usr/share/openstack-dashboard/manage.py collectstatic --clear --noinput
/usr/share/openstack-dashboard/manage.py compress --force
if [ -f /var/lib/openstack-dashboard/secret-key/.secret_key_store ]; then
rm /var/lib/openstack-dashboard/secret-key/.secret_key_store
fi
chown -R www-data /var/lib/openstack-dashboard/secret-key /var/lib/openstack-dashboard/static
fi
#DEBHELPER#

View File

@ -1,3 +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)
ironic-ui source: source-is-missing ironic_ui/static/dashboard/admin/ironic/base-node/base-node.service.js line length is 1173 characters (>512)

View File

@ -31,3 +31,4 @@ Administrator's Guide
Introduction to ironic <http://docs.openstack.org/developer/ironic/deploy/user-guide.html>
Installing the ironic UI <installation>
Contributing <contributing>
Release notes <http://docs.openstack.org/releasenotes/ironic-ui>

View File

@ -14,8 +14,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
from django.conf import settings
from ironicclient import client
@ -25,9 +23,7 @@ from horizon.utils.memoized import memoized # noqa
from openstack_dashboard.api import base
LOG = logging.getLogger(__name__)
DEFAULT_IRONIC_API_VERSION = '1.6'
DEFAULT_IRONIC_API_VERSION = '1.11'
DEFAULT_INSECURE = False
DEFAULT_CACERT = None
@ -103,6 +99,19 @@ def node_set_power_state(request, node_id, state):
return ironicclient(request).node.set_power_state(node_id, state)
def node_set_provision_state(request, node_uuid, state):
"""Set the target provision state for a given node.
:param request: HTTP request.
:param node_uuid: The UUID of the node.
:param state: the target provision state to set.
:return: node.
http://docs.openstack.org/developer/python-ironicclient/api/ironicclient.v1.node.html#ironicclient.v1.node.NodeManager.set_provision_state
"""
return ironicclient(request).node.set_provision_state(node_uuid, state)
def node_set_maintenance(request, node_id, state, maint_reason=None):
"""Set the maintenance mode on a given node.
@ -149,6 +158,19 @@ def node_delete(request, node_id):
return ironicclient(request).node.delete(node_id)
def node_update(request, node_id, patch):
"""Update a specified node.
:param request: HTTP request.
:param node_id: The UUID of the node.
:param patch: Sequence of update operations
:return: node.
http://docs.openstack.org/developer/python-ironicclient/api/ironicclient.v1.node.html#ironicclient.v1.node.NodeManager.update
"""
ironicclient(request).node.update(node_id, patch)
def driver_list(request):
"""Retrieve a list of drivers.

View File

@ -69,11 +69,21 @@ class Node(generic.View):
"""Get information on a specific node.
:param request: HTTP request.
:param node_id: Node name or uuid.
:param node_id: Node id.
:return: node.
"""
return ironic.node_get(request, node_id).to_dict()
@rest_utils.ajax(data_required=True)
def patch(self, request, node_id):
"""Update an Ironic node
:param request: HTTP request
:param node_uuid: Node uuid.
"""
patch = request.DATA.get('patch')
return ironic.node_update(request, node_id, patch)
@urls.register
class Ports(generic.View):
@ -130,6 +140,23 @@ class StatesPower(generic.View):
return ironic.node_set_power_state(request, node_id, state)
@urls.register
class StatesProvision(generic.View):
url_regex = r'ironic/nodes/(?P<node_uuid>[0-9a-f-]+)/states/provision$'
@rest_utils.ajax(data_required=True)
def put(self, request, node_uuid):
"""Set the provision state for a specified node.
:param request: HTTP request.
:param node_id: Node uuid
:return: Return code
"""
verb = request.DATA.get('verb')
return ironic.node_set_provision_state(request, node_uuid, verb)
@urls.register
class Maintenance(generic.View):

View File

@ -24,6 +24,7 @@ PANEL_GROUP = 'admin'
ADD_PANEL = 'ironic_ui.content.ironic.panel.Ironic'
# A list of applications to be prepended to INSTALLED_APPS
ADD_INSTALLED_APPS = ['ironic_ui', ]
# A list of AngularJS modules to be loaded when Angular bootstraps.
ADD_ANGULAR_MODULES = ['horizon.dashboard.admin.ironic']
# Automatically discover static resources in installed apps
AUTO_DISCOVER_STATIC_FILES = True

View File

@ -1,16 +1,17 @@
# Akihiro Motoki <amotoki@gmail.com>, 2016. #zanata
# 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"
"Project-Id-Version: ironic-ui 2.0.1.dev1\n"
"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n"
"POT-Creation-Date: 2016-08-15 22:06+0000\n"
"POT-Creation-Date: 2016-08-19 12:56+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"
"PO-Revision-Date: 2016-08-23 07:39+0000\n"
"Last-Translator: Akihiro Motoki <amotoki@gmail.com>\n"
"Language-Team: Japanese\n"
"Language: ja\n"
"X-Generator: Zanata 3.7.3\n"
@ -35,6 +36,16 @@ msgid ""
"Are you sure you want to delete nodes \"%s\"? This action cannot be undone."
msgstr "ノード「%s」 を削除してよろしいですか?この操作は取り消せません。"
#, python-format
msgid ""
"Are you sure you want to delete port \"%s\"? This action cannot be undone."
msgstr "ポート「%s」 を削除してよろしいですか?この操作は取り消せません。"
#, python-format
msgid ""
"Are you sure you want to delete ports \"%s\"? This action cannot be undone."
msgstr "ポート「%s」 を削除してよろしいですか?この操作は取り消せません。"
msgid "Cancel"
msgstr "取り消し"
@ -53,6 +64,12 @@ msgstr "設定"
msgid "Console Enabled"
msgstr "コンソールの有効化"
msgid "Create Port"
msgstr "ポートの作成"
msgid "Create port"
msgstr "ポートの作成"
msgid "Created At"
msgstr "作成時刻"
@ -62,12 +79,21 @@ msgstr "ノードの削除"
msgid "Delete Nodes"
msgstr "ノードの削除"
msgid "Delete Port"
msgstr "ポートの削除"
msgid "Delete Ports"
msgstr "ポートの削除"
msgid "Delete node"
msgstr "ノードの削除"
msgid "Delete nodes"
msgstr "ノードの削除"
msgid "Delete ports"
msgstr "ポートの削除"
msgid "Deploy Kernel"
msgstr "カーネルのデプロイ"
@ -90,6 +116,10 @@ msgstr "ノードの登録"
msgid "Error deleting nodes \"%s\""
msgstr "ノード「%s」の削除中にエラーが発生しました"
#, python-format
msgid "Error deleting ports \"%s\""
msgstr "ポート \"%s\" の削除中にエラーが発生しました"
msgid "Extra"
msgstr "拡張"
@ -117,6 +147,15 @@ msgstr "カーネル"
msgid "Last Error"
msgstr "最後のエラー"
msgid "MAC Address"
msgstr "MAC アドレス"
msgid "MAC address"
msgstr "MAC アドレス"
msgid "MAC address for this port. Required."
msgstr "このポートの MAC アドレス。必須。"
msgid "Maintenance"
msgstr "メンテナンス"
@ -138,6 +177,9 @@ msgstr "インスタンスなし"
msgid "No maintenance reason given."
msgstr "メンテナンスの理由がありません。"
msgid "No network ports have been defined"
msgstr "ネットワークポートが定義されていません"
#, python-format
msgid "Node %s is already in maintenance mode."
msgstr "ノード %s は既にメンテナンスモードです。"
@ -169,6 +211,9 @@ msgstr "ノード名"
msgid "Overview"
msgstr "概要"
msgid "Port successfully created"
msgstr "ポートが正常に作成されました"
msgid "Ports"
msgstr "ポート"
@ -234,6 +279,14 @@ msgstr "ノード「%s」を正常に削除しました"
msgid "Successfully deleted nodes \"%s\""
msgstr "ノード「%s」を正常に削除しました"
#, python-format
msgid "Successfully deleted port \"%s\""
msgstr "ポート \"%s\" を正常に削除しました"
#, python-format
msgid "Successfully deleted ports \"%s\""
msgstr "ポート \"%s\" を正常に削除しました"
msgid "Target Power State"
msgstr "ターゲット電源状態"
@ -247,6 +300,10 @@ msgstr "UUID"
msgid "Unable to create node: %s"
msgstr "ノードを作成できません: %s"
#, python-format
msgid "Unable to create port: %s"
msgstr "ポートを作成できません: %s"
#, python-format
msgid "Unable to delete node \"%s\""
msgstr "ノード 「%s」を削除できません"
@ -255,6 +312,14 @@ msgstr "ノード 「%s」を削除できません"
msgid "Unable to delete node %s: %s"
msgstr "ノード %s を削除できません: %s"
#, python-format
msgid "Unable to delete port \"%s\""
msgstr "ポート \"%s\" を削除できません"
#, python-format
msgid "Unable to delete port: %s"
msgstr "ポートを削除できません: %s"
#, python-format
msgid "Unable to power off the node: %s"
msgstr "ードを電源OFFにできません: %s"

View File

@ -0,0 +1,385 @@
# Andreas Jaeger <jaegerandi@gmail.com>, 2016. #zanata
# HYUNGBAI PARK <openstack.make@gmail.com>, 2016. #zanata
# Ian Y. Choi <ianyrchoi@gmail.com>, 2016. #zanata
# Sungjin Kang <gang.sungjin@gmail.com>, 2016. #zanata
msgid ""
msgstr ""
"Project-Id-Version: ironic-ui 1.1.1.dev41\n"
"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n"
"POT-Creation-Date: 2016-08-17 13:27+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-17 09:15+0000\n"
"Last-Translator: HYUNGBAI PARK <openstack.make@gmail.com>\n"
"Language-Team: Korean (South Korea)\n"
"Language: ko-KR\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 "Extra 추가하기:"
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\"를 삭제하시겠습니까? 이 작업은 취소할 수 없습니다."
#, python-format
msgid ""
"Are you sure you want to delete port \"%s\"? This action cannot be undone."
msgstr "포트 \"%s\"를 삭제하시겠습니까? 이 작업은 취소할 수 없습니다."
#, python-format
msgid ""
"Are you sure you want to delete ports \"%s\"? This action cannot be undone."
msgstr "포트들 \"%s\"를 삭제하시겠습니까? 이 작업은 취소할 수 없습니다."
msgid "Cancel"
msgstr "취소하기"
msgid "Capabilities"
msgstr "기능들"
msgid "Chassis ID"
msgstr "Chassis ID"
msgid "Choose an Image"
msgstr "이미지 선택하기"
msgid "Configuration"
msgstr "구성"
msgid "Console Enabled"
msgstr "콘솔 활성화"
msgid "Create Port"
msgstr "포트 생성하기"
msgid "Create port"
msgstr "포트 생성하기"
msgid "Created At"
msgstr "생성 시점"
msgid "Defaults to ([^\"\\. ]+|\"[^\"]+\")"
msgstr "Defaults to ([^\"\\. ]+|\"[^\"]+\")"
msgid "Delete Node"
msgstr "노드 삭제하기"
msgid "Delete Nodes"
msgstr "노드들 삭제하기"
msgid "Delete Port"
msgstr "포트 삭제하기"
msgid "Delete Ports"
msgstr "포트 삭제하기"
msgid "Delete node"
msgstr "노드 삭제하기"
msgid "Delete nodes"
msgstr "노드들 삭제하기"
msgid "Delete ports"
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\" 삭제 중 에러가 발생했습니다"
#, python-format
msgid "Error deleting ports \"%s\""
msgstr "포트들 \"%s\" 삭제 중 에러가 발생했습니다"
msgid "Extra"
msgstr "Extra"
msgid "Extra Property Name"
msgstr "Extra 속성 명칭"
msgid "Extras"
msgstr "Extra"
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 "MAC Address"
msgstr "MAC 주소"
msgid "MAC address"
msgstr "MAC 주소"
msgid "MAC address for this port. Required."
msgstr "이 포트를 위한 MAC 주소. 필수항목."
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 "유지보수 사유 없습니다."
msgid "No network ports have been defined"
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 "One of this, (.*) must be specified\\."
msgid "Overview"
msgstr "개요"
msgid "Port successfully created"
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 Username"
msgid "Select a Driver"
msgstr "드라이버 선택하기"
#, python-format
msgid "Successfully deleted node \"%s\""
msgstr "노드 \"%s\"를 성공적으로 삭제했습니다"
#, python-format
msgid "Successfully deleted nodes \"%s\""
msgstr "노드들 \"%s\"를 성공적으로 삭제했습니다"
#, python-format
msgid "Successfully deleted port \"%s\""
msgstr "포트 \"%s\"를 성공적으로 삭제했습니다"
#, python-format
msgid "Successfully deleted ports \"%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 create port: %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 delete port \"%s\""
msgstr "포트를 삭제할 수 없습니다: %s"
#, python-format
msgid "Unable to delete port: %s"
msgstr "포트를 삭제할 수 없습니다: %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 "유지보수 모드에서 아이러닉 노드를 넣을 수 없습니다: %s"
#, python-format
msgid "Unable to remove the Ironic node from maintenance mode: %s"
msgstr "유지보수 모드에서 아이러닉 노드를 삭제할 수 없습니다: %s"
#, python-format
msgid "Unable to retrieve Ironic drivers: %s"
msgstr "아이러닉 드라이버를 되찾을 수 없습니다: %s"
msgid "Unable to retrieve Ironic nodes."
msgstr "아이러닉 노드네트워크를 되찾을 수 없습니다."
#, python-format
msgid "Unable to retrieve driver properties: %s"
msgstr "드라이버 속성을 되찾을 수 없습니다 : %s"
#, python-format
msgid "Unable to retrieve the Ironic node ports: %s"
msgstr "아이러닉 노드 포트를 되찾을 수 없습니다: %s"
#, python-format
msgid "Unable to retrieve the Ironic node: %s"
msgstr "아이러닉 노드를 되찾을 수 없습니다: %s "
msgid "Updated At"
msgstr "업데이트 시점"
msgid "default (?:value )?is ([^\"\\. ]+|\"[^\"]+\")"
msgstr "default (?:value )?is ([^\"\\. ]+|\"[^\"]+\")"
msgid "{$ property.getDescription() $}"
msgstr "{$ property.getDescription() $}"

View File

@ -6,14 +6,14 @@
# vuuv <froms2008@gmail.com>, 2016. #zanata
msgid ""
msgstr ""
"Project-Id-Version: ironic-ui 1.1.1.dev33\n"
"Project-Id-Version: ironic-ui 2.0.1.dev1\n"
"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n"
"POT-Creation-Date: 2016-08-15 22:06+0000\n"
"POT-Creation-Date: 2016-08-19 12:56+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"
"PO-Revision-Date: 2016-08-23 03:05+0000\n"
"Last-Translator: sunanchen <KF.sunanchen@h3c.com>\n"
"Language-Team: Chinese (China)\n"
"Language: zh-CN\n"
"X-Generator: Zanata 3.7.3\n"
@ -47,6 +47,16 @@ msgid ""
"Are you sure you want to delete nodes \"%s\"? This action cannot be undone."
msgstr "你确认要删除节点\"%s\"嘛?此操作将不可恢复"
#, python-format
msgid ""
"Are you sure you want to delete port \"%s\"? This action cannot be undone."
msgstr "你确认要删除端口\"%s\"吗?本操作无法恢复"
#, python-format
msgid ""
"Are you sure you want to delete ports \"%s\"? This action cannot be undone."
msgstr "你确认要删除端口\"%s\"吗?本操作无法恢复"
msgid "Cancel"
msgstr "取消"
@ -65,6 +75,12 @@ msgstr "配置"
msgid "Console Enabled"
msgstr "允许控制台"
msgid "Create Port"
msgstr "创建端口"
msgid "Create port"
msgstr "创建端口"
msgid "Created At"
msgstr "创建于"
@ -77,12 +93,21 @@ msgstr "删除节点"
msgid "Delete Nodes"
msgstr "删除多个节点"
msgid "Delete Port"
msgstr "删除端口"
msgid "Delete Ports"
msgstr "删除端口"
msgid "Delete node"
msgstr "删除节点"
msgid "Delete nodes"
msgstr "删除多个节点"
msgid "Delete ports"
msgstr "删除端口"
msgid "Deploy Kernel"
msgstr "部署内核"
@ -105,6 +130,10 @@ msgstr "注册节点"
msgid "Error deleting nodes \"%s\""
msgstr "删除多个节点\"%s\"错误"
#, python-format
msgid "Error deleting ports \"%s\""
msgstr "删除端口\"%s\"错误"
msgid "Extra"
msgstr "额外信息"
@ -138,6 +167,15 @@ msgstr "内核"
msgid "Last Error"
msgstr "最近的一次错误"
msgid "MAC Address"
msgstr "MAC地址"
msgid "MAC address"
msgstr "MAC地址"
msgid "MAC address for this port. Required."
msgstr "需要该端口的MAC地址"
msgid "Maintenance"
msgstr "维护"
@ -159,6 +197,9 @@ msgstr "没有实例"
msgid "No maintenance reason given."
msgstr "缺少提供维护原因"
msgid "No network ports have been defined"
msgstr "未定义网络端口"
#, python-format
msgid "Node %s is already in maintenance mode."
msgstr "节点\"%s\"已经处于维护模式"
@ -193,6 +234,9 @@ msgstr "必须指定其中的一个 (.*)"
msgid "Overview"
msgstr "概览"
msgid "Port successfully created"
msgstr "端口创建成功"
msgid "Ports"
msgstr "端口"
@ -257,6 +301,14 @@ msgstr "成功删除节点\"%s\""
msgid "Successfully deleted nodes \"%s\""
msgstr "成功删除多个节点\"%s\""
#, python-format
msgid "Successfully deleted port \"%s\""
msgstr "删除端口\"%s\"成功"
#, python-format
msgid "Successfully deleted ports \"%s\""
msgstr "成功删除端口\"%s\""
msgid "Target Power State"
msgstr "标记电源状态"
@ -270,6 +322,10 @@ msgstr "UUID"
msgid "Unable to create node: %s"
msgstr "无法创建Ironic节点: %s"
#, python-format
msgid "Unable to create port: %s"
msgstr "无法创建端口: %s"
#, python-format
msgid "Unable to delete node \"%s\""
msgstr "无法删除节点\"%s\""
@ -278,6 +334,14 @@ msgstr "无法删除节点\"%s\""
msgid "Unable to delete node %s: %s"
msgstr "无法删除Ironic节点\"%s\": %s"
#, python-format
msgid "Unable to delete port \"%s\""
msgstr "无法删除端口\"%s\""
#, python-format
msgid "Unable to delete port: %s"
msgstr "无法删除端口: %s"
#, python-format
msgid "Unable to power off the node: %s"
msgstr "无法关闭节点电源: %s"

View File

@ -0,0 +1,303 @@
/*
* 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 support operations on an Ironic node
*/
angular
.module('horizon.dashboard.admin.ironic')
.controller('BaseNodeController', BaseNodeController);
BaseNodeController.$inject = [
'$modalInstance',
'horizon.app.core.openstack-service-api.ironic',
'horizon.app.core.openstack-service-api.glance',
'horizon.dashboard.admin.ironic.base-node.service',
'horizon.dashboard.admin.ironic.validHostNamePattern',
'$log',
'ctrl'
];
function BaseNodeController($modalInstance,
ironic,
glance,
baseNodeService,
validHostNamePattern,
$log,
ctrl) {
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;
ctrl.modalTitle = gettext("Node");
ctrl.submitButtonTitle = gettext("Submit");
ctrl.showInstanceInfo = false;
// Node object suitable for Ironic api
ctrl.node = {
name: null,
driver: null,
driver_info: {},
properties: {},
extra: {}
};
/**
* @description Get the list of currently active Ironic drivers
*
* @return {void}
*/
ctrl._loadDrivers = function() {
return ironic.getDrivers().then(function(response) {
ctrl.drivers = response.data.items;
});
};
/**
* @description Get the list of images from Glance
*
* @return {void}
*/
ctrl._getImages = function() {
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 baseNodeService.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;
return ironic.getDriverProperties(driverName).then(function(response) {
ctrl.driverProperties = {};
angular.forEach(response.data, function(desc, property) {
ctrl.driverProperties[property] =
new baseNodeService.DriverProperty(property,
desc,
ctrl.driverProperties);
});
ctrl.driverPropertyGroups = ctrl._sortDriverProperties();
ctrl.loadingDriverProperties = false;
});
};
/**
* @description Cancel the current node operation
*
* @return {void}
*/
ctrl.cancel = function() {
$modalInstance.dismiss('cancel');
};
/**
* @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,350 @@
<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>{$ ctrl.modalTitle $}</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>
<!--base node form-->
<form id="baseNodeForm"
name="baseNodeForm">
<!--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': baseNodeForm.name.$invalid &&
baseNodeForm.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 add-property-->
<form id="addPropertyForm"
name="addPropertyForm">
<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>
</form>
<!--properties property-list-->
<form id="propertiesForm"
name="propertiesForm">
<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>
</form>
<!--extras add-property-->
<form id="addExtraForm"
name="addExtraForm">
<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>
</form>
<!--extras property-list-->
<form id="extraForm"
name="extraForm">
<div class="form-group">
<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>
</form>
<!--instance-info add-property-->
<form ng-if="ctrl.showInstanceInfo"
id="addInstancePropertyForm"
name="addInstancePropertyForm">
<div class="form-group">
<label for="instanceProperty"
class="control-label"
translate>Instance Info</label>
<div class="input-group input-group-sm">
<span class="input-group-addon"
style="width:25%;text-align:right"
translate>
Add New Instance Property:</span>
<input class="form-control"
id="instanceProperty"
type="text"
ng-model="instancePropertyName"
validate-unique="ctrl.checkInstancePropertyUnique"
placeholder="{$ 'Instance Property Name' | translate $}"/>
<span class="input-group-btn">
<button class="btn btn-primary"
type="button"
ng-disabled="!instancePropertyName ||
addInstancePropertyForm.$invalid"
ng-click="ctrl.node.instance_info[instancePropertyName] = null;
instancePropertyName = null">
<span class="fa fa-plus"> </span>
</button>
</span>
</div>
</div>
</form>
<!--instance-info property-list-->
<form ng-if="ctrl.showInstanceInfo"
id="instanceInfoForm"
name="instanceInfoForm">
<div class="form-group">
<div class="input-group input-group-sm"
ng-repeat="(propertyName, propertyValue) in ctrl.node.instance_info">
<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.instance_info[propertyName]"
ng-required="true"/>
<div class="input-group-btn">
<a class="btn btn-default"
ng-click="ctrl.deleteInstanceProperty(propertyName)">
<span class="fa fa-minus"> </span>
</a>
</div>
</div>
</div>
</form>
</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': baseNodeForm.{$ name $}.$invalid &&
baseNodeForm.{$ 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 base 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 ||
propertiesForm.$invalid ||
extraForm.$invalid ||
instanceInfoForm.$invalid"
ng-click="ctrl.submit()"
class="btn btn-primary"
translate>
{$ ctrl.submitButtonTitle $}
</button>
</div>

View File

@ -0,0 +1,673 @@
/*
* 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.base-node.service',
baseNodeService);
baseNodeService.$inject = [
'$modal',
'$log',
'horizon.dashboard.admin.ironic.validHostNamePattern',
'horizon.dashboard.admin.ironic.validUuidPattern'
];
function baseNodeService($modal,
$log,
validHostNamePattern,
validUuidPattern) {
var service = {
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)://.+$");
/**
The DriverProperty class is used to represent an ironic driver
property. It is currently used by the base-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

@ -18,7 +18,7 @@
"use strict";
describe(
'horizon.dashboard.admin.ironic.enroll-node.service',
'horizon.dashboard.admin.ironic.base-node.service',
function() {
var service;
@ -32,7 +32,7 @@
beforeEach(inject(function($injector) {
service =
$injector.get('horizon.dashboard.admin.ironic.enroll-node.service');
$injector.get('horizon.dashboard.admin.ironic.base-node.service');
}));
it('defines the service', function() {

View File

@ -0,0 +1,184 @@
/*
* 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 edit an existing Ironic node
*/
angular
.module('horizon.dashboard.admin.ironic')
.controller('EditNodeController', EditNodeController);
EditNodeController.$inject = [
'$rootScope',
'$controller',
'$modalInstance',
'horizon.framework.widgets.toast.service',
'horizon.app.core.openstack-service-api.ironic',
'horizon.dashboard.admin.ironic.events',
'horizon.dashboard.admin.ironic.edit-node.service',
'$log',
'node'
];
function EditNodeController($rootScope,
$controller,
$modalInstance,
toastService,
ironic,
ironicEvents,
editNodeService,
$log,
node) {
var ctrl = this;
$controller('BaseNodeController',
{ctrl: ctrl,
$modalInstance: $modalInstance});
ctrl.modalTitle = gettext("Edit Node");
ctrl.submitButtonTitle = gettext("Update Node");
ctrl.node.instance_info = {};
ctrl.showInstanceInfo = true;
ctrl.baseNode = null;
init(node);
function init(node) {
ctrl._loadDrivers().then(function() {
_loadNodeData(node.uuid);
});
ctrl._getImages();
}
function _loadNodeData(nodeId) {
ironic.getNode(nodeId).then(function(response) {
var node = response.data;
ctrl.baseNode = node;
ctrl.node.name = node.name;
for (var i = 0; i < ctrl.drivers.length; i++) {
if (ctrl.drivers[i].name === node.driver) {
ctrl.selectedDriver = ctrl.drivers[i];
break;
}
}
ctrl.loadDriverProperties(node.driver).then(function() {
angular.forEach(node.driver_info, function(value, property) {
if (angular.isDefined(ctrl.driverProperties[property])) {
ctrl.driverProperties[property].inputValue = value;
}
});
});
ctrl.node.properties = angular.copy(node.properties);
ctrl.node.extra = angular.copy(node.extra);
ctrl.node.instance_info = angular.copy(node.instance_info);
ctrl.node.uuid = node.uuid;
});
}
/**
* @description Delete a node instance property
*
* @param {string} propertyName - Name of the property
* @return {void}
*/
ctrl.deleteInstanceProperty = function(propertyName) {
delete ctrl.node.instance_info[propertyName];
};
/**
* @description Check whether the specified node instance property
* already exists
*
* @param {string} propertyName - Name of the instance property
* @return {boolean} True if the property already exists,
* otherwise false
*/
ctrl.checkInstancePropertyUnique = function(propertyName) {
return !(propertyName in ctrl.node.instance_info);
};
/**
* @description Construct a patch that converts source node into
* target node
*
* @param {object} sourceNode - Source node
* @param {object} targetNode - Target node
* @return {object[]} Array of patch instructions
*/
function buildPatch(sourceNode, targetNode) {
var patcher = new editNodeService.NodeUpdatePatch();
patcher.buildPatch(sourceNode.name, targetNode.name, "/name");
patcher.buildPatch(sourceNode.driver, targetNode.driver, "/driver");
patcher.buildPatch(sourceNode.properties,
targetNode.properties,
"/properties");
patcher.buildPatch(sourceNode.extra,
targetNode.extra,
"/extra");
patcher.buildPatch(sourceNode.driver_info,
targetNode.driver_info,
"/driver_info");
patcher.buildPatch(sourceNode.instance_info,
targetNode.instance_info,
"/instance_info");
return patcher.getPatch();
}
ctrl.submit = function() {
$modalInstance.close();
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;
}
});
$log.info("Updating node " + JSON.stringify(ctrl.baseNode));
$log.info("to " + JSON.stringify(ctrl.node));
var patch = buildPatch(ctrl.baseNode, ctrl.node);
$log.info("patch = " + JSON.stringify(patch.patch));
if (patch.status === editNodeService.NodeUpdatePatch.status.OK) {
ironic.updateNode(ctrl.baseNode.uuid, patch.patch).then(function() {
$rootScope.$emit(ironicEvents.EDIT_NODE_SUCCESS);
});
} else {
toastService.add('error',
gettext('Unable to create node update patch.'));
}
};
}
})();

View File

@ -0,0 +1,204 @@
/*
* 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.edit-node.service',
editNodeService);
editNodeService.$inject = [
'$modal',
'horizon.dashboard.admin.basePath',
'$log'
];
function editNodeService($modal, basePath, $log) {
var service = {
modal: modal,
NodeUpdatePatch: NodeUpdatePatch
};
function modal(node) {
var options = {
controller: 'EditNodeController as ctrl',
backdrop: 'static',
resolve: {
node: function() {
return node;
}
},
templateUrl: basePath + '/ironic/base-node/base-node.html'
};
return $modal.open(options);
}
/*
The NodeUpdatePatch class is used to construct a set of patch
instructions that transform a base node into a specified target.
This class supports the edit-node functionality.
*/
function NodeUpdatePatch() {
this.patch = [];
this.status = NodeUpdatePatch.status.OK;
}
NodeUpdatePatch.status = {
OK: 0,
ERROR: 1,
UNKNOWN_TYPE: 2
};
/**
* @description Update the status of the patch with a specified code
*
* @param {int} status - latest status code
* @return {void}
*/
NodeUpdatePatch.prototype._updateStatus = function(status) {
this.status = Match.max(this.status, status);
};
/**
* @description Check whether an item is a property
*
* @param {object} item - item to be tested
* @return {boolean} True if the item is a number, string, or date
*/
function isProperty(item) {
return angular.isNumber(item) ||
angular.isString(item) ||
angular.isDate(item);
}
/**
* @description Check whether an item is a collection
*
* @param {object} item - item to be tested
* @return {boolean} True if the item is an array or object
*/
function isCollection(item) {
return angular.isArray(item) || angular.isObject(item);
}
/**
* @description Add instructions to the patch for processing a
* specified item
*
* @param {object} item - item to be added
* @param {string} path - Path to the item being added
* @param {string} op - add or remove
* @return {void}
*/
NodeUpdatePatch.prototype._processItem = function(item, path, op) {
$log.info("NodeUpdatePatch._processItem: " + path + " " + op);
if (isProperty(item)) {
this.patch.push({op: op, path: path, value: item});
} else if (isCollection(item)) {
angular.forEach(item, function(partName, part) {
this._processItem(part, path + "/" + partName, op);
});
} else {
this._updateStatus(NodeUpdatePatch.status.UNKNOWN_TYPE);
$log.error("Unable to process (" + op + ") item (" + path + "). " +
" " + typeof item + " " + JSON.stringify(item));
}
};
/**
* @description Add instructions to the patch for adding a specified item
*
* @param {object} item - item to be added
* @param {string} path - Path to the item being removed
* @return {void}
*/
NodeUpdatePatch.prototype._addItem = function(item, path) {
this._processItem(item, path, "add");
};
/**
* @description Add instructions to the patch for removing a specified item
*
* @param {object} item - item to be removed
* @param {string} path - Path to the item being removed
* @return {void}
*/
NodeUpdatePatch.prototype._removeItem = function(item, path) {
this._processItem(item, path, "remove");
};
/**
* @description Determine the set of operations required to
* transform a source version of an object into a target version,
* and add them to a patch.
*
* @param {object} source - Source object
* @param {object} target - Target object
* @param {string} path - Pathname of the patched object
* @return {void}
*/
NodeUpdatePatch.prototype.buildPatch = function(source, target, path) {
$log.info("NodeUpdatePatch._buildPatch: " + path);
var patcher = this;
if (isProperty(source) && isProperty(target)) {
if (source !== target) {
patcher.patch.push({op: "replace", path: path, value: target});
}
} else if (isCollection(source) && isCollection(target)) {
angular.forEach(source, function(sourceItem, sourceItemName) {
if (angular.isDefined(target[sourceItemName])) {
patcher.buildPatch(sourceItem,
target[sourceItemName],
path + '/' + sourceItemName);
} else {
patcher._removeItem(sourceItem, path + '/' + sourceItemName);
}
});
angular.forEach(target, function(targetItem, targetItemName) {
if (angular.isUndefined(source[targetItemName])) {
patcher._addItem(targetItem, path + '/' + targetItemName);
}
});
} else if (isProperty(source) && isCollection(target) ||
isCollection(source) && isProperty(target)) {
patcher._removeItem(source, path);
patcher._addItem(target, path);
} else {
patcher._updateStatus(NodeUpdatePatch.status.ERROR);
$log.error("Unable to patch " + path + " " +
"source = " + JSON.stringify(source) + ", " +
"target = " + JSON.stringify(target));
}
};
/**
* @description Get the patch
*
* @return {object} An object with two properties:
* patch: Array of patch instructions compatible with the Ironic
* node update function
* status: Code indicating whether patch creation was successful
*
*/
NodeUpdatePatch.prototype.getPatch = function() {
return {patch: angular.copy(this.patch), status: this.status};
};
return service;
}
})();

View File

@ -25,241 +25,37 @@
EnrollNodeController.$inject = [
'$rootScope',
'$controller',
'$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,
$controller,
$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;
$controller('BaseNodeController',
{ctrl: ctrl,
$modalInstance: $modalInstance});
// Parameter object that defines the node to be enrolled
ctrl.node = {
name: null,
driver: null,
driver_info: {},
properties: {},
extra: {}
};
ctrl.modalTitle = gettext("Enroll Node");
ctrl.submitButtonTitle = ctrl.modalTitle;
init();
function init() {
loadDrivers();
getImages();
ctrl._loadDrivers();
ctrl._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()");
ctrl.submit = function() {
$log.debug(">> EnrollNodeController.submit()");
angular.forEach(ctrl.driverProperties, function(property, name) {
$log.debug(name +
", required = " + property.isRequired() +
@ -276,68 +72,19 @@
});
ironic.createNode(ctrl.node).then(
function() {
function(response) {
$log.info("create node response = " + JSON.stringify(response));
$modalInstance.close();
$rootScope.$emit(ironicEvents.ENROLL_NODE_SUCCESS);
if (ctrl.moveNodeToManageableState) {
$log.info("Setting node provision state");
ironic.setNodeProvisionState(response.data.uuid, 'manage');
}
},
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();
$log.debug("<< EnrollNodeController.submit()");
};
}
})();

View File

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

@ -16,35 +16,6 @@
(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',
@ -52,634 +23,23 @@
enrollNodeService.$inject = [
'$modal',
'horizon.dashboard.admin.basePath',
'$log',
'horizon.dashboard.admin.ironic.validHostNamePattern',
'horizon.dashboard.admin.ironic.validUuidPattern'
'horizon.dashboard.admin.basePath'
];
function enrollNodeService($modal,
basePath,
$log,
validHostNamePattern,
validUuidPattern) {
function enrollNodeService($modal, basePath) {
var service = {
modal: modal,
DriverProperty: DriverProperty,
Graph: Graph
modal: modal
};
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'
templateUrl: basePath + '/ironic/base-node/base-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

@ -38,6 +38,7 @@
return {
ENROLL_NODE_SUCCESS:'horizon.dashboard.admin.ironic.ENROLL_NODE_SUCCESS',
DELETE_NODE_SUCCESS:'horizon.dashboard.admin.ironic.DELETE_NODE_SUCCESS',
EDIT_NODE_SUCCESS:'horizon.dashboard.admin.ironic.EDIT_NODE_SUCCESS',
CREATE_PORT_SUCCESS:'horizon.dashboard.admin.ironic.CREATE_PORT_SUCCESS',
DELETE_PORT_SUCCESS:'horizon.dashboard.admin.ironic.DELETE_PORT_SUCCESS'
};

View File

@ -18,6 +18,41 @@
(function () {
'use strict';
var provisionStateTransitionMatrix = {
enroll: {
manageable: 'manage'
},
manageable: {
active: 'adopt',
available: 'provide'
},
active: {
manageable: 'deleted'
},
available: {
active: 'active',
manageable: 'manage'
},
'adopt failed': {
manageable: 'manage',
active: 'adopt'
},
'inspect failed': {
manageable: 'manage'
},
'clean failed': {
manageable: 'manage'
},
'deploy failed': {
active: 'active',
manageable: 'deleted'
},
error: {
active: 'rebuild',
manageable: 'deleted'
}
};
angular
.module('horizon.app.core.openstack-service-api')
.factory('horizon.app.core.openstack-service-api.ironic', ironicAPI);
@ -45,10 +80,13 @@
getNode: getNode,
getNodes: getNodes,
getPortsWithNode: getPortsWithNode,
getProvisionStateTransitionVerb: getProvisionStateTransitionVerb,
powerOffNode: powerOffNode,
powerOnNode: powerOnNode,
putNodeInMaintenanceMode: putNodeInMaintenanceMode,
removeNodeFromMaintenanceMode: removeNodeFromMaintenanceMode
removeNodeFromMaintenanceMode: removeNodeFromMaintenanceMode,
setNodeProvisionState: setNodeProvisionState,
updateNode: updateNode
};
return service;
@ -199,6 +237,34 @@
});
}
/**
* @description Set the target provision state of the node.
*
* http://docs.openstack.org/developer/ironic/webapi/v1.html#
* put--v1-nodes-(node_ident)-states-provision
*
* @param {string} uuid UUID of a node.
* @param {string} verb Provisioning verb used to move node to desired
* target state
* @return {promise} Promise
*/
function setNodeProvisionState(uuid, verb) {
var data = {
verb: verb
};
return apiService.put('/api/ironic/nodes/' + uuid + '/states/provision',
data)
.success(function() {
var msg = gettext(
'A request has been made to change the provisioning state of node %s');
toastService.add('success', interpolate(msg, [uuid], false));
})
.error(function(reason) {
var msg = gettext('Unable to set node provision state: %s');
toastService.add('error', interpolate(msg, [reason], false));
});
}
/**
* @description Create an Ironic node
*
@ -245,6 +311,32 @@
});
}
/**
* @description Update the definition of a specified node.
*
* http://docs.openstack.org/developer/ironic/webapi/v1.html#
* patch--v1-nodes-(node_ident)
*
* @param {string} uuid UUID of a node.
* @param {object[]} patch Sequence of update operations
* @return {promise} Promise
*/
function updateNode(uuid, patch) {
var data = {
patch: patch
};
return apiService.patch('/api/ironic/nodes/' + uuid, data)
.success(function() {
var msg = gettext(
'Successfully updated node %s');
toastService.add('success', interpolate(msg, [uuid], false));
})
.error(function(reason) {
var msg = gettext('Unable to update node %s: %s');
toastService.add('error', interpolate(msg, [uuid, reason], false));
});
}
/**
* @description Retrieve the list of Ironic drivers
*
@ -319,6 +411,25 @@
toastService.add('error', interpolate(msg, [reason], false));
});
}
/**
* @description Get the verb used to transition a node from a source
* provision-state to a target provision-state
*
* @param {string} sourceState source state
* @param {string} targetState target state
* @return {string} Verb used to transition from source to target state.
* null if the requested transition is not allowed.
*/
function getProvisionStateTransitionVerb(sourceState, targetState) {
var verb = null;
if (angular.isDefined(provisionStateTransitionMatrix[sourceState]) &&
angular.isDefined(
provisionStateTransitionMatrix[sourceState][targetState])) {
verb = provisionStateTransitionMatrix[sourceState][targetState];
}
return verb;
}
}
}());

View File

@ -9,6 +9,7 @@
<input type="text"
class="form-control input-sm"
ng-model="maintReason"
auto-focus
placeholder=""/>
</div>
</div>
@ -28,4 +29,3 @@
Put Node(s) Into Maintenance Mode
</button>
</div>

View File

@ -75,6 +75,7 @@
deleteNodes: deleteNodes,
deletePort: deletePort,
deletePorts: deletePorts,
getProvisionStateTransitionVerb: getProvisionStateTransitionVerb,
powerOn: powerOn,
powerOff: powerOff,
powerOnAll: powerOnNodes,
@ -82,7 +83,8 @@
putNodeInMaintenanceMode: putInMaintenanceMode,
removeNodeFromMaintenanceMode: removeFromMaintenanceMode,
putAllInMaintenanceMode: putNodesInMaintenanceMode,
removeAllFromMaintenanceMode: removeNodesFromMaintenanceMode
removeAllFromMaintenanceMode: removeNodesFromMaintenanceMode,
setProvisionState: setProvisionState
};
return service;
@ -191,6 +193,24 @@
return applyFuncToNodes(removeFromMaintenanceMode, nodes);
}
/*
* @name horizon.dashboard.admin.ironic.actions.setProvisionState
* @description Set the provisioning state of a specified node
*
* @param {object} args - Object with two properties named 'node'
* and 'verb'.
* node: node object.
* verb: string the value of which is the verb used to move
* the node to the desired target state for the node.
*/
function setProvisionState(args) {
ironic.setNodeProvisionState(args.node.uuid, args.verb);
}
function getProvisionStateTransitionVerb(sourceState, targetState) {
return ironic.getProvisionStateTransitionVerb(sourceState, targetState);
}
function createPort(node) {
return createPortService.modal(node);
}

View File

@ -26,10 +26,12 @@
'$scope',
'$rootScope',
'$location',
'horizon.framework.widgets.toast.service',
'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.edit-node.service',
'horizon.dashboard.admin.ironic.maintenance.service',
'horizon.dashboard.admin.ironic.validUuidPattern'
];
@ -37,10 +39,12 @@
function IronicNodeDetailsController($scope,
$rootScope,
$location,
toastService,
ironic,
ironicEvents,
actions,
basePath,
editNodeService,
maintenanceService,
validUuidPattern) {
var ctrl = this;
@ -61,6 +65,7 @@
}
];
ctrl.node = null;
ctrl.ports = [];
ctrl.portsSrc = [];
ctrl.basePath = basePath;
@ -69,9 +74,17 @@
ctrl.getVifPortId = getVifPortId;
ctrl.putNodeInMaintenanceMode = putNodeInMaintenanceMode;
ctrl.removeNodeFromMaintenanceMode = removeNodeFromMaintenanceMode;
ctrl.editNode = editNode;
ctrl.createPort = createPort;
ctrl.deletePort = deletePort;
ctrl.deletePorts = deletePorts;
ctrl.refresh = refresh;
var editNodeHandler =
$rootScope.$on(ironicEvents.EDIT_NODE_SUCCESS,
function() {
init();
});
var createPortHandler =
$rootScope.$on(ironicEvents.CREATE_PORT_SUCCESS,
@ -87,6 +100,7 @@
});
$scope.$on('$destroy', function() {
editNodeHandler();
createPortHandler();
deletePortHandler();
});
@ -119,9 +133,19 @@
* @return {promise} promise
*/
function retrieveNode(uuid) {
var lastError = ctrl.node ? ctrl.node.last_error : null;
return ironic.getNode(uuid).then(function (response) {
ctrl.node = response.data;
ctrl.node.id = uuid;
if (lastError &&
ctrl.node.last_error !== "" &&
ctrl.node.last_error !== lastError) {
toastService.add(
'error',
"Node " + ctrl.node.name + ". " + ctrl.node.last_error);
}
});
}
@ -176,6 +200,10 @@
maintenanceService.removeNodeFromMaintenanceMode(ctrl.node);
}
function editNode() {
editNodeService.modal(ctrl.node);
}
/**
* @name horizon.dashboard.admin.ironic.NodeDetailsController.createPort
* @description Initiate creation of a newtwork port for the current
@ -212,5 +240,15 @@
});
ctrl.actions.deletePorts(selectedPorts);
}
/**
* @name horizon.dashboard.admin.ironic.NodeDetailsController.refresh
* @description Update node information
*
* @return {void}
*/
function refresh() {
init();
}
}
})();

View File

@ -2,31 +2,52 @@
ng-controller="horizon.dashboard.admin.ironic.NodeDetailsController as ctrl">
<div class="pull-right">
<button class="btn btn-default btn-sm"
style="margin-right:10px;"
ng-click="ctrl.refresh()">
<span translate>Refresh</span>
</button>
<action-list dropdown>
<action button-type="split-button"
action-classes="'btn btn-default btn-sm'"
callback="ctrl.actions.powerOn"
item="ctrl.node"
disabled="ctrl.node['power_state']!=='power off'">
disabled="ctrl.node.power_state!=='power off'">
{$ 'Power on' | translate $}
</action>
<menu>
<action button-type="menu-item"
callback="ctrl.actions.powerOff"
item="ctrl.node"
disabled="ctrl.node['power_state']!=='power on'">
disabled="ctrl.node.power_state!=='power on'">
{$ 'Power off' | translate $}
</action>
<action button-type="menu-item"
callback="ctrl.putNodeInMaintenanceMode"
disabled="ctrl.node['maintenance']">
disabled="ctrl.node.maintenance">
{$ 'Maintenance on' | translate $}
</action>
<action button-type="menu-item"
callback="ctrl.removeNodeFromMaintenanceMode"
disabled="!ctrl.node['maintenance']">
disabled="!ctrl.node.maintenance">
{$ 'Maintenance off' | translate $}
</action>
<action ng-repeat="targetState in ['manageable', 'available', 'active']"
button-type="menu-item"
callback="ctrl.actions.setProvisionState"
item="{node: ctrl.node,
verb: ctrl.actions.getProvisionStateTransitionVerb(
ctrl.node.provision_state,
targetState)}"
disabled="ctrl.actions.getProvisionStateTransitionVerb(
ctrl.node.provision_state,
targetState) === null">
{$ ('Move to ' | translate) + targetState $}
</action>
<action button-type="menu-item"
callback="ctrl.editNode">
{$ 'Edit' | translate $}
</action>
</menu>
</action-list>
</div>

View File

@ -6,13 +6,13 @@
<hr class="header_rule">
<dl class="dl-horizontal">
<dt translate>Node ID</dt>
<dd>{$ ctrl.node.uuid $}</dd>
<dd>{$ ctrl.node.uuid | noValue $}</dd>
<dt translate>Chassis ID</dt>
<dd>{$ ctrl.node.chassis_uuid | noValue $}</dd>
<dt translate>Created At</dt>
<dd>{$ ctrl.node.created_at | date:'medium' $}</dd>
<dd>{$ ctrl.node.created_at | date:'medium' | noValue $}</dd>
<dt translate>Extra</dt>
<dd>{$ ctrl.node.extra $}</dd>
<dd>{$ ctrl.node.extra | noValue $}</dd>
</dl>
</div>
@ -114,7 +114,7 @@
<dl class="dl-horizontal">
<dt ng-repeat-start="(propertyName, propertyValue) in ctrl.node.properties">
{$ propertyName $}</dt>
<dd ng-repeat-end>{$ propertyValue $}</dd>
<dd ng-repeat-end>{$ propertyValue | noValue $}</dd>
</dl>
</div>
@ -125,29 +125,29 @@
<div ng-switch="ctrl.node.driver">
<dl ng-switch-when="pxe_ssh" class="dl-horizontal">
<dt translate>Driver</dt>
<dd>{$ ctrl.node.driver $}</dd>
<dd>{$ ctrl.node.driver | noValue $}</dd>
<dt translate>SSH Port</dt>
<dd>{$ ctrl.node.driver_info.ssh_port $}</dd>
<dd>{$ ctrl.node.driver_info.ssh_port | noValue $}</dd>
<dt translate>SSH Username</dt>
<dd>{$ ctrl.node.driver_info.ssh_username $}</dd>
<dd>{$ ctrl.node.driver_info.ssh_username | noValue $}</dd>
<dt translate>Deploy Kernel</dt>
<dd>
<a ng-if="deploy_kernel_is_uuid = ctrl.isUuid(ctrl.node.driver_info.deploy_kernel)"
href="/dashboard/admin/images/{$ ctrl.node.driver_info.deploy_kernel $}/detail">
{$ ctrl.node.driver_info.deploy_kernel $}
{$ ctrl.node.driver_info.deploy_kernel | noValue $}
</a>
<span ng-if="!deploy_kernel_is_uuid">
{$ ctrl.node.driver_info.deploy_kernel $}
{$ ctrl.node.driver_info.deploy_kernel | noValue $}
</span>
</dd>
<dt translate>Deploy Ramdisk</dt>
<dd>
<a ng-if="deploy_ramdisk_is_uuid = ctrl.isUuid(ctrl.node.driver_info.deploy_ramdisk)"
href="/dashboard/admin/images/{$ ctrl.node.driver_info.deploy_ramdisk $}/detail">
{$ ctrl.node.driver_info.deploy_ramdisk $}
{$ ctrl.node.driver_info.deploy_ramdisk | noValue $}
</a>
<span ng-if="!deploy_ramdisk_is_uuid">
{$ ctrl.node.driver_info.deploy_ramdisk $}
{$ ctrl.node.driver_info.deploy_ramdisk | noValue $}
</span>
</dd>
</dl>
@ -160,15 +160,6 @@
</div>
<div class="row">
<!-- Capabilities -->
<div class="col-md-6 status detail">
<h4 translate>Capabilities</h4>
<hr class="header_rule">
<dl class="dl-horizontal">
<dd>{$ ctrl.node.capabilities $}</dd>
</dl>
</div>
<!-- Instance Info -->
<div class="col-md-6 status detail">
<h4 translate>Instance Info</h4>
@ -176,23 +167,23 @@
<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>
<dd>{$ ctrl.node.instance_info.display_name | noValue $}</dd>
<dt translate>Ramdisk</dt>
<dd>
<a href="/dashboard/admin/images/{$ ctrl.node.instance_info.ramdisk $}/detail">
{$ ctrl.node.instance_info.ramdisk $}
{$ ctrl.node.instance_info.ramdisk | noValue $}
</a>
</dd>
<dt translate>Kernel</dt>
<dd>
<a href="/dashboard/admin/images/{$ ctrl.node.instance_info.kernel $}/detail">
{$ ctrl.node.instance_info.kernel $}
{$ ctrl.node.instance_info.kernel | noValue $}
</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>
<dd ng-repeat-end>{$ value | noValue $}</dd>
</dl>
</div>
</div>

View File

@ -6,19 +6,19 @@
<hr class="header_rule">
<dl class="dl-horizontal">
<dt translate>Name</dt>
<dd>{$ ctrl.node['name'] $}</dd>
<dd>{$ ctrl.node.name | noValue $}</dd>
<dt translate>Maintenance</dt>
<dd>{$ ctrl.node['maintenance'] ? 'True' : 'False' $}</dd>
<dd>{$ ctrl.node.maintenance | yesno $}</dd>
<dt translate>Maintenance Reason</dt>
<dd>{$ ctrl.node['maintenance_reason'] $}</dd>
<dd>{$ ctrl.node.maintenance_reason | noValue $}</dd>
<dt translate>Inspection Started At</dt>
<dd>{$ ctrl.node['inspection_started_at'] $}</dd>
<dd>{$ ctrl.node.inspection_started_at | date: 'medium' | noValue $}</dd>
<dt translate>Inspection Finished At</dt>
<dd>{$ ctrl.node['inspection_finished_at'] $}</dd>
<dd>{$ ctrl.node.inspection_finished_at | date: 'medium' | noValue $}</dd>
<dt translate>Reservation</dt>
<dd>{$ ctrl.node['reservation'] $}</dd>
<dd>{$ ctrl.node.reservation | noValue $}</dd>
<dt translate>Console Enabled</dt>
<dd>{$ ctrl.node['console_enabled'] ? 'True' : 'False' $}</dd>
<dd>{$ ctrl.node.console_enabled | yesno $}</dd>
</dl>
</div>
@ -29,22 +29,22 @@
<dl class="dl-horizontal">
<dt translate>Instance ID</dt>
<dd>
<a href="/admin/instances/{$ ctrl.node['instance_uuid'] $}/detail">
{$ ctrl.node['instance_uuid'] $}
<a href="/admin/instances/{$ ctrl.node.instance_uuid $}/detail">
{$ ctrl.node.instance_uuid | noValue $}
</a>
</dd>
<dt translate>Power State</dt>
<dd ng-class="{'running': ctrl.node['target_power_state']}">{$ ctrl.node['power_state'] $}</dd>
<dd ng-class="{'running': ctrl.node.target_power_state}">{$ ctrl.node.power_state | noValue $}</dd>
<dt translate>Target Power State</dt>
<dd>{$ ctrl.node['target_power_state'] $}</dd>
<dd>{$ ctrl.node.target_power_state | noValue $}</dd>
<dt translate>Provision State</dt>
<dd>{$ ctrl.node['provision_state'] $}</dd>
<dd>{$ ctrl.node.provision_state | noValue $}</dd>
<dt translate>Target Provision State</dt>
<dd>{$ ctrl.node['target_provision_state'] $}</dd>
<dd>{$ ctrl.node.target_provision_state | noValue $}</dd>
<dt translate>Last Error</dt>
<dd>{$ ctrl.node['last_error'] $}</dd>
<dd>{$ ctrl.node.last_error | noValue $}</dd>
<dt translate>Updated At</dt>
<dd>{$ ctrl.node['updated_at'] $}</dd>
<dd>{$ ctrl.node.updated_at | date: 'medium' | noValue $}</dd>
</dl>
</div>
</div>

View File

@ -22,26 +22,34 @@
.controller('IronicNodeListController', IronicNodeListController);
IronicNodeListController.$inject = [
'$scope',
'$rootScope',
'$q',
'horizon.framework.widgets.toast.service',
'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.enroll-node.service'
'horizon.dashboard.admin.ironic.enroll-node.service',
'horizon.dashboard.admin.ironic.edit-node.service'
];
function IronicNodeListController($rootScope,
function IronicNodeListController($scope,
$rootScope,
$q,
toastService,
ironic,
ironicEvents,
actions,
basePath,
maintenanceService,
enrollNodeService) {
enrollNodeService,
editNodeService) {
var ctrl = this;
ctrl.nodes = [];
ctrl.nodeSrc = [];
ctrl.nodesSrc = [];
ctrl.basePath = basePath;
ctrl.actions = actions;
@ -50,6 +58,8 @@
ctrl.removeNodeFromMaintenanceMode = removeNodeFromMaintenanceMode;
ctrl.removeNodesFromMaintenanceMode = removeNodesFromMaintenanceMode;
ctrl.enrollNode = enrollNode;
ctrl.editNode = editNode;
ctrl.refresh = refresh;
/**
* Filtering - client-side MagicSearch
@ -89,20 +99,38 @@
];
// Listen for the creation of new nodes, and update the node list
$rootScope.$on(ironicEvents.ENROLL_NODE_SUCCESS, function() {
init();
});
var enrollNodeHandler =
$rootScope.$on(ironicEvents.ENROLL_NODE_SUCCESS,
function() {
init();
});
$rootScope.$on(ironicEvents.DELETE_NODE_SUCCESS, function() {
init();
});
var deleteNodeHandler = $rootScope.$on(ironicEvents.DELETE_NODE_SUCCESS,
function() {
init();
});
$rootScope.$on(ironicEvents.CREATE_PORT_SUCCESS, function() {
init();
});
var editNodeHandler = $rootScope.$on(ironicEvents.EDIT_NODE_SUCCESS,
function() {
init();
});
$rootScope.$on(ironicEvents.DELETE_PORT_SUCCESS, function() {
init();
var createPortHandler = $rootScope.$on(ironicEvents.CREATE_PORT_SUCCESS,
function() {
init();
});
var deletePortHandler = $rootScope.$on(ironicEvents.DELETE_PORT_SUCCESS,
function() {
init();
});
$scope.$on('destroy', function() {
enrollNodeHandler();
deleteNodeHandler();
editNodeHandler();
createPortHandler();
deletePortHandler();
});
init();
@ -118,15 +146,26 @@
}
function onGetNodes(response) {
ctrl.nodesSrc = response.data.items;
ctrl.nodesSrc.forEach(function (node) {
var promises = [];
angular.forEach(response.data.items, function (node) {
node.id = node.uuid;
retrievePorts(node);
promises.push(retrievePorts(node));
// Report any changes in last-error
if (node.last_error !== "" &&
angular.isDefined(ctrl.nodesSrc[node.uuid]) &&
node.last_error !== ctrl.nodesSrc[node.uuid].last_error) {
toastService.add('error',
"Node " + node.name + ". " + node.last_error);
}
});
$q.all(promises).then(function() {
ctrl.nodesSrc = response.data.items;
});
}
function retrievePorts(node) {
ironic.getPortsWithNode(node.uuid).then(
return ironic.getPortsWithNode(node.uuid).then(
function (response) {
node.ports = response.data.items;
}
@ -152,6 +191,14 @@
function enrollNode() {
enrollNodeService.modal();
}
function editNode(node) {
editNodeService.modal(node);
}
function refresh() {
init();
}
}
})();

View File

@ -15,11 +15,18 @@
<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>
<div class="pull-right">
<button class="btn btn-default btn-sm"
style="margin-right:10px;"
ng-click="table.refresh()">
<span translate>Refresh</span>
</button>
<button class="btn btn-default btn-sm"
ng-click="table.enrollNode()">
<span class="fa fa-plus"></span>
<span translate>Enroll Node</span>
</button>
</div>
</th>
<th class="action-col">
<action-list dropdown class="pull-right">
@ -117,7 +124,7 @@
<span ng-if="!node.instance_uuid">{$ 'No Instance' | translate $}</span>
</td>
<td class="rsp-p2" >
<div ng-class="{'running': node['target_power_state']}">
<div ng-class="{'running': node.target_power_state}">
{$ node.power_state $}
</div>
</td>
@ -130,32 +137,32 @@
<action button-type="split-button"
action-classes="'btn btn-default btn-sm'"
callback="table.actions.powerOn"
disabled="node['power_state']!=='power off'"
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'"
disabled="node.power_state !== 'power on'"
item="node">
{$ 'Power off' | translate $}
</action>
<action button-type="menu-item"
callback="table.putNodeInMaintenanceMode"
disabled="node['maintenance']"
disabled="node.maintenance"
item="node">
{$ 'Maintenance on' | translate $}
</action>
<action button-type="menu-item"
callback="table.removeNodeFromMaintenanceMode"
disabled="!node['maintenance']"
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')"
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 $}
@ -165,6 +172,24 @@
item="node">
{$ 'Create port' | translate $}
</action>
<action ng-repeat="targetState in
['manageable', 'available', 'active']"
button-type="menu-item"
callback="table.actions.setProvisionState"
item="{node: node,
verb: table.actions.getProvisionStateTransitionVerb(
node.provision_state,
targetState)}"
disabled="table.actions.getProvisionStateTransitionVerb(
node.provision_state,
targetState) === null">
{$ ('Move to ' | translate) + targetState $}
</action>
<action button-type="menu-item"
callback="table.editNode"
item="node">
{$ 'Edit' | translate $}
</action>
</menu>
</action-list>
</td>

View File

@ -14,7 +14,7 @@
# under the License.
import horizon
from ironic_ui.content.ironic.panel import Ironic
from ironic_ui.content.ironic import panel as i_panel
from openstack_dashboard.test import helpers as test
@ -23,4 +23,4 @@ class RegistrationTests(test.TestCase):
dashboard = horizon.get_dashboard('admin')
panel = dashboard.get_panel('ironic')
self.assertEqual(panel.__class__, Ironic)
self.assertEqual(panel.__class__, i_panel.Ironic)

View File

@ -0,0 +1,12 @@
---
prelude: >
This release adds support for editing nodes and
moving those nodes between enroll, manageable, available
and active states. It is now possible to enroll a node
without all required fields for moving to manageable state
being present due to the facility for editing the node once
it has been created.
features:
- Edit nodes after creation
- Move nodes between enroll, manageable, available and active states
- Ability to enroll a node without all required fields for other states

View File

@ -0,0 +1,68 @@
# sunanchen <KF.sunanchen@h3c.com>, 2016. #zanata
msgid ""
msgstr ""
"Project-Id-Version: ironic-ui 2.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-08-19 12:56+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-23 03:16+0000\n"
"Last-Translator: sunanchen <KF.sunanchen@h3c.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 "2.0.0"
msgstr "2.0.0版本"
msgid "Add and delete nodes"
msgstr "增加或删除节点"
msgid "Add and delete ports"
msgstr "增加或删除端口"
msgid "Breadcrumbs have been added"
msgstr "已增加面包屑导航"
msgid "Current Series Release Notes"
msgstr "当前版本发布说明"
msgid ""
"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."
msgstr ""
"目前不支持通过UI修改已经注册的节点的信息。因此请务必保证节点注册信息的精"
"确性和可用性。目前,注册节点过程中出现的错误,只能通过删除节点之后重新注册来"
"修正"
msgid "Ironic UI Release Notes"
msgstr "Ironic UI发布说明"
msgid "Known Issues"
msgstr "已知的问题"
msgid "New Features"
msgstr "新特性"
msgid "Newton Series Release Notes"
msgstr "Newton版本发布说明"
msgid "Panel hidden if baremetal service or admin rights are not present"
msgstr "如果当前baremetal service不可用或者admin权限不够面板将会不可见"
msgid ""
"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."
msgstr ""
"该版本支持增加和删除节点的功能。同样也支持增加和删除端口。在多keystone "
"regions的情况下如果当前场景下baremetal service不可用面板将会被隐藏"
msgid "UX improvements across the interface"
msgstr "通过修改接口的方式提升UX "

View File

@ -5,7 +5,7 @@ description-file =
README.rst
author = OpenStack
author-email = openstack-dev@lists.openstack.org
home-page = http://www.openstack.org/
home-page = http://docs.openstack.org/developer/ironic-ui
classifier =
Environment :: OpenStack
Intended Audience :: Information Technology

36
tox.ini
View File

@ -16,44 +16,21 @@ deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
commands = {toxinidir}/manage.py test ironic_ui --settings=ironic_ui.test.settings
[testenv:common-constraints]
install_command = pip install -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages}
[testenv:pep8]
commands = flake8 {posargs}
[testenv:pep8-constraints]
install_command = {[testenv:common-constraints]install_command}
commands = flake8 {posargs}
[testenv:venv]
commands = {posargs}
[testenv:venv-constraints]
install_command = {[testenv:common-constraints]install_command}
commands = {posargs}
[testenv:cover]
commands = python setup.py test --coverage --testr-args='{posargs}'
[testenv:cover-constraints]
install_command = {[testenv:common-constraints]install_command}
commands = python setup.py test --coverage --testr-args='{posargs}'
[testenv:docs]
commands = python setup.py build_sphinx
[testenv:docs-constraints]
install_command = {[testenv:common-constraints]install_command}
commands = python setup.py build_sphinx
[testenv:debug]
commands = oslo_debug_helper {posargs}
[testenv:debug-constraints]
install_command = {[testenv:common-constraints]install_command}
commands = oslo_debug_helper {posargs}
[flake8]
show-source = True
@ -62,3 +39,16 @@ 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
[testenv:extractmessages]
commands =
pybabel extract -F babel-django.cfg \
-o ironic_ui/locale/django.pot -k gettext_noop -k gettext_lazy -k ngettext_lazy:1,2 \
-k ugettext_noop -k ugettext_lazy -k ungettext_lazy:1,2 -k npgettext:1c,2,3 \
-k pgettext_lazy:1c,2 -k npgettext_lazy:1c,2,3 --add-comments Translators: ironic_ui
pybabel extract -F babel-djangojs.cfg \
-o ironic_ui/locale/djangojs.pot -k gettext_noop -k gettext_lazy -k ngettext_lazy:1,2 \
-k ugettext_noop -k ugettext_lazy -k ungettext_lazy:1,2 -k npgettext:1c,2,3 \
-k pgettext_lazy:1c,2 -k npgettext_lazy:1c,2,3 --add-comments Translators: ironic_ui