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('