diff -Nru maas-0.1+bzr400+dfsg/debian/changelog maas-0.1+bzr415+dfsg/debian/changelog
--- maas-0.1+bzr400+dfsg/debian/changelog 2012-04-03 23:56:19.000000000 +0000
+++ maas-0.1+bzr415+dfsg/debian/changelog 2012-04-04 23:11:25.000000000 +0000
@@ -1,3 +1,20 @@
+maas (0.1+bzr415+dfsg-0ubuntu1) precise; urgency=low
+
+ * debian/control: Update package descriptions; Suggests maas-dhcp for maas
+ and add a maas-dhcp binary.
+ * Add maas-dhcp package to configure a DHCP server.
+ - debian/maas-dhcp.config: Add to ask debconf questions about range,
+ gateway, and domain.
+ - debian/maas-dhcp.postinst: Handle update of config values.
+ - debian/maas-dhcp.templates: Debconf questions.
+ * debian/po: Update for templates.
+ * Add message telling MAAS URL after installation.
+ - debian/maas.templates: Add message.
+ - debian/maas.postinst: Display message.
+ * debian/maas.config: Hide dbconfig-install question.
+
+ -- Andres Rodriguez Wed, 04 Apr 2012 14:47:13 -0400
+
maas (0.1+bzr400+dfsg-0ubuntu1) precise; urgency=low
* debian/patches/{02-pserv-config,03-txlongpoll-config}.patch: Refreshed.
diff -Nru maas-0.1+bzr400+dfsg/debian/control maas-0.1+bzr415+dfsg/debian/control
--- maas-0.1+bzr400+dfsg/debian/control 2012-04-03 23:56:19.000000000 +0000
+++ maas-0.1+bzr415+dfsg/debian/control 2012-04-04 23:11:25.000000000 +0000
@@ -2,7 +2,7 @@
Section: net
Priority: optional
Maintainer: Ubuntu Developers
-Build-Depends: debhelper (>= 8), python (>= 2.7), python-distribute, dh-apport
+Build-Depends: debhelper (>= 8), python (>= 2.7), python-distribute, dh-apport, po-debconf
Standards-Version: 3.9.3
X-Python-Version: >= 2.7
Homepage: https://launchpad.net/maas
@@ -26,8 +26,17 @@
${misc:Depends},
${python:Depends}
Recommends: openssh-server
-Description: The next step in the development of orchestra.
- It provides an easy to use UI to provision your Ubuntu servers.
+Suggests: maas-dhcp
+Description: Ubuntu MAAS Server
+ Ubuntu MAAS Server is the successor to Orchestra. It offers a
+ nice UI to provision your Ubuntu servers. Each physical server
+ (“node”) will be commissioned automatically on first boot.
+ During the commissioning process administrators are able to
+ configure hardware settings manually before an automated smoke
+ test and burn-in test are done. Once commissioned, a node can
+ be deployed on demand by name, or allocated to a queue for
+ dynamic allocation to services being deployed on this MAAS.
+
Package: python-django-maas
Architecture: all
@@ -47,5 +56,29 @@
python-zope.interface,
${misc:Depends},
${python:Depends}
-Description: The next step in the development of Orchestra.
- It provides an easy to use UI to provision your Ubuntu servers.
+Description: Ubuntu MAAS Server - (django files)
+ Ubuntu MAAS Server is the successor to Orchestra. It offers a
+ nice UI to provision your Ubuntu servers. Each physical server
+ (“node”) will be commissioned automatically on first boot.
+ During the commissioning process administrators are able to
+ configure hardware settings manually before an automated smoke
+ test and burn-in test are done. Once commissioned, a node can
+ be deployed on demand by name, or allocated to a queue for
+ dynamic allocation to services being deployed on this MAAS.
+ .
+ This package contains the Django files.
+
+Package: maas-dhcp
+Architecture: all
+Depends: dnsmasq
+Description: Ubuntu MAAS Server - DHCP configuration
+ Ubuntu MAAS Server is the successor to Orchestra. It offers a
+ nice UI to provision your Ubuntu servers. Each physical server
+ (“node”) will be commissioned automatically on first boot.
+ During the commissioning process administrators are able to
+ configure hardware settings manually before an automated smoke
+ test and burn-in test are done. Once commissioned, a node can
+ be deployed on demand by name, or allocated to a queue for
+ dynamic allocation to services being deployed on this MAAS.
+ .
+ This package configures a DHCP that can be used with MAAS.
diff -Nru maas-0.1+bzr400+dfsg/debian/maas.config maas-0.1+bzr415+dfsg/debian/maas.config
--- maas-0.1+bzr400+dfsg/debian/maas.config 2012-04-03 23:56:19.000000000 +0000
+++ maas-0.1+bzr415+dfsg/debian/maas.config 2012-04-04 23:11:25.000000000 +0000
@@ -3,6 +3,19 @@
. /usr/share/debconf/confmodule
db_version 2.0
+# creates question
+set_question() {
+ if ! db_fget "$1" seen; then
+ db_register dbconfig-common/dbconfig-install "$1"
+ db_subst "$1" ID "$1"
+ db_fget "$1" seen
+ fi
+ if [ "$RET" = false ]; then
+ db_set "$1" "$2"
+ db_fset "$1" seen true
+ fi
+}
+
# source dbconfig-common shell library, and call the hook function
if [ -f /usr/share/dbconfig-common/dpkg/config.pgsql ]; then
. /usr/share/dbconfig-common/dpkg/config.pgsql
@@ -15,5 +28,7 @@
dbc_dbpass="$maas_db_pass"
dbc_remove="true"
+ # Hide maas/dbconfig-install question by setting default.
+ set_question maas/dbconfig-install true
dbc_go maas $@
fi
diff -Nru maas-0.1+bzr400+dfsg/debian/maas-dhcp.config maas-0.1+bzr415+dfsg/debian/maas-dhcp.config
--- maas-0.1+bzr400+dfsg/debian/maas-dhcp.config 1970-01-01 00:00:00.000000000 +0000
+++ maas-0.1+bzr415+dfsg/debian/maas-dhcp.config 2012-04-04 23:11:25.000000000 +0000
@@ -0,0 +1,37 @@
+#!/bin/sh -e
+
+# Only ask this question on new installs and reconfigures
+if ([ "$1" = "configure" ] && [ -z "$2" ]) || [ "$1" = "reconfigure" ]; then
+ . /usr/share/debconf/confmodule
+
+ # Try to obtain the default range for dnsmasq-dhcp-range and set it
+ range=$(grep -s "^dhcp-range=.*$" /etc/cobbler/dnsmasq.template | awk '{split($0,array,"=")} END{print array[2]}')
+ if [ -n "$range" ]; then
+ db_set maas-dhcp/dnsmasq-dhcp-range "$range"
+ fi
+
+ # try to obtain the default gateway and set it
+ gateway=$(grep -s "^dhcp-option=.*$" /etc/cobbler/dnsmasq.template | awk '{split($0,array,"3,")} END{print array[2]}')
+ if [ "$gateway" = "\$next_server" ]; then
+ # If gateway is $next_server, obtain the IP address of gateway
+ gateway=$(grep -s "^next_server:.*$" /etc/cobbler/settings | awk '{split($0,array,": ")} END{print array[2]}')
+ fi
+ if [ -n "$gateway" ]; then
+ db_set maas-dhcp/dnsmasq-default-gateway "$gateway"
+ fi
+
+ # try to obtain the domain and set it
+ domain=$(grep -s "^domain=.*$" /etc/cobbler/dnsmasq.template | awk '{split($0,array,"=")} END{print array[2]}')
+ if [ -n "$domain" ]; then
+ db_set maas-dhcp/dnsmasq-domain-name "$domain"
+ fi
+
+ db_input high maas-dhcp/dnsmasq-dhcp-range || true
+ db_go
+ db_input high maas-dhcp/dnsmasq-default-gateway || true
+ db_go
+ db_input high maas-dhcp/dnsmasq-domain-name || true
+ db_go
+
+fi
+#DEBHELPER#
diff -Nru maas-0.1+bzr400+dfsg/debian/maas-dhcp.postinst maas-0.1+bzr415+dfsg/debian/maas-dhcp.postinst
--- maas-0.1+bzr400+dfsg/debian/maas-dhcp.postinst 1970-01-01 00:00:00.000000000 +0000
+++ maas-0.1+bzr415+dfsg/debian/maas-dhcp.postinst 2012-04-04 23:11:25.000000000 +0000
@@ -0,0 +1,50 @@
+#!/bin/sh -e
+
+. /usr/share/debconf/confmodule
+db_version 2.0
+
+if ([ "$1" = "configure" ] && [ -z "$2" ]) || [ "$1" = "reconfigure" ] || [ -n "$DEBCONF_RECONFIGURE" ]; then
+
+ # Setup dnsmasq
+ sed -i -e "s/^manage_dns:.*$/manage_dns: 1/" \
+ -e "s/^manage_dhcp:.*$/manage_dhcp: 1/" /etc/cobbler/settings
+ sed -i -e "s/^module = manage_bind/module = manage_dnsmasq/" \
+ -e "s/^module = manage_isc/module = manage_dnsmasq/" /etc/cobbler/modules.conf
+
+ # Set the DHCP range
+ db_get maas-dhcp/dnsmasq-dhcp-range || true
+ range="$RET"
+ if [ -n "$range" ]; then
+ sed -i -e "s/^dhcp-range=.*$/dhcp-range=$range/" /etc/cobbler/dnsmasq.template
+ fi
+
+ # Setup Default Gateway
+ db_get maas-dhcp/dnsmasq-default-gateway || true
+ ipaddr="$RET"
+ if [ -n "$ipaddr" ]; then
+ # If template has $next_server as default gateway, set it to $ipaddr
+ if grep -qs "^dhcp-option=.*$" /etc/cobbler/dnsmasq.template; then
+ sed -i -e "s/^dhcp-option=.*$/dhcp-option=3,$ipaddr/" /etc/cobbler/dnsmasq.template
+ fi
+ fi
+
+ # Setup Domain Name
+ db_get maas-dhcp/dnsmasq-domain-name || true
+ domain="$RET"
+ if [ -n "$domain" ]; then
+ # if the domain hasn't been set, set it to $domain
+ if grep -qs "^#domain=.*$" /etc/cobbler/dnsmasq.template; then
+ sed -i -e "s/^#domain=.*/domain=$domain/" /etc/cobbler/dnsmasq.template
+ # if the domain has been set, change it to $domain
+ elif grep -qs "^domain=.*$" /etc/cobbler/dnsmasq.template; then
+ sed -i -e "s/^domain=.*$/domain=$domain/" /etc/cobbler/dnsmasq.template
+ fi
+ fi
+ if [ -x /usr/sbin/invoke-rc.d ]; then
+ invoke-rc.d cobbler restart || true
+ fi
+
+fi
+
+#DEBHELPER#
+exit 0
diff -Nru maas-0.1+bzr400+dfsg/debian/maas-dhcp.templates maas-0.1+bzr415+dfsg/debian/maas-dhcp.templates
--- maas-0.1+bzr400+dfsg/debian/maas-dhcp.templates 1970-01-01 00:00:00.000000000 +0000
+++ maas-0.1+bzr415+dfsg/debian/maas-dhcp.templates 2012-04-04 23:11:25.000000000 +0000
@@ -0,0 +1,26 @@
+Template: maas-dhcp/dnsmasq-dhcp-range
+Type: string
+_Description: Set the network range for DHCP Clients:
+ Ubuntu MAAS Server can manage DHCP for address allocation for
+ the provisioned systems. If the network range for the DHCP is
+ different from the default (192.168.1.5,192.168.1.200), you
+ should set it here.
+ .
+ An example of how a network range should be specified is:
+ .
+ 10.10.10.2,10.10.10.254
+
+Template: maas-dhcp/dnsmasq-default-gateway
+Type: string
+_Description: Set default Gateway for DHCP Clients:
+ Ubuntu MAAS Server can manage DHCP for address allocation for
+ the provisioned systems. If the Ubuntu MAAS Server is NOT the
+ default Gateway for the provisioned systems, you should set the
+ default Gateway here, otherwise leave this blank.
+
+Template: maas-dhcp/dnsmasq-domain-name
+Type: string
+_Description: Set the domain name for DHCP Clients:
+ Ubuntu MAAS Server can manage DHCP for address allocation for
+ the provisioned systems. If these systems are required to be
+ under a domain, you should enter it here.
diff -Nru maas-0.1+bzr400+dfsg/debian/maas.postinst maas-0.1+bzr415+dfsg/debian/maas.postinst
--- maas-0.1+bzr400+dfsg/debian/maas.postinst 2012-04-03 23:56:19.000000000 +0000
+++ maas-0.1+bzr415+dfsg/debian/maas.postinst 2012-04-04 23:11:25.000000000 +0000
@@ -73,11 +73,12 @@
ipaddr=${ipaddr#* inet }
ipaddr=${ipaddr%%/*}
# Set the IP address of the interface with default route
- if [ "$ipaddr" ]; then
+ if [ -n "$ipaddr" ]; then
if grep -qs "^DEFAULT_MAAS_URL\ \= \"[a-zA-Z0-9:/.]\{0,\}\"$" /etc/maas/maas_local_settings.py; then
sed -i "s/^DEFAULT_MAAS_URL\ \= \"[a-zA-Z0-9:/.]\{0,\}\"$/DEFAULT_MAAS_URL = \"http:\/\/"$ipaddr"\/\"/" \
/etc/maas/maas_local_settings.py
fi
+ db_subst maas/installation-note MAAS_URL "$ipaddr"
fi
#########################################################
@@ -150,6 +151,10 @@
maas_sync_migrate_db
fi
+ # Display installation note
+ db_input high maas/installation-note || true
+ db_go
+
elif [ "$1" = "configure" ] && dpkg --compare-versions "$2" gt 0.1+bzr266+dfsg-0ubuntu1; then
# If upgrading to any later package version, then upgrade db.
if [ -x /usr/sbin/invoke-rc.d ]; then
diff -Nru maas-0.1+bzr400+dfsg/debian/maas.templates maas-0.1+bzr415+dfsg/debian/maas.templates
--- maas-0.1+bzr400+dfsg/debian/maas.templates 1970-01-01 00:00:00.000000000 +0000
+++ maas-0.1+bzr415+dfsg/debian/maas.templates 2012-04-04 23:11:25.000000000 +0000
@@ -0,0 +1,8 @@
+Template: maas/installation-note
+Type: note
+Default: true
+_Description: Ubuntu MAAS Server
+ The Ubuntu MAAS Server has been installed in your system. You
+ can access the MAAS Web interface here:
+ .
+ http://${MAAS_URL}/MAAS
diff -Nru maas-0.1+bzr400+dfsg/debian/po/POTFILES.in maas-0.1+bzr415+dfsg/debian/po/POTFILES.in
--- maas-0.1+bzr400+dfsg/debian/po/POTFILES.in 1970-01-01 00:00:00.000000000 +0000
+++ maas-0.1+bzr415+dfsg/debian/po/POTFILES.in 2012-04-04 23:11:25.000000000 +0000
@@ -0,0 +1,2 @@
+[type: gettext/rfc822deb] maas-dhcp.templates
+[type: gettext/rfc822deb] maas.templates
diff -Nru maas-0.1+bzr400+dfsg/debian/po/templates.pot maas-0.1+bzr415+dfsg/debian/po/templates.pot
--- maas-0.1+bzr400+dfsg/debian/po/templates.pot 1970-01-01 00:00:00.000000000 +0000
+++ maas-0.1+bzr415+dfsg/debian/po/templates.pot 2012-04-04 23:11:25.000000000 +0000
@@ -0,0 +1,96 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: maas\n"
+"Report-Msgid-Bugs-To: maas@packages.debian.org\n"
+"POT-Creation-Date: 2012-04-04 14:45-0400\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#. Type: string
+#. Description
+#: ../maas-dhcp.templates:1001
+msgid "Set the network range for DHCP Clients:"
+msgstr ""
+
+#. Type: string
+#. Description
+#: ../maas-dhcp.templates:1001
+msgid ""
+"Ubuntu MAAS Server can manage DHCP for address allocation for the "
+"provisioned systems. If the network range for the DHCP is different from "
+"the default (192.168.1.5,192.168.1.200), you should set it here."
+msgstr ""
+
+#. Type: string
+#. Description
+#: ../maas-dhcp.templates:1001
+msgid "An example of how a network range should be specified is:"
+msgstr ""
+
+#. Type: string
+#. Description
+#: ../maas-dhcp.templates:1001
+msgid "10.10.10.2,10.10.10.254"
+msgstr ""
+
+#. Type: string
+#. Description
+#: ../maas-dhcp.templates:2001
+msgid "Set default Gateway for DHCP Clients:"
+msgstr ""
+
+#. Type: string
+#. Description
+#: ../maas-dhcp.templates:2001
+msgid ""
+"Ubuntu MAAS Server can manage DHCP for address allocation for the "
+"provisioned systems. If the Ubuntu MAAS Server is NOT the default Gateway "
+"for the provisioned systems, you should set the default Gateway here, "
+"otherwise leave this blank."
+msgstr ""
+
+#. Type: string
+#. Description
+#: ../maas-dhcp.templates:3001
+msgid "Set the domain name for DHCP Clients:"
+msgstr ""
+
+#. Type: string
+#. Description
+#: ../maas-dhcp.templates:3001
+msgid ""
+"Ubuntu MAAS Server can manage DHCP for address allocation for the "
+"provisioned systems. If these systems are required to be under a domain, you "
+"should enter it here."
+msgstr ""
+
+#. Type: note
+#. Description
+#: ../maas.templates:1001
+msgid "Ubuntu MAAS Server"
+msgstr ""
+
+#. Type: note
+#. Description
+#: ../maas.templates:1001
+msgid ""
+"The Ubuntu MAAS Server has been installed in your system. You can access the "
+"MAAS Web interface here:"
+msgstr ""
+
+#. Type: note
+#. Description
+#: ../maas.templates:1001
+msgid " http://${MAAS_URL}/MAAS"
+msgstr ""
diff -Nru maas-0.1+bzr400+dfsg/src/maasserver/api.py maas-0.1+bzr415+dfsg/src/maasserver/api.py
--- maas-0.1+bzr400+dfsg/src/maasserver/api.py 2012-04-03 22:07:07.000000000 +0000
+++ maas-0.1+bzr415+dfsg/src/maasserver/api.py 2012-04-04 18:59:25.000000000 +0000
@@ -432,9 +432,9 @@
"""Create a new Node.
Adding a server to a MAAS puts it on a path that will wipe its disks
- and re-install its operating system. In anonymous enlistment,
- therefore, the node is held in the "Declared" state for approval by a
- MAAS user.
+ and re-install its operating system. In anonymous enlistment and when
+ the enlistment is done by a non-admin, the node is held in the
+ "Declared" state for approval by a MAAS admin.
"""
return create_node(request)
@@ -473,21 +473,22 @@
def new(self, request):
"""Create a new Node.
- When a node has been added to MAAS by a logged-in MAAS user, it is
+ When a node has been added to MAAS by an admin MAAS user, it is
ready for allocation to services running on the MAAS.
"""
node = create_node(request)
- node.accept_enlistment()
+ if request.user.is_superuser:
+ node.accept_enlistment()
return node
@api_exported('accept', 'POST')
def accept(self, request):
"""Accept declared nodes into the MAAS.
- Nodes can be enlisted in the MAAS anonymously, as opposed to by a
- logged-in user, at the nodes' own request. These nodes are held in
- the Declared state; a MAAS user must first verify the authenticity of
- these enlistments, and accept them.
+ Nodes can be enlisted in the MAAS anonymously or by non-admin users,
+ as opposed to by an admin. These nodes are held in the Declared
+ state; a MAAS admin must first verify the authenticity of these
+ enlistments, and accept them.
Enlistments can be accepted en masse, by passing multiple nodes to
this call. Accepting an already accepted node is not an error, but
@@ -500,11 +501,21 @@
excluded from the result.
"""
system_ids = set(request.POST.getlist('nodes'))
- nodes = Node.objects.filter(system_id__in=system_ids)
- found_ids = set(node.system_id for node in nodes)
- if len(nodes) < len(system_ids):
+ # Check the existence of these nodes first.
+ existing_ids = set(Node.objects.filter().values_list(
+ 'system_id', flat=True))
+ if len(existing_ids) < len(system_ids):
raise MAASAPIBadRequest(
- "Unknown node(s): %s" % ', '.join(system_ids - found_ids))
+ "Unknown node(s): %s." % ', '.join(system_ids - existing_ids))
+ # Make sure that the user has the required permission.
+ nodes = Node.objects.get_nodes(
+ request.user, perm=NODE_PERMISSION.ADMIN, ids=system_ids)
+ ids = set(node.system_id for node in nodes)
+ if len(nodes) < len(system_ids):
+ raise PermissionDenied(
+ "You don't have the required permission to accept the "
+ "following node(s): %s." % (
+ ', '.join(system_ids - ids)))
return filter(None, [node.accept_enlistment() for node in nodes])
@api_exported('list', 'GET')
@@ -513,7 +524,8 @@
match_ids = request.GET.getlist('id')
if match_ids == []:
match_ids = None
- nodes = Node.objects.get_visible_nodes(request.user, ids=match_ids)
+ nodes = Node.objects.get_nodes(
+ request.user, NODE_PERMISSION.VIEW, ids=match_ids)
return nodes.order_by('id')
@api_exported('list_allocated', 'GET')
diff -Nru maas-0.1+bzr400+dfsg/src/maasserver/forms.py maas-0.1+bzr415+dfsg/src/maasserver/forms.py
--- maas-0.1+bzr400+dfsg/src/maasserver/forms.py 2012-04-03 22:07:07.000000000 +0000
+++ maas-0.1+bzr415+dfsg/src/maasserver/forms.py 2012-04-04 18:59:25.000000000 +0000
@@ -11,12 +11,12 @@
__metaclass__ = type
__all__ = [
"CommissioningForm",
- "get_transition_form",
+ "get_action_form",
"HostnameFormField",
"NodeForm",
"MACAddressForm",
"MAASAndNetworkForm",
- "NodeTransitionForm",
+ "NodeActionForm",
"SSHKeyForm",
"UbuntuForm",
"UIAdminNodeEditForm",
@@ -54,7 +54,6 @@
NODE_PERMISSION,
NODE_STATUS,
SSHKey,
- UserProfile,
)
@@ -110,13 +109,11 @@
class UIAdminNodeEditForm(ModelForm):
after_commissioning_action = forms.ChoiceField(
choices=NODE_AFTER_COMMISSIONING_ACTION_CHOICES)
- owner = forms.ModelChoiceField(
- queryset=UserProfile.objects.all_users(), required=False)
class Meta:
model = Node
fields = (
- 'hostname', 'after_commissioning_action', 'power_type', 'owner')
+ 'hostname', 'after_commissioning_action', 'power_type')
class MACAddressForm(ModelForm):
@@ -194,79 +191,112 @@
return node
-# Node transitions methods.
-# The format is:
+def start_node(node, user):
+ """Start a node from the UI. It will have no meta_data."""
+ # TODO: Provide some kind of feedback to the user. Starting a node
+ # is fire-and-forget. From the user's point of view, it's as if all
+ # that happens is the page reloading.
+ Node.objects.start_nodes([node.system_id], user)
+
+
+# Node actions per status.
+#
+# This maps each NODE_STATUS to a list of actions applicable to a node
+# in that state:
+#
# {
-# old_status1: [
-# {
-# 'display': display_string11, # The name of the transition
-# # (to be displayed in the UI).
-# 'name': transition_name11, # The name of the transition.
-# 'permission': permission_required11,
-# },
-# ]
-# ...
+# NODE_STATUS.: [action1, action2],
+# NODE_STATUS.: [action1],
+# NODE_STATUS.: [action1, action2, action3],
+# }
+#
+# The available actions (insofar as the user has privileges to use them)
+# show up in the user interface as buttons on the node page.
#
-NODE_TRANSITIONS_METHODS = {
+# Each action is a dict:
+#
+# {
+# # Action's display name; will be shown in the button.
+# 'display': "Paint node",
+# # Permission required to perform action.
+# 'permission': NODE_PERMISSION.EDIT,
+# # Callable that performs action. Takes parameters (node, user).
+# 'execute': lambda node, user: paint_node(
+# node, favourite_colour(user)),
+# }
+#
+NODE_ACTIONS = {
NODE_STATUS.DECLARED: [
{
'display': "Accept Enlisted node",
'permission': NODE_PERMISSION.ADMIN,
'execute': lambda node, user: Node.accept_enlistment(node),
},
+ {
+ 'display': "Commission node",
+ 'permission': NODE_PERMISSION.ADMIN,
+ 'execute': lambda node, user: Node.start_commissioning(node, user),
+ },
+ ],
+ NODE_STATUS.READY: [
+ {
+ 'display': "Start node",
+ 'permission': NODE_PERMISSION.EDIT,
+ 'execute': start_node,
+ },
+ ],
+ NODE_STATUS.ALLOCATED: [
+ {
+ 'display': "Start node",
+ 'permission': NODE_PERMISSION.EDIT,
+ 'execute': start_node,
+ },
],
}
-class NodeTransitionForm(forms.Form):
- """A form used to perform a status change on a Node.
+class NodeActionForm(forms.Form):
+ """Base form for performing a node action.
- That form class should not be used directly but through subclasses
- created using `get_transition_form`.
+ This form class should not be used directly but through subclasses
+ created using `get_action_form`.
"""
user = AnonymousUser()
# The name of the input button used with this form.
- input_name = 'node_transition'
+ input_name = 'node_action'
def __init__(self, instance, *args, **kwargs):
- super(NodeTransitionForm, self).__init__(*args, **kwargs)
+ super(NodeActionForm, self).__init__(*args, **kwargs)
self.node = instance
- self.transition_buttons = self.available_transition_methods(
+ self.action_buttons = self.available_action_methods(
self.node, self.user)
- # Create a convenient dict to fetch the transition name and
+ # Create a convenient dict to fetch the action's name and
# the permission to be checked from the button name.
- self.transition_dict = {
- transition['display']: (
- transition['permission'], transition['execute'])
- for transition in self.transition_buttons
+ self.action_dict = {
+ action['display']: (action['permission'], action['execute'])
+ for action in self.action_buttons
}
- def available_transition_methods(self, node, user):
- """Return the transitions that this user is allowed to perform on
- a node.
+ def available_action_methods(self, node, user):
+ """Return the actions that this user is allowed to perform on a node.
:param node: The node for which the check should be performed.
:type node: :class:`maasserver.models.Node`
- :param user: The user used to perform the permission checks. Only the
- transitions available to this user will be returned.
+ :param user: The user who would be performing the action. Only the
+ actions available to this user will be returned.
:type user: :class:`django.contrib.auth.models.User`
- :return: A list of transition dicts (each dict contains 3 values:
- 'name': the name of the transition, 'permission': the permission
- required to perform this transition, 'method': the name of the
- method to execute on the node to perform the transition).
+ :return: Any applicable action dicts, as found in NODE_ACTIONS.
:rtype: Sequence
"""
- node_transitions = NODE_TRANSITIONS_METHODS.get(node.status, ())
return [
- node_transition for node_transition in node_transitions
- if user.has_perm(node_transition['permission'], node)]
+ action for action in NODE_ACTIONS.get(node.status, ())
+ if user.has_perm(action['permission'], node)]
def save(self):
- transition_name = self.data.get(self.input_name)
- permission, execute = self.transition_dict.get(
- transition_name, (None, None))
+ action_name = self.data.get(self.input_name)
+ permission, execute = self.action_dict.get(action_name, (None, None))
if execute is not None:
if not self.user.has_perm(permission, self.node):
raise PermissionDenied()
@@ -275,18 +305,17 @@
raise PermissionDenied()
-def get_transition_form(user):
- """Return a class derived from NodeTransitionForm for a specific user.
+def get_action_form(user):
+ """Return a class derived from NodeActionForm for a specific user.
:param user: The user for which to build a form derived from
- NodeTransitionForm.
+ NodeActionForm.
:type user: :class:`django.contrib.auth.models.User`
- :return: A form class derived from NodeTransitionForm.
+ :return: A form class derived from NodeActionForm.
:rtype: class:`django.forms.Form`
"""
return type(
- str("SpecificNodeTransitionForm"), (NodeTransitionForm,),
- {'user': user})
+ str("SpecificNodeActionForm"), (NodeActionForm,), {'user': user})
class ProfileForm(ModelForm):
diff -Nru maas-0.1+bzr400+dfsg/src/maasserver/migrations/0004_add_node_error.py maas-0.1+bzr415+dfsg/src/maasserver/migrations/0004_add_node_error.py
--- maas-0.1+bzr400+dfsg/src/maasserver/migrations/0004_add_node_error.py 1970-01-01 00:00:00.000000000 +0000
+++ maas-0.1+bzr415+dfsg/src/maasserver/migrations/0004_add_node_error.py 2012-04-04 18:59:25.000000000 +0000
@@ -0,0 +1,132 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding field 'Node.error'
+ db.add_column('maasserver_node', 'error', self.gf('django.db.models.fields.CharField')(default=u'', max_length=255, blank=True), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Deleting field 'Node.error'
+ db.delete_column('maasserver_node', 'error')
+
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'maasserver.config': {
+ 'Meta': {'object_name': 'Config'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'value': ('maasserver.fields.JSONObjectField', [], {'null': 'True'})
+ },
+ 'maasserver.filestorage': {
+ 'Meta': {'object_name': 'FileStorage'},
+ 'data': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}),
+ 'filename': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ 'maasserver.macaddress': {
+ 'Meta': {'object_name': 'MACAddress'},
+ 'created': ('django.db.models.fields.DateField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'mac_address': ('maasserver.fields.MACAddressField', [], {'unique': 'True'}),
+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['maasserver.Node']"}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ 'maasserver.node': {
+ 'Meta': {'object_name': 'Node'},
+ 'after_commissioning_action': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'architecture': ('django.db.models.fields.CharField', [], {'default': "u'i386'", 'max_length': '10'}),
+ 'created': ('django.db.models.fields.DateField', [], {}),
+ 'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+ 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}),
+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '10'}),
+ 'system_id': ('django.db.models.fields.CharField', [], {'default': "u'node-75a4d6a0-7e71-11e1-b17c-0025bce60bc2'", 'unique': 'True', 'max_length': '41'}),
+ 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'null': 'True'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ 'maasserver.sshkey': {
+ 'Meta': {'object_name': 'SSHKey'},
+ 'created': ('django.db.models.fields.DateField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.TextField', [], {}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'maasserver.userprofile': {
+ 'Meta': {'object_name': 'UserProfile'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
+ },
+ 'piston.consumer': {
+ 'Meta': {'object_name': 'Consumer'},
+ 'description': ('django.db.models.fields.TextField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '16'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'consumers'", 'null': 'True', 'to': "orm['auth.User']"})
+ },
+ 'piston.token': {
+ 'Meta': {'object_name': 'Token'},
+ 'callback': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'callback_confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Consumer']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_approved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'timestamp': ('django.db.models.fields.IntegerField', [], {'default': '1333556156L'}),
+ 'token_type': ('django.db.models.fields.IntegerField', [], {}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tokens'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'verifier': ('django.db.models.fields.CharField', [], {'max_length': '10'})
+ }
+ }
+
+ complete_apps = ['maasserver']
diff -Nru maas-0.1+bzr400+dfsg/src/maasserver/models.py maas-0.1+bzr415+dfsg/src/maasserver/models.py
--- maas-0.1+bzr400+dfsg/src/maasserver/models.py 2012-04-03 22:07:07.000000000 +0000
+++ maas-0.1+bzr415+dfsg/src/maasserver/models.py 2012-04-04 18:59:25.000000000 +0000
@@ -288,11 +288,13 @@
else:
return query.filter(system_id__in=ids)
- def get_visible_nodes(self, user, ids=None):
- """Fetch Nodes visible by a User_.
+ def get_nodes(self, user, perm, ids=None):
+ """Fetch Nodes on which the User_ has the given permission.
:param user: The user that should be used in the permission check.
:type user: User_
+ :param perm: The permission to check.
+ :type perm: a permission string from NODE_PERMISSION
:param ids: If given, limit result to nodes with these system_ids.
:type ids: Sequence.
@@ -302,11 +304,21 @@
"""
if user.is_superuser:
- visible_nodes = self.all()
+ nodes = self.all()
else:
- visible_nodes = self.filter(
- models.Q(owner__isnull=True) | models.Q(owner=user))
- return self.filter_by_ids(visible_nodes, ids)
+ if perm == NODE_PERMISSION.VIEW:
+ nodes = self.filter(
+ models.Q(owner__isnull=True) | models.Q(owner=user))
+ elif perm == NODE_PERMISSION.EDIT:
+ nodes = self.filter(owner=user)
+ elif perm == NODE_PERMISSION.ADMIN:
+ nodes = self.none()
+ else:
+ raise NotImplementedError(
+ "Invalid permission check (invalid permission name: %s)." %
+ perm)
+
+ return self.filter_by_ids(nodes, ids)
def get_allocated_visible_nodes(self, token, ids):
"""Fetch Nodes that were allocated to the User_/oauth token.
@@ -329,22 +341,6 @@
nodes = self.filter(token=token, system_id__in=ids)
return nodes
- def get_editable_nodes(self, user, ids=None):
- """Fetch Nodes a User_ has ownership privileges on.
-
- An admin has ownership privileges on all nodes.
-
- :param user: The user that should be used in the permission check.
- :type user: User_
- :param ids: If given, limit result to nodes with these system_ids.
- :type ids: Sequence.
- """
- if user.is_superuser:
- visible_nodes = self.all()
- else:
- visible_nodes = self.filter(owner=user)
- return self.filter_by_ids(visible_nodes, ids)
-
def get_node_or_404(self, system_id, user, perm):
"""Fetch a `Node` by system_id. Raise exceptions if no `Node` with
this system_id exist or if the provided user has not the required
@@ -382,7 +378,7 @@
if constraints is None:
constraints = {}
available_nodes = (
- self.get_visible_nodes(for_user)
+ self.get_nodes(for_user, NODE_PERMISSION.VIEW)
.filter(status=NODE_STATUS.READY))
if constraints.get('name'):
@@ -408,7 +404,7 @@
:return: Those Nodes for which shutdown was actually requested.
:rtype: list
"""
- nodes = self.get_editable_nodes(by_user, ids=ids)
+ nodes = self.get_nodes(by_user, NODE_PERMISSION.EDIT, ids=ids)
get_papi().stop_nodes([node.system_id for node in nodes])
return nodes
@@ -429,12 +425,19 @@
:return: Those Nodes for which power-on was actually requested.
:rtype: list
"""
+ # TODO: File structure needs sorting out to avoid this circular
+ # import dance.
from metadataserver.models import NodeUserData
- nodes = self.get_editable_nodes(by_user, ids=ids)
- if user_data is not None:
- for node in nodes:
- NodeUserData.objects.set_user_data(node, user_data)
- get_papi().start_nodes([node.system_id for node in nodes])
+ from maasserver.provisioning import select_profile_for_node
+ nodes = self.get_nodes(by_user, NODE_PERMISSION.EDIT, ids=ids)
+ profiles = {}
+ for node in nodes:
+ NodeUserData.objects.set_user_data(node, user_data)
+ profiles[node.system_id] = {
+ 'profile': select_profile_for_node(node)}
+ papi = get_papi()
+ papi.modify_nodes(profiles)
+ papi.start_nodes([node.system_id for node in nodes])
return nodes
@@ -489,7 +492,7 @@
default=NODE_STATUS.DEFAULT_STATUS)
owner = models.ForeignKey(
- User, default=None, blank=True, null=True, editable=True)
+ User, default=None, blank=True, null=True, editable=False)
after_commissioning_action = models.IntegerField(
choices=NODE_AFTER_COMMISSIONING_ACTION_CHOICES,
@@ -508,6 +511,8 @@
token = models.ForeignKey(
Token, db_index=True, null=True, editable=False, unique=False)
+ error = models.CharField(max_length=255, blank=True, default='')
+
objects = NodeManager()
def __unicode__(self):
@@ -614,9 +619,23 @@
self.save()
return self
+ def start_commissioning(self, user):
+ """Install OS and self-test a new node."""
+ self.status = NODE_STATUS.COMMISSIONING
+ self.owner = user
+ self.save()
+ # The commissioning profile is handled in start_nodes.
+ Node.objects.start_nodes(
+ [self.system_id], user, user_data=None)
+
def delete(self):
# Delete the related mac addresses first.
self.macaddress_set.all().delete()
+ # Allocated nodes can't be deleted.
+ if self.status == NODE_STATUS.ALLOCATED:
+ raise NodeStateViolation(
+ "Cannot delete node %s: node is in state %s."
+ % (self.system_id, NODE_STATUS_CHOICES_DICT[self.status]))
super(Node, self).delete()
def set_mac_based_hostname(self, mac_address):
diff -Nru maas-0.1+bzr400+dfsg/src/maasserver/provisioning.py maas-0.1+bzr415+dfsg/src/maasserver/provisioning.py
--- maas-0.1+bzr400+dfsg/src/maasserver/provisioning.py 2012-04-03 22:07:07.000000000 +0000
+++ maas-0.1+bzr415+dfsg/src/maasserver/provisioning.py 2012-04-04 18:59:25.000000000 +0000
@@ -31,6 +31,7 @@
Config,
MACAddress,
Node,
+ NODE_STATUS,
)
from provisioningserver.enum import PSERV_FAULT
@@ -228,18 +229,21 @@
return conversions.get(architecture, architecture)
-def select_profile_for_node(node, papi):
+def select_profile_for_node(node):
"""Select which profile a node should be configured for."""
assert node.architecture, "Node's architecture is not known."
cobbler_arch = name_arch_in_cobbler_style(node.architecture)
- return "maas-%s-%s" % ("precise", cobbler_arch)
+ profile = "maas-%s-%s" % ("precise", cobbler_arch)
+ if node.status == NODE_STATUS.COMMISSIONING:
+ profile += "-commissioning"
+ return profile
@receiver(post_save, sender=Node)
def provision_post_save_Node(sender, instance, created, **kwargs):
"""Create or update nodes in the provisioning server."""
papi = get_provisioning_api_proxy()
- profile = select_profile_for_node(instance, papi)
+ profile = select_profile_for_node(instance)
power_type = instance.get_effective_power_type()
metadata = compose_metadata(instance)
papi.add_node(
diff -Nru maas-0.1+bzr400+dfsg/src/maasserver/static/css/components/yui_node_add.css maas-0.1+bzr415+dfsg/src/maasserver/static/css/components/yui_node_add.css
--- maas-0.1+bzr400+dfsg/src/maasserver/static/css/components/yui_node_add.css 1970-01-01 00:00:00.000000000 +0000
+++ maas-0.1+bzr415+dfsg/src/maasserver/static/css/components/yui_node_add.css 2012-04-04 18:59:25.000000000 +0000
@@ -0,0 +1,6 @@
+.yui3-node-add-widget {
+ width: 360px;
+ }
+.yui3-node-add-widget .buttons {
+ margin-top: 30px;
+ }
diff -Nru maas-0.1+bzr400+dfsg/src/maasserver/static/css/forms.css maas-0.1+bzr415+dfsg/src/maasserver/static/css/forms.css
--- maas-0.1+bzr400+dfsg/src/maasserver/static/css/forms.css 2012-04-03 22:07:07.000000000 +0000
+++ maas-0.1+bzr415+dfsg/src/maasserver/static/css/forms.css 2012-04-04 18:59:25.000000000 +0000
@@ -127,6 +127,22 @@
background-image: -webkit-linear-gradient(bottom, rgb(51,51,51) 0%, rgb(90,90,90) 100%);
background-image: -ms-linear-gradient(bottom, rgb(51,51,51) 0%, rgb(90,90,90) 100%);
}
+/* Disabled buttons */
+.yui3-button.disabled,
+.button.disabled,
+button.disabled {
+ cursor: not-allowed;
+ border-color: #999;
+ background-image: linear-gradient(bottom, rgb(200,200,200) 0%, rgb(150,150,150) 100%);
+ background-image: -o-linear-gradient(bottom, rgb(200,200,200) 0%, rgb(150,150,150) 100%);
+ background-image: -moz-linear-gradient(bottom, rgb(200,200,200) 0%, rgb(150,150,150) 100%);
+ background-image: -webkit-linear-gradient(bottom, rgb(200,200,200) 0%, rgb(150,150,150) 100%);
+ background-image: -ms-linear-gradient(bottom, rgb(200,200,200) 0%, rgb(150,150,150) 100%);
+ }
+.link-button {
+ display: inline-block;
+ padding: 6px 0;
+ }
.spinner {
float: right;
margin: 8px 10px 0 0;
diff -Nru maas-0.1+bzr400+dfsg/src/maasserver/static/css/import.css maas-0.1+bzr415+dfsg/src/maasserver/static/css/import.css
--- maas-0.1+bzr400+dfsg/src/maasserver/static/css/import.css 2012-04-03 22:07:07.000000000 +0000
+++ maas-0.1+bzr415+dfsg/src/maasserver/static/css/import.css 2012-04-04 18:59:25.000000000 +0000
@@ -11,5 +11,6 @@
@import url("components/blocks.css");
@import url("components/yui_panel.css");
@import url("components/yui_overlay.css");
+@import url("components/yui_node_add.css");
@import url("components/data_list.css");
@import url("components/search_box.css");
diff -Nru maas-0.1+bzr400+dfsg/src/maasserver/static/js/morph.js maas-0.1+bzr415+dfsg/src/maasserver/static/js/morph.js
--- maas-0.1+bzr400+dfsg/src/maasserver/static/js/morph.js 2012-04-03 22:07:07.000000000 +0000
+++ maas-0.1+bzr415+dfsg/src/maasserver/static/js/morph.js 2012-04-04 18:59:25.000000000 +0000
@@ -34,6 +34,15 @@
};
Y.extend(Morph, Y.Widget, {
+ initializer: function(cfg) {
+ if (Y.Lang.isValue(cfg.animate)) {
+ this._animate = cfg.animate;
+ }
+ else {
+ this._animate = true;
+ }
+ },
+
morph: function(reverse) {
if (reverse){
var srcNode = this.get('targetNode');
@@ -43,39 +52,50 @@
var srcNode = this.get('srcNode');
var targetNode = this.get('targetNode');
}
-
- target_height = targetNode.getComputedStyle('height');
- var fade_out = new Y.Anim({
- node: targetNode,
- to: {opacity: 0},
- duration: 0.2,
- easing: 'easeOut'
+ if (this._animate) {
+ var target_height = targetNode.getComputedStyle('height');
+ var fade_out = new Y.Anim({
+ node: targetNode,
+ to: {opacity: 0},
+ duration: 0.2,
+ easing: 'easeOut'
+ });
+ var self = this;
+ fade_out.on('end', function () {
+ targetNode.addClass('hidden');
+ srcNode.setStyle('opacity', 0);
+ srcNode.removeClass('hidden');
+ src_height = srcNode.getComputedStyle('height')
+ .replace('px', '');
+ srcNode.setStyle('height', target_height);
+ var fade_in = new Y.Anim({
+ node: srcNode,
+ to: {opacity: 1},
+ duration: 1,
+ easing: 'easeIn'
+ });
+ var resize = new Y.Anim({
+ node: srcNode,
+ to: {height: src_height},
+ duration: 0.5,
+ easing: 'easeOut'
+ });
+ resize.on('end', function () {
+ srcNode.setStyle('height', 'auto');
+ self.fire('morphed');
+ });
+ fade_in.run();
+ resize.run();
});
- fade_out.run();
- fade_out.on('end', function () {
+ fade_out.run();
+ }
+ else {
targetNode.addClass('hidden');
- srcNode.setStyle('opacity', 0);
srcNode.removeClass('hidden');
- src_height = srcNode.getComputedStyle('height').replace('px', '');
- srcNode.setStyle('height', target_height);
- var fade_in = new Y.Anim({
- node: srcNode,
- to: {opacity: 1},
- duration: 1,
- easing: 'easeIn'
- });
- var resize = new Y.Anim({
- node: srcNode,
- to: {height: src_height},
- duration: 0.5,
- easing: 'easeOut'
- });
- fade_in.run();
- resize.run();
- });
+ }
}
});
-module.Morph = Morph
+module.Morph = Morph;
}, '0.1', {'requires': ['widget', 'node', 'anim']});
diff -Nru maas-0.1+bzr400+dfsg/src/maasserver/static/js/node_add.js maas-0.1+bzr415+dfsg/src/maasserver/static/js/node_add.js
--- maas-0.1+bzr400+dfsg/src/maasserver/static/js/node_add.js 2012-04-03 22:07:07.000000000 +0000
+++ maas-0.1+bzr415+dfsg/src/maasserver/static/js/node_add.js 2012-04-04 18:59:25.000000000 +0000
@@ -36,10 +36,20 @@
return this.get(
'srcNode').all('input[name=mac_addresses]').size();
}
+ },
+
+ /**
+ * The DOM node to be morphed from.
+ *
+ * @attribute targetNode
+ * @type string
+ */
+ targetNode: {
+ value: null
}
};
-Y.extend(AddNodeWidget, Y.Panel, {
+Y.extend(AddNodeWidget, Y.Widget, {
/**
* Create an input field to add a MAC Address.
@@ -55,23 +65,6 @@
return Y.Node.create('').append(field);
},
- /**
- * Hide the panel.
- *
- * @method hidePanel
- */
- hidePanel: function() {
- var self = this;
- this.get('boundingBox').transition({
- duration: 0.5,
- top: '-400px'
- },
- function () {
- self.hide();
- self.destroy();
- });
- },
-
addMacField: function() {
if (this.get('nb_mac_fields') === 1) {
var label = this.get(
@@ -112,6 +105,15 @@
},
createForm: function() {
+ var addnode_button = Y.Node.create('')
+ .addClass('add-node-button')
+ .addClass('right')
+ .set('text', "Add node");
+ var cancel_button = Y.Node.create('')
+ .addClass('cancel-button')
+ .set('href', '#')
+ .set('text', "Cancel")
+ .addClass('link-button');
var macaddress_add_link = Y.Node.create('')
.addClass('add-link')
.addClass('add-mac-form')
@@ -123,6 +125,10 @@
.set('value', 'new');
var global_error = Y.Node.create('')
.addClass('form-errors');
+ var buttons = Y.Node.create('')
+ .addClass('buttons')
+ .append(addnode_button)
+ .append(cancel_button);
var addnodeform = Y.Node.create('')
.set('method', 'post')
.append(global_error)
@@ -130,7 +136,8 @@
.append(Y.Node.create(this.add_macaddress))
.append(macaddress_add_link)
.append(Y.Node.create(this.add_architecture))
- .append(Y.Node.create(this.add_node));
+ .append(Y.Node.create(this.add_node))
+ .append(buttons);
return addnodeform;
},
@@ -162,8 +169,8 @@
* @method showSpinner
*/
showSpinner: function() {
- var buttons = this.get('srcNode').one('.yui3-widget-button-wrapper');
- buttons.append(this.spinnerNode);
+ var buttons = this.get('srcNode').one('.add-node-button');
+ buttons.insert(this.spinnerNode, 'after');
},
/**
@@ -176,16 +183,57 @@
},
initializer: function(cfg) {
+ if (Y.Lang.isValue(cfg.animate)) {
+ this._animate = cfg.animate;
+ }
+ else {
+ this._animate = true;
+ }
+ this.get('srcNode').addClass('hidden');
+ this.morpher = new Y.maas.morph.Morph({
+ srcNode: cfg.srcNode,
+ targetNode: this.get('targetNode'),
+ animate: this._animate
+ });
+ },
+
+ renderUI: function() {
// Load form snippets.
this.add_macaddress = Y.one('#add-macaddress').getContent();
this.add_architecture = Y.one('#add-architecture').getContent();
this.add_node = Y.one('#add-node').getContent();
// Create panel's content.
- this.set('bodyContent', this.createForm());
+ var heading = Y.Node.create('')
+ .set('text', "Add node");
+ this.get('srcNode').append(heading).append(this.createForm());
this.initializeNodes();
},
/**
+ * Show the widget
+ *
+ * @method showWidget
+ */
+ showWidget: function() {
+ this.morpher.morph();
+ this.morpher.on('morphed', function(e, widget) {
+ widget.get('srcNode').one('input[type=text]').focus();
+ }, null, this);
+ },
+
+ /**
+ * Hide the widget
+ *
+ * @method showWidget
+ */
+ hideWidget: function() {
+ this.morpher.morph(true);
+ this.morpher.on('morphed', function(e, widget) {
+ widget.destroy();
+ }, null, this);
+ },
+
+ /**
* Initialize the nodes this widget will use.
*
* @method initializeNodes
@@ -207,14 +255,22 @@
bindUI: function() {
var self = this;
- this.get(
- 'bodyContent').one('.add-mac-form').on('click', function(e) {
+ var srcNode = this.get('srcNode');
+ srcNode.one('.add-mac-form').on('click', function(e) {
e.preventDefault();
self.addMacField();
});
- this.get('bodyContent').on('key', function() {
+ srcNode.on('key', function() {
self.sendAddNodeRequest();
}, 'press:enter');
+ srcNode.one('.add-node-button').on('click', function(e) {
+ e.preventDefault();
+ self.sendAddNodeRequest();
+ });
+ srcNode.one('.cancel-button').on('click', function(e, widget) {
+ e.preventDefault();
+ widget.hideWidget();
+ }, null, this);
},
addNode: function(node) {
@@ -231,10 +287,10 @@
start: Y.bind(self.showSpinner, self),
success: function(id, out) {
self.addNode(JSON.parse(out.response));
- self.hidePanel();
+ self.hideWidget();
},
failure: function(id, out) {
- Y.log("Adding a node failed. Response object follows.")
+ Y.log("Adding a node failed. Response object follows.");
Y.log(out);
if (out.status === 400) {
try {
@@ -297,11 +353,7 @@
*
* @method showAddNodeWidget
*/
-module.showAddNodeWidget = function(event) {
- // Cope with manual calls as well as event calls.
- if (Y.Lang.isValue(event)) {
- event.preventDefault();
- }
+module.showAddNodeWidget = function(cfg) {
// If a widget is already present, destroy it.
var destroy = (
Y.Lang.isValue(module._add_node_singleton) &&
@@ -309,48 +361,16 @@
if (destroy) {
module._add_node_singleton.destroy();
}
- var cfg = {
- headerContent: "Add node",
- buttons: [
- {
- value: 'Add node',
- section: 'footer',
- action: function (e) {
- e.preventDefault();
- this.sendAddNodeRequest();
- }
- },
- {
- value: 'Cancel',
- section: 'footer',
- classNames: 'link-button',
- action: function (e) {
- e.preventDefault();
- this.hidePanel();
- }
- }],
- align: {
- node:'',
- points:
- [Y.WidgetPositionAlign.BC, Y.WidgetPositionAlign.TC]
- },
- modal: true,
- zIndex: 2,
- visible: true,
- render: true,
- hideOn: []
- };
+
+ var srcNode = Y.Node.create('')
+ .set('id', 'add-node-widget');
+ cfg.srcNode = srcNode;
+ Y.one(cfg.targetNode).insert(srcNode, 'after');
module._add_node_singleton = new AddNodeWidget(cfg);
- module._add_node_singleton.get('boundingBox').transition({
- duration: 0.5,
- top: '0px'
- });
- // We need to set the focus late as the widget wants to set the focus
- // on the bounding box.
- module._add_node_singleton.get(
- 'boundingBox').one('input[type=text]').focus();
+ module._add_node_singleton.render();
+ module._add_node_singleton.showWidget();
};
-}, '0.1', {'requires': ['io', 'node', 'panel', 'event', 'event-custom',
- 'transition']}
+}, '0.1', {'requires': ['io', 'node', 'widget', 'event', 'event-custom',
+ 'maas.morph']}
);
diff -Nru maas-0.1+bzr400+dfsg/src/maasserver/static/js/tests/test_morph.js maas-0.1+bzr415+dfsg/src/maasserver/static/js/tests/test_morph.js
--- maas-0.1+bzr400+dfsg/src/maasserver/static/js/tests/test_morph.js 2012-04-03 22:07:07.000000000 +0000
+++ maas-0.1+bzr415+dfsg/src/maasserver/static/js/tests/test_morph.js 2012-04-04 18:59:25.000000000 +0000
@@ -25,6 +25,10 @@
Y.Assert.isTrue(
Y.one('#panel-two').hasClass('hidden'),
'The source panel should initially be hidden');
+ var morphed_fired = false;
+ morpher.on('morphed', function() {
+ morphed_fired = true;
+ });
morpher.morph();
this.wait(function() {
Y.Assert.isTrue(
@@ -33,6 +37,13 @@
Y.Assert.isFalse(
Y.one(cfg.srcNode).hasClass('hidden'),
'The source panel should now be visible');
+ Y.Assert.isTrue(
+ morphed_fired,
+ 'The morphed event should have fired');
+ Y.Assert.areEqual(
+ 'auto',
+ Y.one(cfg.srcNode).getStyle('height'),
+ 'The morpher should set the height back to auto');
/* Fire this morph again, this time for the reverse. */
morpher.morph(true);
this.wait(function() {
diff -Nru maas-0.1+bzr400+dfsg/src/maasserver/static/js/tests/test_node_add.html maas-0.1+bzr415+dfsg/src/maasserver/static/js/tests/test_node_add.html
--- maas-0.1+bzr400+dfsg/src/maasserver/static/js/tests/test_node_add.html 2012-04-03 22:07:07.000000000 +0000
+++ maas-0.1+bzr415+dfsg/src/maasserver/static/js/tests/test_node_add.html 2012-04-04 18:59:25.000000000 +0000
@@ -8,6 +8,7 @@
+
@@ -47,7 +48,7 @@
-
+
maas.node_add.tests