diff -Nru maas-0.1+bzr415+dfsg/buildout.cfg maas-0.1+bzr462+dfsg/buildout.cfg --- maas-0.1+bzr415+dfsg/buildout.cfg 2012-04-04 18:59:25.000000000 +0000 +++ maas-0.1+bzr462+dfsg/buildout.cfg 2012-04-12 20:11:10.000000000 +0000 @@ -82,8 +82,10 @@ oops-datedir-repo oops-wsgi psycopg2 + pyasn1 rabbitfixture South + twisted entry-points = maas=django.core.management:execute_from_command_line initialization = diff -Nru maas-0.1+bzr415+dfsg/contrib/maas-http.conf maas-0.1+bzr462+dfsg/contrib/maas-http.conf --- maas-0.1+bzr415+dfsg/contrib/maas-http.conf 2012-04-04 18:59:25.000000000 +0000 +++ maas-0.1+bzr462+dfsg/contrib/maas-http.conf 2012-04-12 20:11:10.000000000 +0000 @@ -1,6 +1,11 @@ +WSGIDaemonProcess maas user=maas group=maas processes=2 threads=1 WSGIScriptAlias /MAAS /usr/share/maas/wsgi.py WSGIPassAuthorization On + + WSGIProcessGroup maas + + ExpiresActive On diff -Nru maas-0.1+bzr415+dfsg/contrib/maas_local_settings_sample.py maas-0.1+bzr462+dfsg/contrib/maas_local_settings_sample.py --- maas-0.1+bzr415+dfsg/contrib/maas_local_settings_sample.py 2012-04-04 18:59:25.000000000 +0000 +++ maas-0.1+bzr462+dfsg/contrib/maas_local_settings_sample.py 2012-04-12 20:11:10.000000000 +0000 @@ -68,3 +68,7 @@ 'HOST': 'localhost', } } + +# The location of the Provisioning API XML-RPC endpoint. +from getpass import getuser +PSERV_URL = "http://%s:password@localhost:5241/api" % getuser() diff -Nru maas-0.1+bzr415+dfsg/contrib/preseeds/maas-commissioning.preseed maas-0.1+bzr462+dfsg/contrib/preseeds/maas-commissioning.preseed --- maas-0.1+bzr415+dfsg/contrib/preseeds/maas-commissioning.preseed 1970-01-01 00:00:00.000000000 +0000 +++ maas-0.1+bzr462+dfsg/contrib/preseeds/maas-commissioning.preseed 2012-04-12 20:11:10.000000000 +0000 @@ -0,0 +1 @@ +$SNIPPET('maas_preseed') diff -Nru maas-0.1+bzr415+dfsg/contrib/preseeds/maas-enlist.preseed maas-0.1+bzr462+dfsg/contrib/preseeds/maas-enlist.preseed --- maas-0.1+bzr415+dfsg/contrib/preseeds/maas-enlist.preseed 2012-04-04 18:59:25.000000000 +0000 +++ maas-0.1+bzr462+dfsg/contrib/preseeds/maas-enlist.preseed 2012-04-12 20:11:10.000000000 +0000 @@ -2,3 +2,5 @@ d-i maas-enlist/skip-maas-discover boolean true d-i maas-enlist/maas-server-address string @@server@@ d-i maas-enlist/host-name string + +$SNIPPET('maas_proxy') diff -Nru maas-0.1+bzr415+dfsg/contrib/snippets/maas_client_packages maas-0.1+bzr462+dfsg/contrib/snippets/maas_client_packages --- maas-0.1+bzr415+dfsg/contrib/snippets/maas_client_packages 2012-04-04 18:59:25.000000000 +0000 +++ maas-0.1+bzr462+dfsg/contrib/snippets/maas_client_packages 2012-04-12 20:11:10.000000000 +0000 @@ -1 +1 @@ -d-i pkgsel/include string cloud-init openssh-server python-software-properties vim avahi-daemon +d-i pkgsel/include string cloud-init openssh-server python-software-properties vim avahi-daemon server^ diff -Nru maas-0.1+bzr415+dfsg/.ctags maas-0.1+bzr462+dfsg/.ctags --- maas-0.1+bzr415+dfsg/.ctags 2012-04-04 18:59:25.000000000 +0000 +++ maas-0.1+bzr462+dfsg/.ctags 2012-04-12 20:11:10.000000000 +0000 @@ -1,5 +1,4 @@ --python-kinds=-iv ---exclude=*-min.js ---exclude=*-debug.js +--exclude=*.js --extra=+f ---links=no +--links=yes diff -Nru maas-0.1+bzr415+dfsg/debian/changelog maas-0.1+bzr462+dfsg/debian/changelog --- maas-0.1+bzr415+dfsg/debian/changelog 2012-04-06 20:03:41.000000000 +0000 +++ maas-0.1+bzr462+dfsg/debian/changelog 2012-04-12 20:46:28.000000000 +0000 @@ -1,3 +1,45 @@ +maas (0.1+bzr462+dfsg-0ubuntu1) precise; urgency=low + + * New upstream release (LP: #980240) + + [ Scott Moser ] + * add dependency on distro-info (LP: #949442) + * debian/control: add dependency on tgt for ephemeral iscsi environment + + [ Andres Rodriguez ] + * Make package lintian clean: + - maas{-dhcp}.lintian-overrides: Add to make lintian clean. + - debian/control: Add missing dependencies; correct section and desc. + - debian/maas.postinst: Do not use absolute path for rabbitmqctl. + - debian/patches: Add headers to all patches. + * debian/maas-dhcp.postrm: Added to disable dnsmasq in cobbler on removal. + * debian/maas.config: Do not set a password with pwgen as it is not an + essential package; allow dbconfig-common to create a password instead by + creating an empty question. (LP: #977475) + * Run MAAS, pserv, txlongpoll as non-root user. (LP: #975436) + - debian/maas.postinst: Create user/group; set correct permissions for + directories. + - debian/maas.postrm: Remove user/group; restart apache2. + - debian/maas.maas-{pserv,txlongpoll}.upstart: Update to run as non-root + 'maas' user. + * debian/patches/01-fix-database-settings.patch: Remove adding of PSERV_URL. + * debian/maas.postinst: + - Handle config file upgrade from versions lower than 0.1+bzr445+dfsg-0ubuntu1, + by creating new passwords and updating accordingly + - use local variables in functions. + - Handle maas tgt configuration for upgrades from 0.1+bzr459+dfsg-0ubuntu1. + * debian/extras/99-maas: Add squid-deb-proxy file to enable PPAs. (LP: #979383) + * debian/maas.install: Install missing commissioning-user-data script. + + [ Dave Walker (Daviey) ] + * debian/patches/02-pserv-config.patch: Refreshed to apply to updated config. + + [ Gavin Panella ] + * debian/maas.postinst: Update pserv.yaml and maas_local_settings.py to use + password. + + -- Andres Rodriguez Thu, 12 Apr 2012 16:46:22 -0400 + maas (0.1+bzr415+dfsg-0ubuntu2) precise; urgency=low * debian/maas-dhcp.{config,postinst}: diff -Nru maas-0.1+bzr415+dfsg/debian/control maas-0.1+bzr462+dfsg/debian/control --- maas-0.1+bzr415+dfsg/debian/control 2012-04-06 20:03:41.000000000 +0000 +++ maas-0.1+bzr462+dfsg/debian/control 2012-04-12 20:38:52.000000000 +0000 @@ -13,6 +13,7 @@ avahi-daemon, cobbler, dbconfig-common, + distro-info, libapache2-mod-wsgi, postgresql-9.1, pwgen, @@ -23,6 +24,7 @@ rabbitmq-server, rsyslog, squid-deb-proxy, + tgt, ${misc:Depends}, ${python:Depends} Recommends: openssh-server @@ -39,6 +41,7 @@ Package: python-django-maas +Section: python Architecture: all Depends: python-avahi, python-convoy, @@ -70,8 +73,8 @@ Package: maas-dhcp Architecture: all -Depends: maas, dnsmasq -Description: Ubuntu MAAS Server - DHCP configuration +Depends: ${misc:Depends}, maas, debconf, dnsmasq +Description: Ubuntu MAAS Server - DHCP configuration (metapackage) 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. diff -Nru maas-0.1+bzr415+dfsg/debian/extras/99-maas maas-0.1+bzr462+dfsg/debian/extras/99-maas --- maas-0.1+bzr415+dfsg/debian/extras/99-maas 1970-01-01 00:00:00.000000000 +0000 +++ maas-0.1+bzr462+dfsg/debian/extras/99-maas 2012-04-12 20:38:52.000000000 +0000 @@ -0,0 +1,4 @@ +# /etc/squid-deb-proxy/mirror-dstdomain.acl.d/99-maas +# +# Enable launchpad personal package archives for squid-deb-proxy +ppa.launchpad.net diff -Nru maas-0.1+bzr415+dfsg/debian/maas.config maas-0.1+bzr462+dfsg/debian/maas.config --- maas-0.1+bzr415+dfsg/debian/maas.config 2012-04-06 20:03:41.000000000 +0000 +++ maas-0.1+bzr462+dfsg/debian/maas.config 2012-04-12 20:38:52.000000000 +0000 @@ -22,13 +22,12 @@ fi if ([ "$1" = "configure" ] && [ -z "$2" ]); then - maas_db_pass=`pwgen` dbc_dbname="maasdb" dbc_dbuser="maas" - dbc_dbpass="$maas_db_pass" dbc_remove="true" # Hide maas/dbconfig-install question by setting default. set_question maas/dbconfig-install true + set_question maas/pgsql/app-pass "" dbc_go maas $@ fi diff -Nru maas-0.1+bzr415+dfsg/debian/maas-dhcp.lintian-overrides maas-0.1+bzr462+dfsg/debian/maas-dhcp.lintian-overrides --- maas-0.1+bzr415+dfsg/debian/maas-dhcp.lintian-overrides 1970-01-01 00:00:00.000000000 +0000 +++ maas-0.1+bzr462+dfsg/debian/maas-dhcp.lintian-overrides 2012-04-12 20:38:52.000000000 +0000 @@ -0,0 +1 @@ +maas-dhcp: postinst-uses-db-input diff -Nru maas-0.1+bzr415+dfsg/debian/maas-dhcp.postrm maas-0.1+bzr462+dfsg/debian/maas-dhcp.postrm --- maas-0.1+bzr415+dfsg/debian/maas-dhcp.postrm 1970-01-01 00:00:00.000000000 +0000 +++ maas-0.1+bzr462+dfsg/debian/maas-dhcp.postrm 2012-04-12 20:38:52.000000000 +0000 @@ -0,0 +1,20 @@ +#!/bin/sh + +set -e + +. /usr/share/debconf/confmodule +db_version 2.0 + +if [ "$1" = "purge" ] ; then + if [ -f /etc/cobbler/settings ]; then + sed -i -e "s/^manage_dns:.*$/manage_dns: 0/" \ + -e "s/^manage_dhcp:.*$/manage_dhcp: 0/" /etc/cobbler/settings + 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+bzr415+dfsg/debian/maas.install maas-0.1+bzr462+dfsg/debian/maas.install --- maas-0.1+bzr415+dfsg/debian/maas.install 2012-04-06 20:03:41.000000000 +0000 +++ maas-0.1+bzr462+dfsg/debian/maas.install 2012-04-12 20:38:52.000000000 +0000 @@ -20,11 +20,16 @@ etc/cron.d/maas-import-isos etc/cron.d/maas-gc etc/maas/import_isos +etc/maas/import_ephemerals +etc/maas/commissioning-user-data man/maas-import-isos.8 usr/share/man/man8 scripts/maas-import-isos usr/sbin +scripts/maas-import-ephemerals usr/sbin +scripts/maas-cloudimg2ephemeral usr/sbin debian/extras/maas usr/bin debian/extras/20-maas.conf etc/rsyslog.d debian/extras/maas_remote_syslog_compress etc/cron.d +debian/extras/99-maas etc/squid-deb-proxy/mirror-dstdomain.acl.d # Install preseeds/snippets contrib/preseeds/* var/lib/cobbler/kickstarts diff -Nru maas-0.1+bzr415+dfsg/debian/maas.lintian-overrides maas-0.1+bzr462+dfsg/debian/maas.lintian-overrides --- maas-0.1+bzr415+dfsg/debian/maas.lintian-overrides 1970-01-01 00:00:00.000000000 +0000 +++ maas-0.1+bzr462+dfsg/debian/maas.lintian-overrides 2012-04-12 20:38:52.000000000 +0000 @@ -0,0 +1,7 @@ +maas: embedded-javascript-library usr/share/maas/web/static/jslibs/yui/3.4.1/build/yui/yui-min.js +maas: extra-license-file usr/share/maas/web/static/jslibs/yui/3.4.1/docs/assets/panel/vendor/prettify/COPYING +maas: extra-license-file usr/share/maas/web/static/jslibs/yui/3.4.1/docs/assets/vendor/prettify/COPYING +maas: script-not-executable usr/share/maas/web/static/jslibs/yui/3.4.1/tests/loader/tests/server/server.js +maas: unusual-interpreter usr/share/maas/web/static/jslibs/yui/3.4.1/tests/loader/tests/server/server.js #!node +maas: postinst-uses-db-input +maas: binary-without-manpage usr/bin/maas diff -Nru maas-0.1+bzr415+dfsg/debian/maas.maas-pserv.upstart maas-0.1+bzr462+dfsg/debian/maas.maas-pserv.upstart --- maas-0.1+bzr415+dfsg/debian/maas.maas-pserv.upstart 2012-04-06 20:03:41.000000000 +0000 +++ maas-0.1+bzr462+dfsg/debian/maas.maas-pserv.upstart 2012-04-12 20:38:52.000000000 +0000 @@ -11,4 +11,4 @@ respawn # To add options to your daemon, edit the line below: -exec /usr/bin/twistd -n --pidfile=/run/maas-pserv.pid --logfile=/dev/null maas-pserv --config-file=/etc/maas/pserv.yaml +exec /usr/bin/twistd -n --uid=maas --gid=maas --pidfile=/run/maas-pserv.pid --logfile=/dev/null maas-pserv --config-file=/etc/maas/pserv.yaml diff -Nru maas-0.1+bzr415+dfsg/debian/maas.maas-txlongpoll.upstart maas-0.1+bzr462+dfsg/debian/maas.maas-txlongpoll.upstart --- maas-0.1+bzr415+dfsg/debian/maas.maas-txlongpoll.upstart 2012-04-06 20:03:41.000000000 +0000 +++ maas-0.1+bzr462+dfsg/debian/maas.maas-txlongpoll.upstart 2012-04-12 20:38:52.000000000 +0000 @@ -24,4 +24,4 @@ end script # To add options to your daemon, edit the line below: -exec /usr/bin/twistd -n --pidfile=/run/maas-txlongpoll.pid --logfile=/dev/null txlongpoll --config-file=/etc/maas/txlongpoll.yaml +exec /usr/bin/twistd -n --uid=maas --gid=maas --pidfile=/run/maas-txlongpoll.pid --logfile=/dev/null txlongpoll --config-file=/etc/maas/txlongpoll.yaml diff -Nru maas-0.1+bzr415+dfsg/debian/maas.postinst maas-0.1+bzr462+dfsg/debian/maas.postinst --- maas-0.1+bzr415+dfsg/debian/maas.postinst 2012-04-06 20:03:41.000000000 +0000 +++ maas-0.1+bzr462+dfsg/debian/maas.postinst 2012-04-12 20:38:52.000000000 +0000 @@ -3,6 +3,10 @@ . /usr/share/debconf/confmodule db_version 2.0 +if [ -f /usr/share/dbconfig-common/dpkg/postinst.pgsql ]; then + . /usr/share/dbconfig-common/dpkg/postinst.pgsql +fi + maas_sync_migrate_db(){ maas syncdb --noinput maas migrate maasserver --noinput @@ -17,17 +21,99 @@ fi } -if [ -f /usr/share/dbconfig-common/dpkg/postinst.pgsql ]; then - . /usr/share/dbconfig-common/dpkg/postinst.pgsql -fi +add_user_group(){ + local user="maas" + local group="maas" + if ! getent group "$group" >/dev/null; then + addgroup --quiet --system "$group" || true + fi + if ! getent passwd "$user" > /dev/null 2>&1; then + adduser --quiet \ + --system \ + --group \ + --no-create-home \ + "$user" || true + fi +} + +configure_maas_cobbler_user() { + # Create 'maas' user and password to autoconfigure + local cblr_pass= + local hash= + cblr_pass="$(pwgen -s 20)" + hash=$(printf "maas:Cobbler:$cblr_pass" | md5sum | awk '{print $1}') + [ -e /etc/cobbler/users.digest ] || install -o root -g root -m 0600 /dev/null /etc/cobbler/users.digest + htpasswd -D /etc/cobbler/users.digest "maas" || true + printf "maas:Cobbler:$hash\n" >> /etc/cobbler/users.digest + + if grep -qs "^\ \{1,\}password: [a-zA-Z0-9]\{1,\}$" /etc/maas/pserv.yaml; then + sed -i "s/^\ \{1,\}password: [a-zA-Z0-9]\{1,\}$/ password: "$cblr_pass"/" /etc/maas/pserv.yaml + fi +} + +configure_maas_pserv_user() { + local pserv_pass= + pserv_pass="$(pwgen -s 20)" + if grep -qs "^password: \"test\"" /etc/maas/pserv.yaml; then + sed -i '/^password:/s/"test"/"'"${pserv_pass}"'"/' /etc/maas/pserv.yaml + fi + if grep -qs "^PSERV_URL\ =\ " /etc/maas/maas_local_settings.py; then + sed -i '/^PSERV_URL[ =]/s/:password@/:'"${pserv_pass}"'@/' /etc/maas/maas_local_settings.py + fi +} + +configure_maas_txlongpoll_rabbitmq_user() { + local longpoll_user="maas_longpoll" + local longpoll_pass= + local longpoll_vhost="/maas_longpoll" + longpoll_pass="$(pwgen -s 20)" + if [ -x /usr/sbin/rabbitmqctl ]; then + if ! rabbitmqctl list_users | grep -qs "$longpoll_user"; then + rabbitmqctl add_user "$longpoll_user" "$longpoll_pass" || true + rabbitmqctl add_vhost "$longpoll_vhost" || true + rabbitmqctl set_permissions -p "$longpoll_vhost" "$longpoll_user" ".*" ".*" ".*" || true + else + rabbitmqctl change_password "$longpoll_user" "$longpoll_pass" || true + fi + fi + + if grep -qs "^\ \{1,\}password: \"[a-zA-Z0-9]\{0,\}\"$" /etc/maas/txlongpoll.yaml; then + sed -i "s/^\ \{1,\}password: \"[a-zA-Z0-9]\{0,\}\"$/ password: \""$longpoll_pass"\"/" \ + /etc/maas/txlongpoll.yaml + fi + if grep -qs "^RABBITMQ_PASSWORD\ \= '[a-zA-Z0-9]\{0,\}'$" /etc/maas/maas_local_settings.py; then + sed -i "s/^RABBITMQ_PASSWORD\ \= '[a-zA-Z0-9]\{0,\}'$/RABBITMQ_PASSWORD = '"$longpoll_pass"'/" \ + /etc/maas/maas_local_settings.py + fi +} + +configure_maas_database() { + local dbc_dbpass="$1" + if grep -qs "^\ \{1,\} 'PASSWORD': '[a-zA-Z0-9]\{0,\}',$" /etc/maas/maas_local_settings.py; then + sed -i "s/^\ \{1,\} 'PASSWORD': '[a-zA-Z0-9]\{0,\}',$/ 'PASSWORD': '"$dbc_dbpass"',/" \ + /etc/maas/maas_local_settings.py + fi +} + +configure_maas_tgt(){ + local tgtcfg="/etc/tgt/targets.conf" + [ -d /etc/tgt/conf.d/ ] || + echo "Warning! $tgtcfg did not exist" 1>&2; + mkdir -p /etc/tgt/conf.d/ /var/lib/maas/ephemeral/ + ln -sf /var/lib/maas/ephemeral/tgt.conf /etc/tgt/conf.d/maas.conf +} if [ "$1" = "configure" ] && [ -z "$2" ]; then + ######################################################### + ################ User/Group Creatiion ################## + ######################################################### + add_user_group ######################################################### ################ Folder Permissions #################### ######################################################### mkdir -p /var/lib/maas/media/storage - chown -R www-data:www-data /var/lib/maas/ + chown -R maas:maas /var/lib/maas/ ######################################################### ################ Configure Apache2 #################### @@ -49,16 +135,13 @@ ########### Configure maas user for Cobbler ############# ######################################################### - # Create 'maas' user and password to autoconfigure - cblr_pass=`pwgen` - hash=$(printf "maas:Cobbler:$cblr_pass" | md5sum | awk '{print $1}') - [ -e /etc/cobbler/users.digest ] || install -o root -g root -m 0600 /dev/null /etc/cobbler/users.digest - htpasswd -D /etc/cobbler/users.digest "maas" || true - printf "maas:Cobbler:$hash\n" >> /etc/cobbler/users.digest + configure_maas_cobbler_user - if grep -qs "^\ \{1,\}password: [a-zA-Z0-9]\{1,\}$" /etc/maas/pserv.yaml; then - sed -i "s/^\ \{1,\}password: [a-zA-Z0-9]\{1,\}$/ password: "$cblr_pass"/" /etc/maas/pserv.yaml - fi + ######################################################### + ############ Configure maas user for pserv ############## + ######################################################### + + configure_maas_pserv_user ######################################################### ########## Configure DEFAULT_MAAS_URL ################# @@ -89,7 +172,7 @@ if [ ! -f /var/log/maas/maas.log ]; then touch /var/log/maas/maas.log fi - chown -R root:www-data /var/log/maas + chown -R maas:maas /var/log/maas chmod 620 /var/log/maas/maas.log chmod -R 775 /var/log/maas/oops @@ -100,7 +183,15 @@ if [ -x /usr/sbin/invoke-rc.d ]; then invoke-rc.d rsyslog restart fi - + + ######################################################### + ################### Squid-deb-proxy #################### + ######################################################### + # Make sure squid-deb-proxy reads our config (99-maas) + if [ -x /usr/sbin/invoke-rc.d ]; then + invoke-rc.d squid-deb-proxy restart + fi + ######################################################### ########## Configure longpoll rabbitmq config ########### ######################################################### @@ -111,23 +202,12 @@ else /etc/init.d/rabbitmq-server restart || true fi - longpoll_user="maas_longpoll" - longpoll_pass=`pwgen` - longpoll_vhost="/maas_longpoll" - if [ -x /usr/sbin/rabbitmqctl ]; then - /usr/sbin/rabbitmqctl add_user "$longpoll_user" "$longpoll_pass" || true - /usr/sbin/rabbitmqctl add_vhost "$longpoll_vhost" || true - /usr/sbin/rabbitmqctl set_permissions -p "$longpoll_vhost" "$longpoll_user" ".*" ".*" ".*" || true - fi + configure_maas_txlongpoll_rabbitmq_user - if grep -qs "^\ \{1,\}password: \"[a-zA-Z0-9]\{0,\}\"$" /etc/maas/txlongpoll.yaml; then - sed -i "s/^\ \{1,\}password: \"[a-zA-Z0-9]\{0,\}\"$/ password: \""$longpoll_pass"\"/" \ - /etc/maas/txlongpoll.yaml - fi - if grep -qs "^RABBITMQ_PASSWORD\ \= '[a-zA-Z0-9]\{0,\}'$" /etc/maas/maas_local_settings.py; then - sed -i "s/^RABBITMQ_PASSWORD\ \= '[a-zA-Z0-9]\{0,\}'$/RABBITMQ_PASSWORD = '"$longpoll_pass"'/" \ - /etc/maas/maas_local_settings.py - fi + ######################################################### + ######## add maas.conf to tgt conf.d #################### + ######################################################### + configure_maas_tgt ######################################################### ################ Configure Database ################### @@ -139,12 +219,11 @@ else /etc/init.d/postgresql restart || true fi + # Create the database dbc_go maas $@ - if grep -qs "^\ \{1,\} 'PASSWORD': '[a-zA-Z0-9]\{0,\}',$" /etc/maas/maas_local_settings.py; then - sed -i "s/^\ \{1,\} 'PASSWORD': '[a-zA-Z0-9]\{0,\}',$/ 'PASSWORD': '"$dbc_dbpass"',/" \ - /etc/maas/maas_local_settings.py - fi + configure_maas_database "$dbc_dbpass" + # Only syncdb if we have selected to install it with dbconfig-common. db_get maas/dbconfig-install if [ "$RET" = "true" ]; then @@ -160,8 +239,27 @@ if [ -x /usr/sbin/invoke-rc.d ]; then invoke-rc.d apache2 stop || true fi + + # If upgrading from any version lower than 0.1+bzr445+dfsg-0ubuntu1 + # we need to update the user/group. + if dpkg --compare-versions "$2" lt 0.1+bzr445+dfsg-0ubuntu1; then + add_user_group + chown -R maas:maas /var/lib/maas/ + chown -R maas:maas /var/log/maas + chown -R syslog:syslog /var/log/maas/rsyslog + configure_maas_cobbler_user + configure_maas_pserv_user + configure_maas_txlongpoll_rabbitmq_user + dbc_go maas $@ + configure_maas_database "$dbc_dbpass" + fi + if dpkg --compare-versions "$2" lt 0.1+bzr459+dfsg-0ubuntu1; then + configure_maas_tgt + fi + maas_sync_migrate_db restart_apache2 + fi db_stop diff -Nru maas-0.1+bzr415+dfsg/debian/maas.postrm maas-0.1+bzr462+dfsg/debian/maas.postrm --- maas-0.1+bzr415+dfsg/debian/maas.postrm 2012-04-06 20:03:41.000000000 +0000 +++ maas-0.1+bzr462+dfsg/debian/maas.postrm 2012-04-12 20:38:52.000000000 +0000 @@ -17,6 +17,18 @@ rm -rf /etc/apache2/conf.d/maas-http.conf fi + # Restarting apache2 + if [ -x /usr/sbin/invoke-rc.d ]; then + invoke-rc.d apache2 restart || true + else + /etc/init.d/apache2 restart || true + fi + + # Deleting user/group + if getent passwd maas >/dev/null; then + deluser maas || true + fi + # Remove rabbitmq/longpoll longpoll_user="maas_longpoll" longpoll_vhost="/maas_longpoll" @@ -28,4 +40,6 @@ #DEBHELPER# +db_stop + exit 0 diff -Nru maas-0.1+bzr415+dfsg/debian/patches/01-fix-database-settings.patch maas-0.1+bzr462+dfsg/debian/patches/01-fix-database-settings.patch --- maas-0.1+bzr415+dfsg/debian/patches/01-fix-database-settings.patch 2012-04-06 20:03:41.000000000 +0000 +++ maas-0.1+bzr462+dfsg/debian/patches/01-fix-database-settings.patch 2012-04-12 20:38:52.000000000 +0000 @@ -1,21 +1,23 @@ -Index: maas-0.1+bzr309+dfsg.orig/contrib/maas_local_settings_sample.py +Description: Use default settings for MAAS + Use default settings for MAAS to work out of the box. + These include setting PSERV_URL, STATIC_ROOT, and the + configuration for the database. +Author: Andres Rodriguez + +Index: maas-0.1+bzr445+dfsg.orig/contrib/maas_local_settings_sample.py =================================================================== ---- maas-0.1+bzr309+dfsg.orig.orig/contrib/maas_local_settings_sample.py 2012-03-19 14:56:36.884477638 -0400 -+++ maas-0.1+bzr309+dfsg.orig/contrib/maas_local_settings_sample.py 2012-03-19 14:56:47.004477016 -0400 -@@ -6,8 +6,11 @@ - # override this. +--- maas-0.1+bzr445+dfsg.orig.orig/contrib/maas_local_settings_sample.py 2012-04-11 11:59:20.402953927 -0400 ++++ maas-0.1+bzr445+dfsg.orig/contrib/maas_local_settings_sample.py 2012-04-11 12:14:10.379958510 -0400 +@@ -7,7 +7,7 @@ DEFAULT_MAAS_URL = "http://maas.internal.example.com/" -+# Default URL specifying provisioning server URL. -+PSERV_URL = "http://localhost:5241/api" -+ # Absolute path to the directory static files should be collected to. -STATIC_ROOT = '/var/lib/maas/static/' +STATIC_ROOT = '/usr/share/maas/web/static/' # Prefix to use for MAAS's urls. # If FORCE_SCRIPT_NAME is None (the default), all the urls will start with -@@ -62,9 +65,9 @@ +@@ -62,9 +62,9 @@ 'default': { # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' etc. 'ENGINE': 'django.db.backends.postgresql_psycopg2', diff -Nru maas-0.1+bzr415+dfsg/debian/patches/02-pserv-config.patch maas-0.1+bzr462+dfsg/debian/patches/02-pserv-config.patch --- maas-0.1+bzr415+dfsg/debian/patches/02-pserv-config.patch 2012-04-06 20:03:41.000000000 +0000 +++ maas-0.1+bzr462+dfsg/debian/patches/02-pserv-config.patch 2012-04-12 20:38:52.000000000 +0000 @@ -1,15 +1,28 @@ -Index: maas-0.1+bzr398+dfsg.orig/etc/pserv.yaml +Description: Use default settings for MAAS PSERV + Use default settings for MAAS pserv. These default settings + include port, logfile, oops directory, cobbler url and username +Author: Andres Rodriguez + +Index: maas-0.1+bzr445+dfsg.orig/etc/pserv.yaml =================================================================== ---- maas-0.1+bzr398+dfsg.orig.orig/etc/pserv.yaml 2012-04-03 09:49:48.153428333 -0400 -+++ maas-0.1+bzr398+dfsg.orig/etc/pserv.yaml 2012-04-03 09:51:53.719228342 -0400 -@@ -4,13 +4,13 @@ +--- maas-0.1+bzr445+dfsg.orig.orig/etc/pserv.yaml 2012-04-11 13:11:30.827410133 -0400 ++++ maas-0.1+bzr445+dfsg.orig/etc/pserv.yaml 2012-04-11 13:11:46.007415852 -0400 +@@ -4,12 +4,13 @@ ## The port on which the Provisioning API will be made available. # -# port: 5241 +port: 5241 - ## Where to log. This log can be rotated by sending SIGUSR1 to the + ## The credentials which the Provisioning API will require. + # + # username: + # password: ++username: maas + password: "test" + + ## Network interface to bind the service on. +@@ -22,7 +23,7 @@ ## running server. # # logfile: "pserv.log" @@ -18,7 +31,7 @@ ## OOPS configuration (optional). # -@@ -19,7 +19,7 @@ +@@ -31,7 +32,7 @@ # or directories other than what the oops machinery creates there. # # directory: @@ -27,7 +40,7 @@ # reporter: reporter: "maas-pserv" -@@ -41,6 +41,6 @@ +@@ -53,6 +54,6 @@ # password: "test" # vdenv specific; see vdenv/README.txt. diff -Nru maas-0.1+bzr415+dfsg/debian/patches/03-txlongpoll-config.patch maas-0.1+bzr462+dfsg/debian/patches/03-txlongpoll-config.patch --- maas-0.1+bzr415+dfsg/debian/patches/03-txlongpoll-config.patch 2012-04-06 20:03:41.000000000 +0000 +++ maas-0.1+bzr462+dfsg/debian/patches/03-txlongpoll-config.patch 2012-04-12 20:38:52.000000000 +0000 @@ -1,3 +1,8 @@ +Description: Use default settings for MAAS txlongpoll + Use default settings for MAAS txlongpoll. These default settings + include oops directory, Message broker configuration, logfile. +Author: Andres Rodriguez + Index: maas-0.1+bzr398+dfsg.orig/etc/txlongpoll.yaml =================================================================== --- maas-0.1+bzr398+dfsg.orig.orig/etc/txlongpoll.yaml 2012-04-03 09:49:48.109427702 -0400 diff -Nru maas-0.1+bzr415+dfsg/docs/hacking.rst maas-0.1+bzr462+dfsg/docs/hacking.rst --- maas-0.1+bzr415+dfsg/docs/hacking.rst 2012-04-04 18:59:25.000000000 +0000 +++ maas-0.1+bzr462+dfsg/docs/hacking.rst 2012-04-12 20:11:10.000000000 +0000 @@ -1,3 +1,5 @@ +.. -*- mode: rst -*- + ************ Hacking MAAS ************ @@ -219,6 +221,7 @@ .. _South's documentation: http://south.aeracode.org/docs/ + Changing the schema ^^^^^^^^^^^^^^^^^^^ @@ -244,6 +247,7 @@ $ make syncdb + Performing data migration ^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -280,14 +284,18 @@ .. _convention for headings as used in the Python documentation: http://sphinx.pocoo.org/rest.html#sections + Acceptance tests ================ -MAAS uses checkbox manual testing infrastructure to verify features -are implemented according to the spec. They can be found in the official -checkbox repository: -https://code.launchpad.net/~nskaggs/checkbox/checkbox-app-testing-qt -in the jobs/ directory. +MAAS uses `Checkbox`_ to verify features are implemented according to +the spec. They can be found in the `official checkbox repository`_, in +the ``jobs/`` directory. + +.. _Checkbox: https://wiki.ubuntu.com/Testing/Automation/Checkbox + +.. _official checkbox repository: + https://code.launchpad.net/~nskaggs/checkbox/checkbox-app-testing-qt You need to install additional QT dependencies:: @@ -297,6 +305,6 @@ $ bzr branch lp:~nskaggs/checkbox/checkbox-app-testing-qt -To run them, cd into the directory:: +To run them, move into the branch directory and run:: $ bin/checbox-app-testing --whitelist-file= diff -Nru maas-0.1+bzr415+dfsg/docs/install.rst maas-0.1+bzr462+dfsg/docs/install.rst --- maas-0.1+bzr415+dfsg/docs/install.rst 2012-04-04 18:59:25.000000000 +0000 +++ maas-0.1+bzr462+dfsg/docs/install.rst 2012-04-12 20:11:10.000000000 +0000 @@ -1,12 +1,14 @@ +.. -*- mode: rst -*- + *************** Installing MAAS *************** - + There are two main ways to install MAAS: * as part of a fresh Ubuntu install using the Ubuntu Server installer * or from Ubuntu's archive on an existing Ubuntu install. - + This is a guide to installing MAAS from the Ubuntu archive. It assumes that you're working with: @@ -16,75 +18,85 @@ * control of the network your machine is connected to * internet access or a local mirror of the Ubuntu archive. + Installing MAAS from the archive ================================ -Installing MAAS is straightforward. At the commandline, type:: +Installing MAAS is straightforward. At the command-line, type:: + + $ sudo apt-get install maas - $ sudo apt-get install maas +From a fresh Ubuntu 12.04 LTS install, MAAS will pull down around 200 +MB of packages. -From a fresh Ubuntu 12.04 LTS install, MAAS will pull down around 200 MB of packages. Creating a superuser account ---------------------------- -Once MAAS is installed, you'll need to create your first administrator account. +Once MAAS is installed, you'll need to create your first administrator +account:: + + $ maas createsuperuser -At the commandline, type:: +Follow the prompts and MAAS will create an admin account that you can +later use to log in. - $ maas createsuperuser - -Follow the prompts and MAAS will create an admin account that you can later use to log in. Configuring a DHCP server ========================= -So that MAAS can PXE boot machines, you'll need a DHCP server. MAAS can work -with your existing DHCP set-up but for this guide we'll use dnsmasq. +So that MAAS can PXE boot machines, you'll need a DHCP server. MAAS +can work with your existing DHCP set-up but for this guide we'll use +dnsmasq. -dnsmasq should already be installed. However, if it is not, enter the -following:: +dnsmasq should already be installed; install it if not:: - $ sudo apt-get install dnsmasq + $ sudo apt-get install dnsmasq + +MAAS enlists nodes using a tool called Cobbler. Cobbler provides a +configuration file for dnsmasq: ``/etc/cobbler/dnsmasq.template``. -MAAS enlists nodes using a tool called Cobbler. Cobbler provides a -configuration file for dnsmasq: `/etc/cobbler/dnsmasq.template`. - Make the following changes: - * ``domain``: if applicable, specify your network's domain. - * ``dhcp-range``: specify the range from wluke haineshich dnsmasq should allocate IP - addresses to servers in your MAAS. - * ``dhcp-option=3,next_server``: replace **next_server** with the current - server's IP address. - -Save that file and now edit the Cobbler settings file: `/etc/cobbler/settings`. +``domain`` + If applicable, specify your network's domain. + +``dhcp-range`` + Specify the range from wluke haineshich dnsmasq should allocate IP + addresses to servers in your MAAS. + +``dhcp-option=3,next_server`` + Replace ``next_server`` with the current server's IP address. + +Save that file and now edit the Cobbler settings file: +``/etc/cobbler/settings``. You need to change two settings: - * ``manage_dns``: change the 0 to 1 - * ``manage_dhcp``: again, change the 0 to 1. - +``manage_dns`` + Change the 0 to 1 + +``manage_dhcp`` + Again, change the 0 to 1. + Now restart dnsmasq:: - $ sudo /etc/init.d/dnsmasq restart + $ sudo /etc/init.d/dnsmasq restart Import the Ubuntu images ======================== -MAAS will check for and download new Ubuntu images once a week. However, -you'll need to download them manually the first time:: +MAAS will check for and download new Ubuntu images once a +week. However, you'll need to download them manually the first time:: - $ sudo maas-import-isos + $ sudo maas-import-isos Next steps ========== -Your MAAS is now ready for use. Visit the MAAS web interface in your browser -at `http://localhost/MAAS`_. +Your MAAS is now ready for use. Visit the MAAS web interface in your +browser at http://localhost/MAAS. -.. _http://localhost/MAAS: http://localhost/MAAS - Now, :doc:`let's prepare your Juju environment `. diff -Nru maas-0.1+bzr415+dfsg/docs/readme.rst maas-0.1+bzr462+dfsg/docs/readme.rst --- maas-0.1+bzr415+dfsg/docs/readme.rst 2012-04-04 18:59:25.000000000 +0000 +++ maas-0.1+bzr462+dfsg/docs/readme.rst 2012-04-12 20:11:10.000000000 +0000 @@ -1,3 +1,5 @@ +.. -*- mode: rst -*- + **** MAAS **** diff -Nru maas-0.1+bzr415+dfsg/etc/cron.d/maas-gc maas-0.1+bzr462+dfsg/etc/cron.d/maas-gc --- maas-0.1+bzr415+dfsg/etc/cron.d/maas-gc 2012-04-04 18:59:25.000000000 +0000 +++ maas-0.1+bzr462+dfsg/etc/cron.d/maas-gc 2012-04-12 20:11:10.000000000 +0000 @@ -1,2 +1,8 @@ -# Runs 'maas gc' every day at midnight +# Perform daily background cleanups in MAAS. +# +# The "maas gc" command is for garbage-collection, such as deleting uploaded +# files from Juju's file storage API, and in the future commissioning logs, +# that have been superseded by newer ones. (This isn't done immediately +# when the files are overwritten because (1) the transaction that overwrites +# them may fail, and (2) a file may still be in use when it's overwritten.) 0 0 * * * root /usr/sbin/maas gc &> /dev/null diff -Nru maas-0.1+bzr415+dfsg/etc/maas/commissioning-user-data maas-0.1+bzr462+dfsg/etc/maas/commissioning-user-data --- maas-0.1+bzr415+dfsg/etc/maas/commissioning-user-data 1970-01-01 00:00:00.000000000 +0000 +++ maas-0.1+bzr462+dfsg/etc/maas/commissioning-user-data 2012-04-12 20:11:10.000000000 +0000 @@ -0,0 +1,348 @@ +#!/bin/sh +# +# This script carries inside it multiple files. When executed, it creates +# the files into a temporary directory, and then calls the 'main' function +# +# main does a run-parts of all "scripts" and then calls home to maas with +# maas-signal, posting output of each of the files added with add_script() +# +#### script setup ###### +TEMP_D=$(mktemp -d "${TMPDIR:-/tmp}/${0##*/}.XXXXXX") +SCRIPTS_D="${TEMP_D}/scripts" +BIN_D="${TEMP_D}/bin" +OUT_D="${TEMP_D}/out" +PATH="$BIN_D:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" +trap cleanup EXIT + +mkdir -p "$BIN_D" "$OUT_D" "$SCRIPTS_D" + +### some utility functions #### +writefile() { + cat > "$1" + chmod "$2" "$1" +} +add_bin() { + cat > "${BIN_D}/$1" + chmod "${2:-755}" "${BIN_D}/$1" +} +add_script() { + cat > "${SCRIPTS_D}/$1" + chmod "${2:-755}" "${SCRIPTS_D}/$1" +} +cleanup() { + [ -n "${TEMP_D}" ] || rm -Rf "${TEMP_D}" +} + +find_creds_cfg() { + local config="" file="" found="" + + # if the config location is set in environment variable, trust it + [ -n "${COMMISSIONING_CREDENTIALS_URL}" ] && + _RET="${COMMISSIONING_CREDENTIALS_URL}" && return + + # go looking for local files written by cloud-init + for file in /etc/cloud/cloud.cfg.d/*cmdline*.cfg; do + [ -f "$file" ] && _RET="$file" && return + done + + local opt="" cmdline="" + if [ -f /proc/cmdline ] && read cmdline < /proc/cmdline; then + # search through /proc/cmdline arguments + # cloud-config-url trumps url= + for opt in $cmdline; do + case "$opt" in + url=*) + found=${opt#url=};; + cloud-config-url=*) + _RET="${opt#*=}" + return 0;; + esac + done + [ -n "$found" ] && _RET="$found" && return 0 + fi + return 1 +} + +signal() { + maas-signal "--config=${CRED_CFG}" "$@" +} + +fail() { + [ -z "$CRED_CFG" ] || signal FAILED "$1" + echo "FAILED: $1" 1>&2; + exit 1 +} + +main() { + # the main function, actually execute stuff that is written below + local script total=0 creds="" + + find_creds_cfg || + fail "failed to find credential config" + creds="$_RET" + + # get remote credentials into a local file + case "$creds" in + http://*|https://*) + wget "$creds" -O "${TEMP_D}/my.creds" || + fail "failed to get credentials from $cred_cfg" + creds="${TEMP_D}/my.creds" + ;; + esac + + # use global name read by signal() and fail + CRED_CFG="$creds" + + # just get a count of how many scripts there are for progress reporting + for script in "${SCRIPTS_D}/"*; do + [ -x "$script" -a -f "$script" ] || continue + total=$(($total+1)) + done + + local cur=1 numpass=0 name="" failed="" + for script in "${SCRIPTS_D}/"*; do + [ -f "$script" -a -f "$script" ] || continue + name=${script##*/} + signal WORKING "starting ${script##*/} [$cur/$total]" + "$script" > "${OUT_D}/${name}.out" 2> "${OUT_D}/${name}.err" + ret=$? + signal WORKING "finished $name [$cur/$total]: $ret" + if [ $ret -eq 0 ]; then + numpass=$(($numpass+1)) + failed="${failed} ${name}" + fi + cur=$(($cur+1)) + done + + # get a list of all files created, ignoring empty ones + local fargs="" + for file in "${OUT_D}/"*; do + [ -f "$file" -a -s "$file" ] || continue + fargs="$fargs --file=${file##*/}" + done + + if [ $numpass -eq $total ]; then + ( cd "${OUT_D}" && + signal $fargs OK "finished [$numpass/$total]" ) + return 0 + else + ( cd "${OUT_D}" && + signal $fargs OK "failed [$numpass/$total] ($failed)" ) + return $(($count-$numpass)) + fi + +} + +### begin writing files ### +add_script "01-lshw" <<"END_LSHW" +#!/bin/sh +lshw -xml +END_LSHW + +add_bin "maas-signal" <<"END_MAAS_SIGNAL" +#!/usr/bin/python + +import mimetypes +import oauth.oauth as oauth +import os.path +import random +import string +import sys +import time +import urllib2 +import yaml + +MD_VERSION = "2012-03-01" +VALID_STATUS = ("OK", "FAILED", "WORKING") + + +def _encode_field(field_name, data, boundary): + return ('--' + boundary, + 'Content-Disposition: form-data; name="%s"' % field_name, + '', str(data)) + + +def _encode_file(name, fileObj, boundary): + return ('--' + boundary, + 'Content-Disposition: form-data; name="%s"; filename="%s"' % + (name, name), + 'Content-Type: %s' % _get_content_type(name), + '', fileObj.read()) + + +def _random_string(length): + return ''.join(random.choice(string.letters) for ii in range(length + 1)) + + +def _get_content_type(filename): + return mimetypes.guess_type(filename)[0] or 'application/octet-stream' + + +def encode_multipart_data(data, files): + """Create a MIME multipart payload from L{data} and L{files}. + + @param data: A mapping of names (ASCII strings) to data (byte string). + @param files: A mapping of names (ASCII strings) to file objects ready to + be read. + @return: A 2-tuple of C{(body, headers)}, where C{body} is a a byte string + and C{headers} is a dict of headers to add to the enclosing request in + which this payload will travel. + """ + boundary = _random_string(30) + + lines = [] + for name in data: + lines.extend(_encode_field(name, data[name], boundary)) + for name in files: + lines.extend(_encode_file(name, files[name], boundary)) + lines.extend(('--%s--' % boundary, '')) + body = '\r\n'.join(lines) + + headers = {'content-type': 'multipart/form-data; boundary=' + boundary, + 'content-length': str(len(body))} + + return body, headers + + +def oauth_headers(url, consumer_key, token_key, token_secret, consumer_secret): + consumer = oauth.OAuthConsumer(consumer_key, consumer_secret) + token = oauth.OAuthToken(token_key, token_secret) + params = { + 'oauth_version': "1.0", + 'oauth_nonce': oauth.generate_nonce(), + 'oauth_timestamp': int(time.time()), + 'oauth_token': token.key, + 'oauth_consumer_key': consumer.key, + } + req = oauth.OAuthRequest(http_url=url, parameters=params) + req.sign_request(oauth.OAuthSignatureMethod_PLAINTEXT(), + consumer, token) + return(req.to_header()) + + +def geturl(url, creds, headers=None, data=None): + # takes a dict of creds to be passed through to oauth_headers + # so it should have consumer_key, token_key, ... + if headers is None: + headers = {} + else: + headers = dict(headers) + + if creds.get('consumer_key', None) != None: + headers.update(oauth_headers(url, + consumer_key=creds['consumer_key'], token_key=creds['token_key'], + token_secret=creds['token_secret'], + consumer_secret=creds['consumer_secret'])) + req = urllib2.Request(url=url, data=data, headers=headers) + return(urllib2.urlopen(req).read()) + +def read_config(url, creds): + if url.startswith("http://") or url.startswith("https://"): + cfg_str = urllib2.urlopen(urllib2.Request(url=url)) + else: + if url.startswith("file://"): + url = url[7:] + cfg_str = open(url,"r").read() + + cfg = yaml.load(cfg_str) + + # support reading cloud-init config for MAAS datasource + if 'datasource' in cfg: + cfg = cfg['datasource']['MAAS'] + + for key in creds.keys(): + if key in cfg and creds[key] == None: + creds[key] = cfg[key] + +def fail(msg): + sys.stderr.write("FAIL: %s" % msg) + sys.exit(1) + + +def main(): + """ + Call with single argument of directory or http or https url. + If url is given additional arguments are allowed, which will be + interpreted as consumer_key, token_key, token_secret, consumer_secret + """ + import argparse + import pprint + + parser = argparse.ArgumentParser( + description='send signal operation and optionally post files to MAAS') + parser.add_argument("--config", metavar="file", + help="specify config file", default=None) + parser.add_argument("--ckey", metavar="key", + help="the consumer key to auth with", default=None) + parser.add_argument("--tkey", metavar="key", + help="the token key to auth with", default=None) + parser.add_argument("--csec", metavar="secret", + help="the consumer secret (likely '')", default="") + parser.add_argument("--tsec", metavar="secret", + help="the token secret to auth with", default=None) + parser.add_argument("--apiver", metavar="version", + help="the apiver to use ("" can be used)", default=MD_VERSION) + parser.add_argument("--url", metavar="url", + help="the data source to query", default=None) + parser.add_argument("--file", dest='files', + help="file to post", action='append', default=[]) + + parser.add_argument("status", + help="status", choices=VALID_STATUS, action='store') + parser.add_argument("message", help="optional message", + default="", nargs='?') + + args = parser.parse_args() + + creds = {'consumer_key': args.ckey, 'token_key': args.tkey, + 'token_secret': args.tsec, 'consumer_secret': args.csec, + 'metadata_url': args.url} + + if args.config: + read_config(args.config, creds) + + url = creds.get('metadata_url', None) + if not url: + fail("Url must be provided either in --url or in config\n") + url = "%s/%s/" % (url, args.apiver) + + params = { + "op": "signal", + "status": args.status, + "error": args.message} + + files = {} + for fpath in args.files: + files[os.path.basename(fpath)] = open(fpath, "r") + + data, headers = encode_multipart_data(params, files) + + exc = None + msg = "" + + try: + payload = geturl(url, creds=creds, headers=headers, data=data) + if payload != "OK": + raise TypeError("Unexpected result from call: %s" % payload) + else: + msg = "Success" + except urllib2.HTTPError as exc: + msg = "http error [%s]" % exc.code + except urllib2.URLError as exc: + msg = "url error [%s]" % exc.reason + except socket.timeout as exc: + msg = "socket timeout [%s]" % exc + except TypeError as exc: + msg = exc.message + except Exception as exc: + msg = "unexpected error [%s]" % exc + + sys.stderr.write("%s\n" % msg) + sys.exit((exc is None)) + +if __name__ == '__main__': + main() +END_MAAS_SIGNAL + +main +exit diff -Nru maas-0.1+bzr415+dfsg/etc/maas/import_ephemerals maas-0.1+bzr462+dfsg/etc/maas/import_ephemerals --- maas-0.1+bzr415+dfsg/etc/maas/import_ephemerals 1970-01-01 00:00:00.000000000 +0000 +++ maas-0.1+bzr462+dfsg/etc/maas/import_ephemerals 2012-04-12 20:11:10.000000000 +0000 @@ -0,0 +1,17 @@ +## get default settings from maas_import_iso +[ ! -f /etc/maas/maas_import_iso ] || . /etc/maas/maas_import_iso + +#REMOTE_IMAGES_MIRROR="https://cloud-images.ubuntu.com" +#ISCSI_TARGET_IP="" # defaults to cobbler server setting +#EPH_KOPTS_CONSOLE="console=${CONSOLE:-ttyS0,9600n8}" +#EPH_KOPTS_ISCSI="ip=dhcp iscsi_target_name=@@iscsi_target@@ iscsi_target_ip=@@iscsi_target_ip@@ iscsi_target_port=3260" +#EPH_KOPTS_ROOT="root=cloudimg-rootfs ro" +#EPH_KOPTS_LOGGING="log_host=@@server_ip@@ log_port=514" +#EPH_UPDATE_CMD="maas-cloudimg2ephemeral" +#TARGET_NAME_PREFIX="iqn.2004-05.com.ubuntu:maas:" +#DATA_DIR="/var/lib/maas/ephemeral" +#RELEASES="precise" +#ARCHES="amd64 i386" +#KSDIR="/var/lib/cobbler/kickstarts" +#KICKSTART="$KSDIR/maas-commissioning.preseed" +#TARBALL_CACHE_D="" # set to cache downloaded content diff -Nru maas-0.1+bzr415+dfsg/etc/maas/import_isos maas-0.1+bzr462+dfsg/etc/maas/import_isos --- maas-0.1+bzr415+dfsg/etc/maas/import_isos 2012-04-04 18:59:25.000000000 +0000 +++ maas-0.1+bzr462+dfsg/etc/maas/import_isos 2012-04-12 20:11:10.000000000 +0000 @@ -6,6 +6,4 @@ #INTERFACE="eth0" #CONSOLE="ttyS0,9600n8" #KOPTS="priority=$PRIORITY locale=$LOCALE netcfg/choose_interface=$INTERFACE console=$CONSOLE" -## Juju Management Classes -#MGMTCLASS_AVAILABLE="maas-juju-available" -#MGMTCLASS_ACQUIRED="maas-juju-acquired" +#IMPORT_EPHEMERALS=1 diff -Nru maas-0.1+bzr415+dfsg/etc/pserv.yaml maas-0.1+bzr462+dfsg/etc/pserv.yaml --- maas-0.1+bzr415+dfsg/etc/pserv.yaml 2012-04-04 18:59:25.000000000 +0000 +++ maas-0.1+bzr462+dfsg/etc/pserv.yaml 2012-04-12 20:11:10.000000000 +0000 @@ -6,6 +6,18 @@ # # port: 5241 +## The credentials which the Provisioning API will require. +# +# username: +# password: +password: "test" + +## Network interface to bind the service on. +# Keep this pointed at the loopback interface unless you really know what +# you're doing. +# +# interface: "127.0.0.1" + ## Where to log. This log can be rotated by sending SIGUSR1 to the ## running server. # diff -Nru maas-0.1+bzr415+dfsg/HACKING.txt maas-0.1+bzr462+dfsg/HACKING.txt --- maas-0.1+bzr415+dfsg/HACKING.txt 2012-04-04 18:59:25.000000000 +0000 +++ maas-0.1+bzr462+dfsg/HACKING.txt 2012-04-12 20:11:10.000000000 +0000 @@ -1,3 +1,5 @@ +.. -*- mode: rst -*- + ************ Hacking MAAS ************ @@ -219,6 +221,7 @@ .. _South's documentation: http://south.aeracode.org/docs/ + Changing the schema ^^^^^^^^^^^^^^^^^^^ @@ -244,6 +247,7 @@ $ make syncdb + Performing data migration ^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -280,14 +284,18 @@ .. _convention for headings as used in the Python documentation: http://sphinx.pocoo.org/rest.html#sections + Acceptance tests ================ -MAAS uses checkbox manual testing infrastructure to verify features -are implemented according to the spec. They can be found in the official -checkbox repository: -https://code.launchpad.net/~nskaggs/checkbox/checkbox-app-testing-qt -in the jobs/ directory. +MAAS uses `Checkbox`_ to verify features are implemented according to +the spec. They can be found in the `official checkbox repository`_, in +the ``jobs/`` directory. + +.. _Checkbox: https://wiki.ubuntu.com/Testing/Automation/Checkbox + +.. _official checkbox repository: + https://code.launchpad.net/~nskaggs/checkbox/checkbox-app-testing-qt You need to install additional QT dependencies:: @@ -297,6 +305,6 @@ $ bzr branch lp:~nskaggs/checkbox/checkbox-app-testing-qt -To run them, cd into the directory:: +To run them, move into the branch directory and run:: $ bin/checbox-app-testing --whitelist-file= diff -Nru maas-0.1+bzr415+dfsg/INSTALL.txt maas-0.1+bzr462+dfsg/INSTALL.txt --- maas-0.1+bzr415+dfsg/INSTALL.txt 2012-04-04 18:59:25.000000000 +0000 +++ maas-0.1+bzr462+dfsg/INSTALL.txt 2012-04-12 20:11:10.000000000 +0000 @@ -1,12 +1,14 @@ +.. -*- mode: rst -*- + *************** Installing MAAS *************** - + There are two main ways to install MAAS: * as part of a fresh Ubuntu install using the Ubuntu Server installer * or from Ubuntu's archive on an existing Ubuntu install. - + This is a guide to installing MAAS from the Ubuntu archive. It assumes that you're working with: @@ -16,75 +18,85 @@ * control of the network your machine is connected to * internet access or a local mirror of the Ubuntu archive. + Installing MAAS from the archive ================================ -Installing MAAS is straightforward. At the commandline, type:: +Installing MAAS is straightforward. At the command-line, type:: + + $ sudo apt-get install maas - $ sudo apt-get install maas +From a fresh Ubuntu 12.04 LTS install, MAAS will pull down around 200 +MB of packages. -From a fresh Ubuntu 12.04 LTS install, MAAS will pull down around 200 MB of packages. Creating a superuser account ---------------------------- -Once MAAS is installed, you'll need to create your first administrator account. +Once MAAS is installed, you'll need to create your first administrator +account:: + + $ maas createsuperuser -At the commandline, type:: +Follow the prompts and MAAS will create an admin account that you can +later use to log in. - $ maas createsuperuser - -Follow the prompts and MAAS will create an admin account that you can later use to log in. Configuring a DHCP server ========================= -So that MAAS can PXE boot machines, you'll need a DHCP server. MAAS can work -with your existing DHCP set-up but for this guide we'll use dnsmasq. +So that MAAS can PXE boot machines, you'll need a DHCP server. MAAS +can work with your existing DHCP set-up but for this guide we'll use +dnsmasq. -dnsmasq should already be installed. However, if it is not, enter the -following:: +dnsmasq should already be installed; install it if not:: - $ sudo apt-get install dnsmasq + $ sudo apt-get install dnsmasq + +MAAS enlists nodes using a tool called Cobbler. Cobbler provides a +configuration file for dnsmasq: ``/etc/cobbler/dnsmasq.template``. -MAAS enlists nodes using a tool called Cobbler. Cobbler provides a -configuration file for dnsmasq: `/etc/cobbler/dnsmasq.template`. - Make the following changes: - * ``domain``: if applicable, specify your network's domain. - * ``dhcp-range``: specify the range from wluke haineshich dnsmasq should allocate IP - addresses to servers in your MAAS. - * ``dhcp-option=3,next_server``: replace **next_server** with the current - server's IP address. - -Save that file and now edit the Cobbler settings file: `/etc/cobbler/settings`. +``domain`` + If applicable, specify your network's domain. + +``dhcp-range`` + Specify the range from wluke haineshich dnsmasq should allocate IP + addresses to servers in your MAAS. + +``dhcp-option=3,next_server`` + Replace ``next_server`` with the current server's IP address. + +Save that file and now edit the Cobbler settings file: +``/etc/cobbler/settings``. You need to change two settings: - * ``manage_dns``: change the 0 to 1 - * ``manage_dhcp``: again, change the 0 to 1. - +``manage_dns`` + Change the 0 to 1 + +``manage_dhcp`` + Again, change the 0 to 1. + Now restart dnsmasq:: - $ sudo /etc/init.d/dnsmasq restart + $ sudo /etc/init.d/dnsmasq restart Import the Ubuntu images ======================== -MAAS will check for and download new Ubuntu images once a week. However, -you'll need to download them manually the first time:: +MAAS will check for and download new Ubuntu images once a +week. However, you'll need to download them manually the first time:: - $ sudo maas-import-isos + $ sudo maas-import-isos Next steps ========== -Your MAAS is now ready for use. Visit the MAAS web interface in your browser -at `http://localhost/MAAS`_. +Your MAAS is now ready for use. Visit the MAAS web interface in your +browser at http://localhost/MAAS. -.. _http://localhost/MAAS: http://localhost/MAAS - Now, :doc:`let's prepare your Juju environment `. diff -Nru maas-0.1+bzr415+dfsg/README.txt maas-0.1+bzr462+dfsg/README.txt --- maas-0.1+bzr415+dfsg/README.txt 2012-04-04 18:59:25.000000000 +0000 +++ maas-0.1+bzr462+dfsg/README.txt 2012-04-12 20:11:10.000000000 +0000 @@ -1,3 +1,5 @@ +.. -*- mode: rst -*- + **** MAAS **** diff -Nru maas-0.1+bzr415+dfsg/scripts/maas-cloudimg2ephemeral maas-0.1+bzr462+dfsg/scripts/maas-cloudimg2ephemeral --- maas-0.1+bzr415+dfsg/scripts/maas-cloudimg2ephemeral 1970-01-01 00:00:00.000000000 +0000 +++ maas-0.1+bzr462+dfsg/scripts/maas-cloudimg2ephemeral 2012-04-12 20:11:10.000000000 +0000 @@ -0,0 +1,493 @@ +#!/bin/bash +# +# maas-cloudimg2ephemeral - update a cloud image to make it sufficient +# for use as a maas ephemeral image +# +# Copyright (C) 2011-2012 Canonical +# +# Authors: +# Scott Moser +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +VERBOSITY=0 + +error() { echo "$@" 1>&2; } +errorp() { printf "$@" 1>&2; } +fail() { [ $# -eq 0 ] || error "$@"; exit 1; } +failp() { [ $# -eq 0 ] || errorp "$@"; exit 1; } + +Usage() { + cat <&2; [ $# -eq 0 ] || error "$@"; exit 1; } +cleanup() { + [ -z "${TEMP_D}" -o ! -d "${TEMP_D}" ] || { + unmount_under "${TEMP_D}" && + rm -Rf "${TEMP_D}" + } +} + +debug() { + local level=${1}; shift; + [ "${level}" -gt "${VERBOSITY}" ] && return + error "${@}" +} + +unmount_under() { + # unmount_under(dir) + # unmount all mounts under 'dir' + [ -f /proc/mounts ] || + { error "/proc/mounts not a file"; return 1; } + tac /proc/mounts | sh -c ' + under=$1 + while read s mp t opt a b ; do + [ "${mp#${under}}" != "${mp}" ] || continue; + umount $mp || + { echo "failed umount $mp, waiting, trying again" 1>&2; + sleep 10; + umount $mp || exit 1; } + done' -- "$1" +} + +loop_mount() { + # Create more loop nodes, if necessary + local mounts=$(grep -c /dev/loop /proc/mounts) || mounts=0 + local loops=$(ls /dev/loop* | wc -l) || loops=0 + if [ $mounts -ge $loops ]; then + mknod -m 660 /dev/loop$loops b 7 $loops && + chown root:disk /dev/loop$loops || + return 1 + fi + # Do the loop mount + mount -o loop "$1" "$2" +} + +mount_callback_umount() { + # mount_callback_umount(img_or_device, func, args) + # mount the image given, call function with args, + # umount the image, return function's exit value + local device="$1" cb="$2" mp="" opts="" ret=0 m="" + shift 2; + mp=$(mktemp -d "$TEMP_D/mp.XXXXXX") + if [ -b "$device" ]; then + mount $opts "$device" "$mp" || return 1 + else + loop_mount "$device" "$mp" || return 1 + fi + for m in "/proc" "/sys"; do + [ -d "$mp/$m" ] || continue + mount --bind "$m" "$mp/$m" || { + error "failed to mount $mp/$m"; + unmount_under "$mp"; + return 1; + } + done + "$cb" "$mp" "$@" + ret=$? + unmount_under "$mp" && rmdir "$mp" || + { error "WARN! failed to umount $device from $mp"; return 2; } + return $ret +} + +add_initramfs_hooks() { + local dir="$1" idir="" hook="" script="" + idir="$dir/etc/initramfs-tools" + mkdir -p "$idir/hooks" "$idir/scripts/init-bottom" || + return 1 + hook="$idir/hooks/overlay-ro" + cat > "$hook" <<"ENDEND" +#!/bin/sh +set -e + +PREREQS="" +case $1 in + prereqs) echo "${PREREQS}"; exit 0;; +esac + +. /usr/share/initramfs-tools/hook-functions + +## +manual_add_modules overlayfs +force_load overlayfs + +# vi: ts=4 noexpandtab +ENDEND + + [ $? -eq 0 ] || { error "failed to write $hook"; return 1; } + + script="$idir/scripts/init-bottom/root-ro" + cat > "$script" <<"ENDEND" +#!/bin/sh +# Copyright, 2012 Axel Heider +# +# Based on scrpts from +# Sebastian P. +# Nicholas A. Schembri State College PA USA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see +# . +# +# +# Tested with Ubuntu 11.10 +# +# Notes: +# * no changes to the root fs are made by this script. +# * if /home/[user] is on the RO root fs, files are in ram and not saved. +# +# Install: +# put this file in /etc/initramfs-tools/scripts/init-bottom/root-ro +# chmod 0755 root-ro +# optional: clean up menu.lst, update-grub +# update-initramfs -u +# +# Disable read-only root fs +# * option 1: kernel boot parameter "disable-root-ro=true" +# * option 2: create file "/disable-root-ro" +# +# ROOT_RO_DRIVER variable controls which driver isused for the ro/rw layering +# Supported drivers are: overlayfs, aufs +# the kernel parameter "root-ro-driver=[driver]" can be used to initialize +# the variable ROOT_RO_DRIVER. If nothing is given, overlayfs is used. +# + +# no pre requirement +PREREQ="" + +prereqs() +{ + echo "${PREREQ}" +} + +case "$1" in + prereqs) + prereqs + exit 0 + ;; +esac + +. /scripts/functions + +MYTAG="root-ro" +DISABLE_MAGIC_FILE="/disable-root-ro" + +# parse kernel boot command line +ROOT_RO_DRIVER= +DISABLE_ROOT_RO= +for CMD_PARAM in $(cat /proc/cmdline); do + case ${CMD_PARAM} in + disable-root-ro=*) + DISABLE_ROOT_RO=${CMD_PARAM#disable-root-ro=} + ;; + root-ro-driver=*) + ROOT_RO_DRIVER=${CMD_PARAM#root-ro-driver=} + ;; + esac +done + +# check if read-only root fs is disabled +if [ ! -z "${DISABLE_ROOT_RO}" ]; then + log_warning_msg "${MYTAG}: disabled, found boot parameter disable-root-ro=${DISABLE_ROOT_RO}" + exit 0 +fi +if [ -e "${rootmnt}${DISABLE_MAGIC_FILE}" ]; then + log_warning_msg "${MYTAG}: disabled, found file ${rootmnt}${DISABLE_MAGIC_FILE}" + exit 0 +fi + +# generic settings +# ${ROOT} and ${rootmnt} are predefined by caller of this script. Note that +# the root fs ${rootmnt} it mounted readonly on the initrams, which fits nicely +# for our purposes. +ROOT_RW=/mnt/root-rw +ROOT_RO=/mnt/root-ro + +# check if ${ROOT_RO_DRIVER} is defined, otherwise set default +if [ -z "${ROOT_RO_DRIVER}" ]; then + ROOT_RO_DRIVER=overlayfs +fi +# settings based in ${ROOT_RO_DRIVER}, stop here if unsupported. +case ${ROOT_RO_DRIVER} in + overlayfs) + MOUNT_PARMS="-t overlayfs -o lowerdir=${ROOT_RO},upperdir=${ROOT_RW} overlayfs-root ${rootmnt}" + ;; + aufs) + MOUNT_PARMS="-t aufs -o dirs=${ROOT_RW}:${ROOT_RO}=ro aufs-root ${rootmnt}" + ;; + *) + panic "${MYTAG} ERROR: invalide ROOT_RO_DRIVER ${ROOT_RO_DRIVER}" + ;; +esac + + +# check if kernel module exists +modprobe -qb ${ROOT_RO_DRIVER} +if [ $? -ne 0 ]; then + log_failure_msg "${MYTAG} ERROR: missing kernel module ${ROOT_RO_DRIVER}" + exit 0 +fi + +# make the mount point on the init root fs ${ROOT_RW} +[ -d ${ROOT_RW} ] || mkdir -p ${ROOT_RW} +if [ $? -ne 0 ]; then + log_failure_msg "${MYTAG} ERROR: failed to create ${ROOT_RW}" + exit 0 +fi + +# make the mount point on the init root fs ${ROOT_RO} +[ -d ${ROOT_RO} ] || mkdir -p ${ROOT_RO} +if [ $? -ne 0 ]; then + log_failure_msg "${MYTAG} ERROR: failed to create ${ROOT_RO}" + exit 0 +fi + +# mount a tempfs using the device name tmpfs-root at ${ROOT_RW} +mount -t tmpfs tmpfs-root ${ROOT_RW} +if [ $? -ne 0 ]; then + log_failure_msg "${MYTAG} ERROR: failed to create tmpfs" + exit 0 +fi + + +# root is mounted on ${rootmnt}, move it to ${ROOT_RO}. +mount --move ${rootmnt} ${ROOT_RO} +if [ $? -ne 0 ]; then + log_failure_msg "${MYTAG} ERROR: failed to move root away from ${rootmnt} to ${ROOT_RO}" + exit 0 +fi + +# there is nothing left at ${rootmnt} now. So for any error we get we should +# either do recovery to restore ${rootmnt} for drop to a initramfs shell using +# "panic". Otherwise the boot process is very likely to fail with even more +# errors and leave the system in a wired state. + +# mount virtual fs ${rootmnt} with rw-fs ${ROOT_RW} on top or ro-fs ${ROOT_RO}. +mount ${MOUNT_PARMS} +if [ $? -ne 0 ]; then + log_failure_msg "${MYTAG} ERROR: failed to create new ro/rw layerd ${rootmnt}" + # do recovery and try resoring the mount for ${rootmnt} + mount --move ${ROOT_RO} ${rootmnt} + if [ $? -ne 0 ]; then + # thats badm, drpo to s shell to let the user try fixing this + panic "${MYTAG} RECOVERY ERROR: failed to move ${ROOT_RO} back to ${rootmnt}" + fi + exit 0 +fi + +# now the real root fs is on ${ROOT_RO} of the init file system, our layered +# root fs is set up at ${rootmnt}. So we can write anywhere in {rootmnt} and the +# changes will end up in ${ROOT_RW} while ${ROOT_RO} it not touched. However +# ${ROOT_RO} and ${ROOT_RW} are on the initramfs root fs, which will be removed +# an replaced by ${rootmnt}. Thus we must move ${ROOT_RO} and ${ROOT_RW} to the +# rootfs visible later, ie. ${rootmnt}${ROOT_RO} and ${rootmnt}${ROOT_RO}. +# Since the layered ro/rw is already up, these changes also end up on +# ${ROOT_RW} while ${ROOT_RO} is not touched. + +# move mount from ${ROOT_RO} to ${rootmnt}${ROOT_RO} +[ -d ${rootmnt}${ROOT_RO} ] || mkdir -p ${rootmnt}${ROOT_RO} +mount --move ${ROOT_RO} ${rootmnt}${ROOT_RO} +if [ $? -ne 0 ]; then + log_failure_msg "${MYTAG} ERROR: failed to move ${ROOT_RO} to ${rootmnt}${ROOT_RO}" + exit 0 +fi + +# move mount from ${ROOT_RW} to ${rootmnt}${ROOT_RW} +[ -d ${rootmnt}${ROOT_RW} ] || mkdir -p ${rootmnt}${ROOT_RW} +mount --move ${ROOT_RW} ${rootmnt}${ROOT_RW} +if [ $? -ne 0 ]; then + s "${MYTAG}: ERROR: failed to move ${ROOT_RW} to ${rootmnt}${ROOT_RW}" + exit 0 +fi + +# technically, everything is set up nicely now. Since ${rootmnt} had beend +# mounted read-only on the initfamfs already, ${rootmnt}${ROOT_RO} is it, too. +# Now we init process could run - but unfortunately, we may have to prepare +# some more things here. +# Basically, there are two ways to deal with the read-only root fs. If the +# system is made aware of this, things can be simplified a lot. +# If it is not, things need to be done to our best knowledge. +# +# So we assume here, the system does not really know about our read-only root fs. +# +# Let's deal with /etc/fstab first. It usually contains an entry for the root +# fs, which is no longer valid now. We have to remove it and add our new +# ${ROOT_RO} entry. +# Remember we are still on the initramfs root fs here, so we have to work on +# ${rootmnt}/etc/fstab. The original fstab is ${rootmnt}${ROOT_RO}/etc/fstab. +ROOT_TYPE=$(cat /proc/mounts | grep ${ROOT} | cut -d' ' -f3) +ROOT_OPTIONS=$(cat /proc/mounts | grep ${ROOT} | cut -d' ' -f4) +cat <${rootmnt}/etc/fstab +# +# This fstab is in RAM, the real one can be found at ${ROOT_RO}/etc/fstab +# The original entry for '/' and all swap files have been removed. The new +# entry for the read-only the real root fs follows. Write access can be +# enabled using: +# sudo mount -o remount,rw ${ROOT_RO} +# re-mounting it read-only is done using: +# sudo mount -o remount,ro ${ROOT_RO} +# + +${ROOT} ${ROOT_RO} ${ROOT_TYPE} ${ROOT_OPTIONS} 0 0 + +# +# remaining entries from the original ${ROOT_RO}/etc/fstab follow. +# +EOF +if [ $? -ne 0 ]; then + log_failure_msg "${MYTAG} ERROR: failed to modify /etc/fstab (step 1)" + #exit 0 +fi + +#remove root entry and swap from fstab +cat ${rootmnt}${ROOT_RO}/etc/fstab | grep -v ' / ' | grep -v swap >>${rootmnt}/etc/fstab +if [ $? -ne 0 ]; then + log_failure_msg "${MYTAG} ERROR: failed to modify etc/fstab (step 2)" + #exit 0 +fi + +# now we are done. Additinal steps may be necessary depending on the actualy +# distribution and/or its configuration. + +log_success_msg "${MYTAG} sucessfully set up ro/tmpfs-rw layered root fs using ${ROOT_RO_DRIVER}" + +exit 0 +ENDEND + [ $? -eq 0 ] || { error "failed to write $script"; return 1; } + chmod 755 "$hook" "$script" || + { error "failed to chmod $hook, $script"; return 1; } +} + +apply_updates() { + # apply_updates(dir, kernel_out, initramfs_out) + # update directory given, and pull out kernel and initramfs + # to given locations + local dir=$1 kernel_out=$2 initrd_out=$3 + if [ -f "$dir/etc/resolv.conf" ]; then + mv "$dir/etc/resolv.conf" "$dir/etc/resolv.conf.dist" || return 1 + fi + cp "/etc/resolv.conf" "$dir/etc/resolv.conf" || + return 1 + + cat > "$dir/usr/sbin/policy-rc.d" <<"EOF" +#!/bin/sh +while true; do + case "$1" in + -*) shift ;; + makedev) exit 0 ;; + x11-common) exit 0 ;; + *) exit 101 ;; + esac +done +EOF + [ $? -eq 0 ] && chmod 755 "$dir/usr/sbin/policy-rc.d" || + { error "failed to write policy-rc.d"; return 1; } + + add_initramfs_hooks "$dir" || return + + local prox="" apt_opts="" + out=$(apt-config shell prox Acquire::HTTP::Proxy) && + eval $out && [ -n "$prox" ] && + apt_opts="--option=Acquire::HTTP::Proxy=${prox}" + + apt_opts="${apt_opts} --option=Dpkg::Options::=--force-confold" + [ -n "${apt_opts}" ] && + debug 1 "using apt options ${apt_opts} for install" + + LC_ALL=C DEBIAN_FRONTEND=noninteractive \ + apt_opts="${apt_opts}" chroot "$dir" sh -c ' + mkdir -p /etc/iscsi && touch /etc/iscsi/iscsi.initramfs && + apt-get -q ${apt_opts} update && + apt-get remove "linux.*virtual" ${apt_opts} --assume-yes && + apt-get ${apt_opts} install -q -y linux-server open-iscsi || + exit + k="" + for i in /boot/vmlinuz-*; do + [ "${i%-virtual}" = "${i}" ] && k=${i}; done + ver=${k##*/vmlinuz-} + mkinitramfs -o /tmp/initrd.img $ver && + cp $k /tmp/kernel.img && chmod ugo+r /tmp/kernel.img' +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +VERBOSITY=0 +REMOTE_IMAGES_MIRROR="https://cloud-images.ubuntu.com" +CONSOLE="ttyS0,9600n8" +EPH_KOPTS_CONSOLE="console=$CONSOLE" +EPH_KOPTS_ISCSI="ip=dhcp iscsi_target_name=@@iscsi_target@@ iscsi_target_ip=@@iscsi_target_ip@@ iscsi_target_port=3260" +EPH_KOPTS_ROOT="root=LABEL=cloudimg-rootfs ro" +EPH_KOPTS_LOGGING="log_host=@@server_ip@@ log_port=514" +EPH_UPDATE_CMD="maas-cloudimg2ephemeral" +TARGET_NAME_PREFIX="iqn.2004-05.com.ubuntu:maas:" +DATA_DIR="/var/lib/maas/ephemeral" +CONFIG="/etc/maas/import_ephemerals" +RELEASES="precise" +ARCHES="amd64 i386" +BUILD_NAME="server" +STREAM="released" +KSDIR="/var/lib/cobbler/kickstarts" +KICKSTART="$KSDIR/maas-commissioning.preseed" +SYS_TGT_CONF="/etc/tgt/targets.conf" + +# DATA_DIR layout is like: +# tgt.conf +# tgt.conf.d/ +# .conf -> +# ../release/stream/arch/serial.conf +# release/ +# stream/ +# arch/ +# serial/ +# kernel +# disk.img +# initrd +# my.conf + +error() { echo "$@" 1>&2; } +errorp() { printf "$@" 1>&2; } +fail() { [ $# -eq 0 ] || error "$@"; exit 1; } +failp() { [ $# -eq 0 ] || errorp "$@"; exit 1; } + +Usage() { + cat <> + + Import ephemeral (commissioning) images into maas + Settings are read from /etc/maas/maas_import_ephemerals + + options: + -i | --import initial import or freshen the images + -c | --update-check check existing imported data versus available + in mirror. exits 0 if an update is needed or + an initial import is needed. + -u | --update update parameters on cobbler profiles per config +EOF +} + +bad_Usage() { Usage 1>&2; [ $# -eq 0 ] || error "$@"; exit 1; } +cleanup() { + [ -z "${TEMP_D}" -o ! -d "${TEMP_D}" ] || rm -Rf "${TEMP_D}" +} + +debug() { + local level=${1}; shift; + [ "${level}" -gt "${VERBOSITY}" ] && return + error "${@}" +} +arch2u() { + # arch2ubuntu + _RET=$1 + case "$1" in + i?86) _RET=i386;; + x86_64) _RET=amd64;; + esac +} +arch2cob() { + # arch 2 cobbler arch + _RET=$1 + case "$1" in + i?86) _RET=i386;; + amd64) _RET=x86_64;; + esac +} +query_remote() { + # query /query data at REMOTE_IMAGES_MIRROR + # returns 7 values prefixed with 'r_' + local iarch=$1 irelease=$2 istream=$3 out="" + local burl="${REMOTE_IMAGES_MIRROR}/query" + local url="$burl/$irelease/$istream/${STREAM}-dl.current.txt" + mkdir -p "$TEMP_D/query" + local target="$TEMP_D/query/$release.$stream" + if [ ! -f "$TEMP_D/query/$release.$stream" ]; then + wget -q "$url" -O "$target.tmp" && mv "$target.tmp" "$target" || + { error "failed to get $url"; return 1; } + fi + + r_release=""; r_stream=""; r_label=""; r_serial=""; + r_arch=""; r_url=""; r_name="" + + out=$(awk '-F\t' '$1 == release && $2 == stream && $5 == arch { print $3, $4, $6, $7 }' \ + "arch=$iarch" "release=$irelease" "stream=$istream" \ + "$target") && [ -n "$out" ] || + return 1 + + set -- ${out} + r_release=$irelease + r_stream=$istream + r_label=$1; + r_serial=$2; + r_arch=$iarch + r_url=$3 + r_name=$4 + return +} + +query_local() { + local iarch=$1 irelease=$2 istream=$3 out="" + local label="" name="" serial="" url="" + + local found="" + for i in "${DATA_DIR}/"$irelease/$istream/$iarch/*/info; do + [ -f "$i" ] && found=$i + done + found=$(LC_ALL=C; + cd "${DATA_DIR}/$irelease/$istream/$iarch" 2>/dev/null || exit 0; + for d in [0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]*; do + [ -f "$d/info" ] && f=$d; done; + [ -n "$f" ] && echo "$PWD/$f/info") + + l_release=""; l_stream=""; l_label=""; l_serial=""; + l_arch=""; l_url=""; l_name="" + if [ -n "$found" ]; then + . "$found" + l_release="$release"; + l_stream="$stream"; + l_label="$label"; + l_serial="$serial"; + l_arch="$arch"; + l_url="$url"; + l_name="$name"; + l_dir="${found%/*}"; + fi +} +serial_gt() { + # is $1 a larger serial than $2 ? + local a=${1:-0} b=${2:-0} + case "$a" in + *.[0-9]) a="${a%.*}${a##*.}";; + esac + case "$b" in + *.[0-9]) b="${b%.*}${b##*.}";; + esac + [ $a -gt $b ] +} + +prep_dir() { + local wd="$1" exdir="" tarball="" + shift + local release=$1 stream=$2 label=$3 serial=$4 arch=$5 url=$6 name=$7 + local furl="$REMOTE_IMAGES_MIRROR/$url" + + mkdir -p "$wd" + cat > "$wd/info" < "$file" < + readonly 1 + backing-store "$image" + +EOF +} + +cobbler_has() { + local noun="$1" name="$2" out="" + + out=$(cobbler "$noun" find "--name=$name" 2>/dev/null) && + [ "$out" = "$name" ] +} + +cobbler_add_update() { + # cobbler_add_update(distro_name, profile_name, + # release, arch, kopts, kickstart, + # kernel, initrd) + local distro="$1" profile="$2" release="$3" arch="$4" + local kernel="$5" initrd="$6" kopts="$7" kickstart="$8" + local op + + cobbler_has distro "$distro" && op="edit" || op="add" + + cobbler distro "$op" "--name=$distro" --breed=ubuntu \ + "--os-version=$release" "--arch=$arch" \ + "--kernel=$kernel" "--initrd=$initrd" || + { error "failed to $op $distro"; return 1; } + + cobbler_has profile "$profile" && op="edit" || op="add" + + cobbler profile "$op" "--name=$profile" "--distro=$distro" \ + --kopts="$kopts" "--kickstart=$kickstart" || + { error "failed to $op $profile"; return 1; } + + return 0 +} + +replace() { + # replace(input, key1, value1, key2, value2, ...) + local input="$1" key="" val="" + shift + while [ $# -ne 0 ]; do + input=${input//$1/$2} + shift 2 + done + _RET=${input} +} + +short_opts="hciuv" +long_opts="help,import,update,update-check,verbose" +getopt_out=$(getopt --name "${0##*/}" \ + --options "${short_opts}" --long "${long_opts}" -- "$@") && + eval set -- "${getopt_out}" || + bad_Usage + +check=0 +import=0 +update=0 + +while [ $# -ne 0 ]; do + cur=${1}; next=${2}; + case "$cur" in + -h|--help) Usage ; exit 0;; + -v|--verbose) VERBOSITY=$((${VERBOSITY}+1));; + -i|--import) import=1;; + -c|--update-check) check=1;; + -u|--update) update=1;; + --) shift; break;; + esac + shift; +done + +[ $import -eq 0 -a $check -eq 0 -a $update -eq 0 ] && import=1 +[ $(($import + $check + $update)) -eq 0 ] && import=1 + +[ $(($import + $check + $update)) -eq 1 ] || + bad_Usage "only one of --update-check, --update, --import may be given" + +[ ! -f "$CONFIG" ] || . "$CONFIG" +[ ! -f ".${CONFIG}" ] || . ".${CONFIG}" + +# get default server ip +[ -n "$SERVER_IP" ] || + _ip=$(awk '$1 == "server:" { print $2 }' /etc/cobbler/settings) || + fail "must set SERVER_IP to cobbler server" + +SERVER_IP=${SERVER_IP:-${_ip}} +[ -n "${SERVER_IP}" ] && + KOPTS="$KOPTS log_host=$SERVER_IP log_port=514" + +ISCSI_TARGET_IP=${ISCSI_TARGET_IP:-${SERVER_IP}} +[ -n "$ISCSI_TARGET_IP" ] || fail "ISCSI_TARGET_IP must have a value" + +[ -f "$KICKSTART" ] || + fail "kickstart $KICKSTART is not a file" + +mkdir -p "$DATA_DIR" "$DATA_DIR/.working" || + fail "failed to make $DATA_DIR" + +TEMP_D=$(mktemp -d "$DATA_DIR/.working/${0##*/}.XXXXXX") || + fail "failed to make tempdir" +trap cleanup EXIT + +tgt_conf_d="$DATA_DIR/tgt.conf.d" +tgt_conf="${DATA_DIR}/tgt.conf" + +mkdir -p "$tgt_conf_d" || + fail "failed to make directories" +if [ ! -f "${tgt_conf}" ]; then + cat > "${tgt_conf}" < $r_name)" + wd="${TEMP_D}/$release/$arch" + prep_dir "$wd" \ + "$r_release" "$r_stream" "$r_label" \ + "$r_serial" "$r_arch" "$r_url" "$r_name" || + fail "failed to prepare image for $release/$arch" + + target_name="${TARGET_NAME_PREFIX}${r_name}" + + final_d="${r_release}/${r_stream}/${r_arch}/${r_serial}" + fpfinal_d="${DATA_DIR}/${final_d}" + mkdir -p "${fpfinal_d}" + + mv "$wd/"* "${fpfinal_d}/" || + fail "failed to move contents to final directory ${fpfinal_d}" + name="${r_name}" + else + fpfinal_d="${l_dir}" + final_d="${l_release}/${l_stream}/${l_arch}/${l_serial}" + + name="${l_name}" + target_name="${TARGET_NAME_PREFIX}${name}" + debug 1 "updating ${release}-${arch} $final_d" + fi + + rel_tgt="../${final_d}/tgt.conf" + + # iscsi_update + write_tgt_conf "${fpfinal_d}/tgt.conf" "$target_name" \ + "${fpfinal_d}/disk.img" || + fail "failed to write tgt.conf for $release/$arch" + + ln -sf "$rel_tgt" "${tgt_conf_d}/${name}.conf" || + fail "failed to symlink ${name}.conf into place" + + ver_out="${TEMP_D}/verify.${target_name}" + tgt-admin --conf "$SYS_TGT_CONF" --update "${target_name}" && + tgt-admin --conf "$SYS_TGT_CONF" --show > "${ver_out}" && + grep -q "^Target [0-9][0-9]*: ${target_name}" "${ver_out}" || { + mv "${fpfinal_d}/info" "${fpfinal_d}/info.failed" + tgt-admin --conf "$SYS_TGT_CONF" --delete "$target_name" + rm "${tgt_conf_d}/${name}.conf" + fail "failed tgt-admin add for $name" + } + + # cobbler_update + kopts_in="$EPH_CONSOLE_KOPTS $EPH_KOPTS_ISCSI $EPH_KOPTS_ROOT $EPH_KOPTS_LOGGING" + replace "${kopts_in}" \ + "@@server_ip@@" "$SERVER_IP" \ + "@@iscsi_target@@" "${target_name}" \ + "@@iscsi_target_ip@@" "${ISCSI_TARGET_IP}" + kopts=$_RET + + distro="$release-${arch_c}-maas-ephemeral" + profile="maas-${release}-${arch_c}-commissioning" + kernel="$fpfinal_d/kernel" + initrd="$fpfinal_d/initrd" + debug 1 "updating profile $profile, distro $distro kopts:${kopts}" + debug 2 cobbler_add_update "$distro" "$profile" "$release" "${arch_c}" \ + "$kernel" "$initrd" "$kopts" "$KICKSTART" + cobbler_add_update "$distro" "$profile" "$release" "${arch_c}" \ + "$kernel" "$initrd" "$kopts" "$KICKSTART" || { + mv "${fpfinal_d}/info" "${fpfinal_d}/info.failed" + tgt-admin --conf "$SYS_TGT_CONF" --delete "$target_name" + rm "${tgt_conf_d}/${name}.conf"; + fail "failed to update cobbler for $profile/$distro" + } + done +done + +if [ $check -eq 1 ]; then + # if --update-check, but no updates needed, exit 3 + [ $updates_needed -eq 0 ] && exit 3 + # if updates are needed, exit 0 + exit 0 +fi + +cobbler sync + +## cleanup +# here, go through anything non-current, +# * remove the tgt config +# * if tgt-show has entry: +# * remove from tgt-admin by name && remove directories +# * else +# * remove directory + +# vi: ts=4 noexpandtab diff -Nru maas-0.1+bzr415+dfsg/scripts/maas-import-isos maas-0.1+bzr462+dfsg/scripts/maas-import-isos --- maas-0.1+bzr415+dfsg/scripts/maas-import-isos 2012-04-04 18:59:25.000000000 +0000 +++ maas-0.1+bzr462+dfsg/scripts/maas-import-isos 2012-04-12 20:11:10.000000000 +0000 @@ -28,43 +28,18 @@ exit 1 fi -distro_info() { - # we would like to use distro-info, but cannot (LP: #960142) - # if it is there, we will use it. if not, we have a behave-alike - # that will need SRUs to get new release knowledge - if command -v distro-info >/dev/null; then - distro-info "$@" - return - fi - - local supported="hardy lucid maverick natty oneiric precise" - local devel="precise" - local stable="oneiric" - [ "$(date +%Y%m%d)" -lt 20120426 ] || stable="precise" - - case "$1" in - --devel) echo "$devel";; - --stable) echo "$stable";; - --supported) - local d; - for d in $supported; do echo "$d"; done - ;; - *) echo "unknown argument $1"; return 1;; - esac -} - # Definitions for supported releases and architectures [ -r $(pwd)/etc/maas/import_isos ] && . $(pwd)/etc/maas/import_isos [ -r /etc/maas/import_isos ] && . /etc/maas/import_isos -[ -n "$RELEASES" ] || RELEASES=$(distro_info --supported) +[ -n "$RELEASES" ] || RELEASES=$(distro-info --supported) [ -n "$ARCHES" ] || ARCHES="amd64 i386" [ -n "$KSDIR" ] || KSDIR="/var/lib/cobbler/kickstarts" [ -n "$PRIORITY" ] || PRIORITY="critical" [ -n "$LOCALE" ] || LOCALE="en_US" [ -n "$INTERFACE" ] || INTERFACE="auto" -[ -n "$CONSOLE" ] || CONSOLE="ttyS0,9600n8" -[ -n "$KOPTS" ] || KOPTS="priority=$PRIORITY locale=$LOCALE netcfg/choose_interface=$INTERFACE console=$CONSOLE" +[ -n "$KOPTS" ] || KOPTS="priority=$PRIORITY locale=$LOCALE netcfg/choose_interface=$INTERFACE" [ -n "$ENLIST_PROFILE" ] || ENLIST_PROFILE="maas-enlist" +[ -n "$IMPORT_EPHEMERALS" ] || IMPORT_EPHEMERALS=1 if [ -n "$SERVER_IP" ]; then KOPTS="$KOPTS log_host=$SERVER_IP log_port=514" else @@ -73,6 +48,9 @@ KOPTS="$KOPTS log_host=$SERVER_IP log_port=514" fi fi +if [ -n "$CONSOLE" ]; then + KOPTS="$KOPTS console=$CONSOLE" +fi name_arch_in_cobbler_style() { @@ -93,6 +71,8 @@ fi done done + + [ "$IMPORT_EPHEMERALS" = "0" ] || maas-import-ephemerals --update } import_isos(){ @@ -118,11 +98,13 @@ fi done done + + [ "${IMPORT_EPHEMERALS}" = "0" ] || maas-import-ephemerals --import } add_enlist_profile(){ - STABLE=$(distro_info --stable) - DEVEL=$(distro_info --devel) + STABLE=$(distro-info --stable) + DEVEL=$(distro-info --devel) PARENT_PROFILE="" # Check what release to use as parent profile diff -Nru maas-0.1+bzr415+dfsg/src/maas/settings.py maas-0.1+bzr462+dfsg/src/maas/settings.py --- maas-0.1+bzr415+dfsg/src/maas/settings.py 2012-04-04 18:59:25.000000000 +0000 +++ maas-0.1+bzr462+dfsg/src/maas/settings.py 2012-04-12 20:11:10.000000000 +0000 @@ -10,6 +10,7 @@ __metaclass__ = type +from getpass import getuser import os from urlparse import urljoin @@ -254,12 +255,20 @@ # The location of the Provisioning API XML-RPC endpoint. This should # match the setting in etc/pserv.yaml. -PSERV_URL = "http://localhost:5241/api" +PSERV_URL = "http://%s:test@localhost:5241/api" % getuser() + +# Time-out for socket operations against the Provisioning API. +PSERV_TIMEOUT = 7.0 # seconds. # Use a real provisioning server? If yes, the URL for the provisioning # server's API should be set in PSERV_URL. If this is set to False, for # testing or demo purposes, MAAS will use an internal fake service. USE_REAL_PSERV = True +# The location of the commissioning script that is executed on nodes as +# part of commissioning. Only override this if you know what you are +# doing. +COMMISSIONING_SCRIPT = 'etc/maas/commissioning-user-data' + # Allow the user to override settings in maas_local_settings. import_local_settings() diff -Nru maas-0.1+bzr415+dfsg/src/maasserver/api.py maas-0.1+bzr462+dfsg/src/maasserver/api.py --- maas-0.1+bzr415+dfsg/src/maasserver/api.py 2012-04-04 18:59:25.000000000 +0000 +++ maas-0.1+bzr462+dfsg/src/maasserver/api.py 2012-04-12 20:11:10.000000000 +0000 @@ -408,23 +408,21 @@ The node will be in the Declared state. :param request: The http request for this node to be created. - :return: A suitable return value for the handler receiving the "new" - request that this implements. - :rtype: :class:`maasserver.models.Node` or - :class:`django.http.HttpResponseBadRequest`. + :return: A `Node`. + :rtype: :class:`maasserver.models.Node`. + :raises: ValidationError """ form = NodeWithMACAddressesForm(request.data) if form.is_valid(): return form.save() else: - return HttpResponseBadRequest( - form.errors, content_type='application/json') + raise ValidationError(form.errors) @api_operations class AnonNodesHandler(AnonymousBaseHandler): """Create Nodes.""" - allowed_methods = ('POST',) + allowed_methods = ('GET', 'POST',) fields = NODE_FIELDS @api_exported('new', 'POST') @@ -438,6 +436,21 @@ """ return create_node(request) + @api_exported('is_registered', 'GET') + def is_registered(self, request): + """Returns whether or not the given MAC Address is registered within + this MAAS (and attached to a non-retired node). + + :param mac_address: The mac address to be checked. + :type mac_address: basestring + :return: 'true' or 'false'. + :rtype: basestring + """ + mac_address = get_mandatory_param(request.GET, 'mac_address') + return MACAddress.objects.filter( + mac_address=mac_address).exclude( + node__status=NODE_STATUS.RETIRED).exists() + @api_exported('accept', 'POST') def accept(self, request): """Accept a node's enlistment: not allowed to anonymous users.""" @@ -478,7 +491,7 @@ """ node = create_node(request) if request.user.is_superuser: - node.accept_enlistment() + node.accept_enlistment(request.user) return node @api_exported('accept', 'POST') @@ -516,7 +529,8 @@ "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]) + return filter( + None, [node.accept_enlistment(request.user) for node in nodes]) @api_exported('list', 'GET') def list(self, request): diff -Nru maas-0.1+bzr415+dfsg/src/maasserver/components.py maas-0.1+bzr462+dfsg/src/maasserver/components.py --- maas-0.1+bzr415+dfsg/src/maasserver/components.py 1970-01-01 00:00:00.000000000 +0000 +++ maas-0.1+bzr462+dfsg/src/maasserver/components.py 2012-04-12 20:11:10.000000000 +0000 @@ -0,0 +1,48 @@ +# Copyright 2012 Canonical Ltd. This software is licensed under the +# GNU Affero General Public License version 3 (see the file LICENSE). + +"""MAAS components management.""" + +from __future__ import ( + print_function, + unicode_literals, + ) + +__metaclass__ = type +__all__ = [ + "discard_persistent_error", + "get_persistent_errors", + "register_persistent_error", + ] + +import threading + + +class COMPONENT: + COBBLER = 'cobbler server' + PSERV = 'provisioning server' + IMPORT_ISOS = 'maas-import-isos script' + + +# Persistent errors are global to a MAAS instance. +# This is a mapping: component -> error message. +_PERSISTENT_ERRORS = {} + + +_PERSISTENT_ERRORS_LOCK = threading.Lock() + + +def register_persistent_error(component, error_message): + with _PERSISTENT_ERRORS_LOCK: + global _PERSISTENT_ERRORS + _PERSISTENT_ERRORS[component] = error_message + + +def discard_persistent_error(component): + with _PERSISTENT_ERRORS_LOCK: + global _PERSISTENT_ERRORS + _PERSISTENT_ERRORS.pop(component, None) + + +def get_persistent_errors(): + return _PERSISTENT_ERRORS.values() diff -Nru maas-0.1+bzr415+dfsg/src/maasserver/context_processors.py maas-0.1+bzr462+dfsg/src/maasserver/context_processors.py --- maas-0.1+bzr415+dfsg/src/maasserver/context_processors.py 2012-04-04 18:59:25.000000000 +0000 +++ maas-0.1+bzr462+dfsg/src/maasserver/context_processors.py 2012-04-12 20:11:10.000000000 +0000 @@ -14,6 +14,7 @@ ] from django.conf import settings +from maasserver.components import get_persistent_errors from maasserver.forms import NodeForm from maasserver.models import Config @@ -29,6 +30,7 @@ def global_options(context): return { + 'persistent_errors': get_persistent_errors(), 'node_form': NodeForm(), 'global_options': { 'site_name': Config.objects.get_config('maas_name'), diff -Nru maas-0.1+bzr415+dfsg/src/maasserver/forms.py maas-0.1+bzr462+dfsg/src/maasserver/forms.py --- maas-0.1+bzr415+dfsg/src/maasserver/forms.py 2012-04-04 18:59:25.000000000 +0000 +++ maas-0.1+bzr462+dfsg/src/maasserver/forms.py 2012-04-12 20:11:10.000000000 +0000 @@ -24,6 +24,7 @@ ] from django import forms +from django.contrib import messages from django.contrib.auth.forms import ( UserChangeForm, UserCreationForm, @@ -49,7 +50,6 @@ Config, MACAddress, Node, - NODE_AFTER_COMMISSIONING_ACTION, NODE_AFTER_COMMISSIONING_ACTION_CHOICES, NODE_PERMISSION, NODE_STATUS, @@ -82,9 +82,14 @@ system_id = forms.CharField( widget=forms.TextInput(attrs={'readonly': 'readonly'}), required=False) - after_commissioning_action = forms.TypedChoiceField( - choices=NODE_AFTER_COMMISSIONING_ACTION_CHOICES, required=False, - empty_value=NODE_AFTER_COMMISSIONING_ACTION.DEFAULT) + + # XXX JeroenVermeulen 2012-04-12, bug=979539: re-enable. + + #after_commissioning_action = forms.TypedChoiceField( + # label="After commissioning", + # choices=NODE_AFTER_COMMISSIONING_ACTION_CHOICES, required=False, + # empty_value=NODE_AFTER_COMMISSIONING_ACTION.DEFAULT) + architecture = forms.ChoiceField( choices=ARCHITECTURE_CHOICES, required=True, initial=ARCHITECTURE.i386, @@ -93,27 +98,48 @@ class Meta: model = Node fields = ( - 'hostname', 'system_id', 'after_commissioning_action', - 'architecture', 'power_type') + 'hostname', + 'system_id', + # XXX JeroenVermeulen 2012-04-12, bug=979539: re-enable. + #'after_commissioning_action', + 'architecture', + 'power_type', + ) class UINodeEditForm(ModelForm): - after_commissioning_action = forms.ChoiceField( - choices=NODE_AFTER_COMMISSIONING_ACTION_CHOICES) + + # XXX JeroenVermeulen 2012-04-12, bug=979539: re-enable. + + #after_commissioning_action = forms.ChoiceField( + # label="After commissioning", + # choices=NODE_AFTER_COMMISSIONING_ACTION_CHOICES) class Meta: model = Node - fields = ('hostname', 'after_commissioning_action') + fields = ( + 'hostname', + # XXX JeroenVermeulen 2012-04-12, bug=979539: re-enable. + #'after_commissioning_action', + ) class UIAdminNodeEditForm(ModelForm): - after_commissioning_action = forms.ChoiceField( - choices=NODE_AFTER_COMMISSIONING_ACTION_CHOICES) + + # XXX JeroenVermeulen 2012-04-12, bug=979539: re-enable. + + #after_commissioning_action = forms.ChoiceField( + # label="After commissioning", + # choices=NODE_AFTER_COMMISSIONING_ACTION_CHOICES) class Meta: model = Node fields = ( - 'hostname', 'after_commissioning_action', 'power_type') + 'hostname', + # XXX JeroenVermeulen 2012-04-12, bug=979539: re-enable. + #'after_commissioning_action', + 'power_type', + ) class MACAddressForm(ModelForm): @@ -134,10 +160,31 @@ super(SSHKeyForm, self).__init__(*args, **kwargs) self.user = user - def save(self): - key = super(SSHKeyForm, self).save(commit=False) - key.user = self.user - return key.save() + def validate_unique(self): + # This is a trick to work around a problem in Django. + # See https://code.djangoproject.com/ticket/13091#comment:19 for + # details. + # Without this overridden validate_unique the validation error that + # can occur if this user already has the same key registered would + # occur when save() would be called. The error would be an + # IntegrityError raised when inserting the new key in the database + # rather than a proper ValidationError raised by 'clean'. + + # Set the instance user. + self.instance.user = self.user + + # Allow checking against the missing attribute. + exclude = self._get_validation_exclusions() + exclude.remove('user') + try: + self.instance.validate_unique(exclude=exclude) + except ValidationError, e: + # Publish this error as a 'key' error rather than a 'general' + # error because only the 'key' errors are displayed on the + # 'add key' form. + error = e.message_dict.pop('__all__') + e.message_dict['key'] = error + self._update_errors(e.message_dict) class MultipleMACAddressField(forms.MultiValueField): @@ -193,9 +240,6 @@ 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) @@ -228,14 +272,10 @@ NODE_ACTIONS = { NODE_STATUS.DECLARED: [ { - 'display': "Accept Enlisted node", - 'permission': NODE_PERMISSION.ADMIN, - 'execute': lambda node, user: Node.accept_enlistment(node), - }, - { - 'display': "Commission node", + 'display': "Accept & commission", 'permission': NODE_PERMISSION.ADMIN, 'execute': lambda node, user: Node.start_commissioning(node, user), + 'message': "Node commissioning started." }, ], NODE_STATUS.READY: [ @@ -243,6 +283,7 @@ 'display': "Start node", 'permission': NODE_PERMISSION.EDIT, 'execute': start_node, + 'message': "Node started." }, ], NODE_STATUS.ALLOCATED: [ @@ -250,6 +291,7 @@ 'display': "Start node", 'permission': NODE_PERMISSION.EDIT, 'execute': start_node, + 'message': "Node started." }, ], } @@ -263,6 +305,7 @@ """ user = AnonymousUser() + request = None # The name of the input button used with this form. input_name = 'node_action' @@ -275,7 +318,8 @@ # Create a convenient dict to fetch the action's name and # the permission to be checked from the button name. self.action_dict = { - action['display']: (action['permission'], action['execute']) + action['display']: ( + action['permission'], action['execute'], action['message']) for action in self.action_buttons } @@ -294,28 +338,37 @@ action for action in NODE_ACTIONS.get(node.status, ()) if user.has_perm(action['permission'], node)] + def display_message(self, message): + if self.request is not None: + messages.add_message(self.request, messages.INFO, message) + def save(self): action_name = self.data.get(self.input_name) - permission, execute = self.action_dict.get(action_name, (None, None)) + permission, execute, message = ( + self.action_dict.get(action_name, (None, None, None))) if execute is not None: if not self.user.has_perm(permission, self.node): raise PermissionDenied() execute(self.node, self.user) + self.display_message(message) else: raise PermissionDenied() -def get_action_form(user): +def get_action_form(user, request=None): """Return a class derived from NodeActionForm for a specific user. :param user: The user for which to build a form derived from NodeActionForm. :type user: :class:`django.contrib.auth.models.User` + :param request: An optional request object to publish action messages. + :type request: django.http.HttpRequest :return: A form class derived from NodeActionForm. :rtype: class:`django.forms.Form` """ return type( - str("SpecificNodeActionForm"), (NodeActionForm,), {'user': user}) + str("SpecificNodeActionForm"), (NodeActionForm,), + {'user': user, 'request': request}) class ProfileForm(ModelForm): diff -Nru maas-0.1+bzr415+dfsg/src/maasserver/migrations/0005_sshkey_user_and_key_unique_together.py maas-0.1+bzr462+dfsg/src/maasserver/migrations/0005_sshkey_user_and_key_unique_together.py --- maas-0.1+bzr415+dfsg/src/maasserver/migrations/0005_sshkey_user_and_key_unique_together.py 1970-01-01 00:00:00.000000000 +0000 +++ maas-0.1+bzr462+dfsg/src/maasserver/migrations/0005_sshkey_user_and_key_unique_together.py 2012-04-12 20:11:10.000000000 +0000 @@ -0,0 +1,138 @@ +# flake8: noqa +# SKIP this file when reformatting. +# The rest of this file was generated by South. + +# encoding: utf-8 +import datetime + +from django.db import models +from south.db import db +from south.v2 import SchemaMigration + + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding unique constraint on 'SSHKey', fields ['user', 'key'] + db.create_unique('maasserver_sshkey', ['user_id', 'key']) + + + def backwards(self, orm): + + # Removing unique constraint on 'SSHKey', fields ['user', 'key'] + db.delete_unique('maasserver_sshkey', ['user_id', 'key']) + + + 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-692e05e6-7fc4-11e1-a47c-00219bd0a2de'", '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': {'unique_together': "((u'user', u'key'),)", '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': '1333701735L'}), + '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+bzr415+dfsg/src/maasserver/models.py maas-0.1+bzr462+dfsg/src/maasserver/models.py --- maas-0.1+bzr415+dfsg/src/maasserver/models.py 2012-04-04 18:59:25.000000000 +0000 +++ maas-0.1+bzr462+dfsg/src/maasserver/models.py 2012-04-12 20:11:10.000000000 +0000 @@ -90,8 +90,9 @@ class CommonInfo(models.Model): - """A base model which records the creation date and the last modification - date. + """A base model which: + - calls full_clean before saving the model (by default). + - records the creation date and the last modification date. :ivar created: The creation date. :ivar updated: The last modification date. @@ -103,11 +104,13 @@ class Meta: abstract = True - def save(self, *args, **kwargs): + def save(self, skip_check=False, *args, **kwargs): if not self.id: self.created = datetime.date.today() self.updated = datetime.datetime.today() - super(CommonInfo, self).save(*args, **kwargs) + if not skip_check: + self.full_clean() + return super(CommonInfo, self).save(*args, **kwargs) def generate_node_system_id(): @@ -428,16 +431,10 @@ # TODO: File structure needs sorting out to avoid this circular # import dance. from metadataserver.models import NodeUserData - 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]) + get_papi().start_nodes([node.system_id for node in nodes]) return nodes @@ -542,11 +539,6 @@ super(Node, self).clean(*args, **kwargs) self.clean_status() - def save(self, skip_check=False, *args, **kwargs): - if not skip_check: - self.full_clean() - return super(Node, self).save(*args, **kwargs) - def display_status(self): """Return status text as displayed to the user. @@ -576,7 +568,6 @@ """ mac = MACAddress(mac_address=mac_address, node=self) - mac.full_clean() mac.save() return mac @@ -591,7 +582,7 @@ if mac: mac.delete() - def accept_enlistment(self): + def accept_enlistment(self, user): """Accept this node's (anonymous) enlistment. This call makes sense only on a node in Declared state, i.e. one that @@ -603,10 +594,6 @@ :return: This node if it has made the transition from Declared, or None if it was already in an accepted state. """ - # Accepting a node puts it into Ready state. This will change - # once we implement commissioning. - target_state = NODE_STATUS.READY - accepted_states = [NODE_STATUS.READY, NODE_STATUS.COMMISSIONING] if self.status in accepted_states: return None @@ -615,18 +602,28 @@ "Cannot accept node enlistment: node %s is in state %s." % (self.system_id, NODE_STATUS_CHOICES_DICT[self.status])) - self.status = target_state - self.save() + self.start_commissioning(user) return self def start_commissioning(self, user): """Install OS and self-test a new node.""" + # Avoid circular imports. + from metadataserver.models import NodeCommissionResult + + path = settings.COMMISSIONING_SCRIPT + if not os.path.exists(path): + raise ValidationError( + "Commissioning script is missing: %s" % path) + with open(path, 'r') as f: + commissioning_user_data = f.read() + + NodeCommissionResult.objects.clear_results(self) 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) + [self.system_id], user, user_data=commissioning_user_data) def delete(self): # Delete the related mac addresses first. @@ -924,9 +921,6 @@ :ivar user: The user which owns the key. :ivar key: The ssh public key. """ - class Meta: - verbose_name_plural = "SSH keys" - objects = SSHKeyManager() user = models.ForeignKey(User, null=False, editable=False) @@ -934,6 +928,16 @@ key = models.TextField( null=False, editable=True, validators=[validate_ssh_public_key]) + class Meta: + verbose_name_plural = "SSH keys" + unique_together = ('user', 'key') + + def unique_error_message(self, model_class, unique_check): + if unique_check == ('user', 'key'): + return "This key has already been added for this user." + return super( + SSHKey, self).unique_error_message(model_class, unique_check) + def __unicode__(self): return self.key diff -Nru maas-0.1+bzr415+dfsg/src/maasserver/provisioning.py maas-0.1+bzr462+dfsg/src/maasserver/provisioning.py --- maas-0.1+bzr415+dfsg/src/maasserver/provisioning.py 2012-04-04 18:59:25.000000000 +0000 +++ maas-0.1+bzr462+dfsg/src/maasserver/provisioning.py 2012-04-12 20:11:10.000000000 +0000 @@ -11,9 +11,11 @@ __metaclass__ = type __all__ = [ 'get_provisioning_api_proxy', + 'present_detailed_user_friendly_fault', 'ProvisioningProxy', ] +from functools import partial from logging import getLogger from textwrap import dedent from urllib import urlencode @@ -26,6 +28,11 @@ post_save, ) from django.dispatch import receiver +from maasserver.components import ( + COMPONENT, + discard_persistent_error, + register_persistent_error, + ) from maasserver.exceptions import MAASAPIException from maasserver.models import ( Config, @@ -34,9 +41,11 @@ NODE_STATUS, ) from provisioningserver.enum import PSERV_FAULT +import yaml -# Presentation templates for various provisioning faults. -PRESENTATIONS = { +# Presentation templates for various provisioning faults (will be used +# for long-lasting warnings about failing components). +DETAILED_PRESENTATIONS = { PSERV_FAULT.NO_COBBLER: """ The provisioning server was unable to reach the Cobbler service: %(fault_string)s @@ -69,6 +78,13 @@ If the error message is not clear, you may need to check the Cobbler logs in /var/log/cobbler/ or pserv.log. """, + PSERV_FAULT.COBBLER_DNS_LOOKUP_ERROR: """ + The provisioning server was unable to resolve the Cobbler server's + DNS address: %(fault_string)s. + + Has Cobbler been properly installed and is it accessible by the + provisioning server? Check /var/log/cobbler/ and pserv.log. + """, 8002: """ Unable to reach provisioning server (%(fault_string)s). @@ -77,12 +93,41 @@ """, } +# Shorter presentation templates for various provisioning faults (will +# be used for one-off messages). +SHORT_PRESENTATIONS = { + PSERV_FAULT.NO_COBBLER: """ + Unable to reach the Cobbler server. + """, + PSERV_FAULT.COBBLER_AUTH_FAILED: """ + Failed to authenticate with the Cobbler server. + """, + PSERV_FAULT.COBBLER_AUTH_ERROR: """ + Failed to authenticate with the Cobbler server. + """, + PSERV_FAULT.NO_SUCH_PROFILE: """ + Missing system profile: %(fault_string)s. + """, + PSERV_FAULT.GENERIC_COBBLER_ERROR: """ + Unknown problem encountered with the Cobbler server. + """, + PSERV_FAULT.COBBLER_DNS_LOOKUP_ERROR: """ + Unable to resolve the Cobbler server's DNS address: + %(fault_string)s. + """, + 8002: """ + Unable to reach provisioning server. + """, +} + -def present_user_friendly_fault(fault): +def _present_user_friendly_fault(fault, presentations): """Return a more user-friendly exception to represent `fault`. :param fault: An exception raised by, or received across, xmlrpc. :type fault: :class:`xmlrpclib.Fault` + :param presentations: A mapping error -> message. + :type fault: dict :return: A more user-friendly exception, if one can be produced. Otherwise, this returns None and the original exception should be re-raised. (This is left to the caller in order to minimize @@ -93,7 +138,7 @@ 'fault_code': fault.faultCode, 'fault_string': fault.faultString, } - user_friendly_text = PRESENTATIONS.get(fault.faultCode) + user_friendly_text = presentations.get(fault.faultCode) if user_friendly_text is None: return None else: @@ -101,25 +146,110 @@ user_friendly_text.lstrip('\n') % params)) +present_user_friendly_fault = partial( + _present_user_friendly_fault, presentations=SHORT_PRESENTATIONS) +"""Return a concise but user-friendly exception to represent `fault`. + +:param fault: An exception raised by, or received across, xmlrpc. +:type fault: :class:`xmlrpclib.Fault` +:return: A more user-friendly exception, if one can be produced. + Otherwise, this returns None and the original exception should be + re-raised. (This is left to the caller in order to minimize + erosion of the backtrace). +:rtype: :class:`MAASAPIException`, or None. +""" + + +present_detailed_user_friendly_fault = partial( + _present_user_friendly_fault, presentations=DETAILED_PRESENTATIONS) +"""Return a detailed and user-friendly exception to represent `fault`. + +:param fault: An exception raised by, or received across, xmlrpc. +:type fault: :class:`xmlrpclib.Fault` +:return: A more user-friendly exception, if one can be produced. + Otherwise, this returns None and the original exception should be + re-raised. (This is left to the caller in order to minimize + erosion of the backtrace). +:rtype: :class:`MAASAPIException`, or None. +""" + +# A mapping method_name -> list of components. +# For each method name, indicate the list of components that the method +# uses. This way, when calling the method is a success, if means that +# the related components are working properly. +METHOD_COMPONENTS = { + 'add_node': [COMPONENT.PSERV, COMPONENT.COBBLER, COMPONENT.IMPORT_ISOS], + 'modify_nodes': [COMPONENT.PSERV, COMPONENT.COBBLER], + 'delete_nodes_by_name': [COMPONENT.PSERV, COMPONENT.COBBLER], +} + +# A mapping exception -> component. +# For each exception in this dict, the related component is there to +# tell us which component will be marked as 'failing' when this +# exception is raised. +EXCEPTIONS_COMPONENTS = { + PSERV_FAULT.NO_COBBLER: COMPONENT.COBBLER, + PSERV_FAULT.COBBLER_AUTH_FAILED: COMPONENT.COBBLER, + PSERV_FAULT.COBBLER_AUTH_ERROR: COMPONENT.COBBLER, + PSERV_FAULT.NO_SUCH_PROFILE: COMPONENT.IMPORT_ISOS, + PSERV_FAULT.GENERIC_COBBLER_ERROR: COMPONENT.COBBLER, + PSERV_FAULT.COBBLER_DNS_LOOKUP_ERROR: COMPONENT.COBBLER, + 8002: COMPONENT.PSERV, +} + + +def register_working_components(method_name): + """Register that the components related to the provided method + (if any) are working. + """ + components = METHOD_COMPONENTS.get(method_name, []) + for component in components: + discard_persistent_error(component) + + +def register_failing_component(exception): + """Register that the component corresponding to exception (if any) + is failing. + """ + component = EXCEPTIONS_COMPONENTS.get(exception.faultCode, None) + if component is not None: + detailed_friendly_fault = unicode( + present_detailed_user_friendly_fault(exception)) + register_persistent_error(component, detailed_friendly_fault) + + class ProvisioningCaller: """Wrapper for an XMLRPC call. - Runs xmlrpc exceptions through `present_user_friendly_fault` for better + - Runs xmlrpc exceptions through `present_user_friendly_fault` for better presentation to the user. + - Registers failing/working components. """ - def __init__(self, method): + def __init__(self, method_name, method): + # Keep track of the method name; xmlrpclib does not take lightly + # to us attempting to look it up as an attribute of the method + # object. + self.method_name = method_name self.method = method - def __call__(self, *args, **kwargs): + def __call__(self, *args): try: - return self.method(*args, **kwargs) + result = self.method(*args) except xmlrpclib.Fault as e: + # Register failing component. + register_failing_component(e) + # Raise a more user-friendly error. friendly_fault = present_user_friendly_fault(e) if friendly_fault is None: raise else: raise friendly_fault + else: + # The call was a success, discard persistent errors for + # components referenced by this method. + register_working_components(self.method_name) + return result class ProvisioningProxy: @@ -133,18 +263,32 @@ def __init__(self, xmlrpc_proxy): self.proxy = xmlrpc_proxy - def patch(self, method, replacement): - setattr(self.proxy, method, replacement) - def __getattr__(self, attribute_name): """Return a wrapped version of the requested method.""" attribute = getattr(self.proxy, attribute_name) - if getattr(attribute, '__call__', None) is None: + if callable(attribute): + # This attribute is callable. Wrap it in a caller. + return ProvisioningCaller(attribute_name, attribute) + else: # This is a regular attribute. Return it as-is. return attribute - else: - # This attribute is callable. Wrap it in a caller. - return ProvisioningCaller(attribute) + + +class ProvisioningTransport(xmlrpclib.Transport): + """An XML-RPC transport that sets a low socket timeout.""" + + @property + def timeout(self): + return settings.PSERV_TIMEOUT + + def make_connection(self, host): + """See `xmlrpclib.Transport.make_connection`. + + This also sets the desired socket timeout. + """ + connection = xmlrpclib.Transport.make_connection(self, host) + connection.timeout = self.timeout + return connection def get_provisioning_api_proxy(): @@ -157,8 +301,9 @@ if settings.USE_REAL_PSERV: # Use a real provisioning server. This requires PSERV_URL to be # set. + xmlrpc_transport = ProvisioningTransport(use_datetime=True) xmlrpc_proxy = xmlrpclib.ServerProxy( - settings.PSERV_URL, allow_none=True, use_datetime=True) + settings.PSERV_URL, transport=xmlrpc_transport, allow_none=True) else: # Create a fake. The code that provides the testing fake is not # available in an installed production system, so import it only @@ -187,26 +332,67 @@ return urljoin(maas_url, path) -def compose_metadata(node): - """Put together metadata information for `node`. +def compose_cloud_init_preseed(token): + """Compose the preseed value for a node in any state but Commissioning.""" + credentials = urlencode({ + 'oauth_consumer_key': token.consumer.key, + 'oauth_token_key': token.key, + 'oauth_token_secret': token.secret, + }) + + # Preseed data to send to cloud-init. We set this as MAAS_PRESEED in + # ks_meta, and it gets fed straight into debconf. + metadata_preseed_items = [ + ('datasources', 'multiselect', 'MAAS'), + ('maas-metadata-url', 'string', get_metadata_server_url()), + ('maas-metadata-credentials', 'string', credentials), + ] + + return '\n'.join( + "cloud-init cloud-init/%s %s %s" % ( + item_name, + item_type, + item_value, + ) + for item_name, item_type, item_value in metadata_preseed_items) + + +def compose_commissioning_preseed(token): + """Compose the preseed value for a Commissioning node.""" + return "#cloud-config\n%s" % yaml.dump({ + 'datasource': { + 'MAAS': { + 'metadata_url': get_metadata_server_url(), + 'consumer_key': token.consumer.key, + 'token_key': token.key, + 'token_secret': token.secret, + } + } + }) + + +def compose_preseed(node): + """Put together preseed data for `node`. + + This produces preseed data in different formats depending on the node's + state: if it's Commissioning, it boots into commissioning mode with its + own profile, its own user_data, and also its own preseed format. It's + basically a network boot. + Otherwise, it will get a different format that feeds directly into the + installer. - :param node: The node to provide with metadata. + :param node: The node to compose preseed data for. :type node: Node - :return: A dict containing metadata information that will be seeded to - the node, so that it can access the metadata service. + :return: Preseed data containing the information the node needs in order + to access the metadata service: its URL and auth token. """ # Circular import. from metadataserver.models import NodeKey token = NodeKey.objects.get_token_for_node(node) - credentials = urlencode({ - 'oauth_consumer_key': token.consumer.key, - 'oauth_token_key': token.key, - 'oauth_token_secret': token.secret, - }) - return { - 'maas-metadata-url': get_metadata_server_url(), - 'maas-metadata-credentials': credentials, - } + if node.status == NODE_STATUS.COMMISSIONING: + return compose_commissioning_preseed(token) + else: + return compose_cloud_init_preseed(token) def name_arch_in_cobbler_style(architecture): @@ -245,10 +431,24 @@ papi = get_provisioning_api_proxy() profile = select_profile_for_node(instance) power_type = instance.get_effective_power_type() - metadata = compose_metadata(instance) + preseed_data = compose_preseed(instance) papi.add_node( instance.system_id, instance.hostname, - profile, power_type, metadata) + profile, power_type, preseed_data) + + # When the node is allocated this must not modify the netboot_enabled + # parameter. The node, once it has booted and installed itself, asks the + # provisioning server to disable netbooting. If this were to enable + # netbooting again, the node would reinstall itself the next time it + # booted. However, netbooting must be enabled at the point the node is + # allocated so that the first install goes ahead, hence why it is set for + # all other statuses... with one exception; retired nodes are never + # netbooted. + if instance.status != NODE_STATUS.ALLOCATED: + netboot_enabled = instance.status not in ( + NODE_STATUS.DECLARED, NODE_STATUS.RETIRED) + delta = {"netboot_enabled": netboot_enabled} + papi.modify_nodes({instance.system_id: delta}) def set_node_mac_addresses(node): diff -Nru maas-0.1+bzr415+dfsg/src/maasserver/templates/maasserver/base.html maas-0.1+bzr462+dfsg/src/maasserver/templates/maasserver/base.html --- maas-0.1+bzr415+dfsg/src/maasserver/templates/maasserver/base.html 2012-04-04 18:59:25.000000000 +0000 +++ maas-0.1+bzr462+dfsg/src/maasserver/templates/maasserver/base.html 2012-04-12 20:11:10.000000000 +0000 @@ -84,6 +84,9 @@ {% endif %}
    + {% for persistent_error in persistent_errors %} +
  • {{ persistent_error }}
  • + {% endfor %} {% if messages %} {% for message in messages %} diff -Nru maas-0.1+bzr415+dfsg/src/maasserver/templates/maasserver/settings.html maas-0.1+bzr462+dfsg/src/maasserver/templates/maasserver/settings.html --- maas-0.1+bzr415+dfsg/src/maasserver/templates/maasserver/settings.html 2012-04-04 18:59:25.000000000 +0000 +++ maas-0.1+bzr462+dfsg/src/maasserver/templates/maasserver/settings.html 2012-04-12 20:11:10.000000000 +0000 @@ -67,7 +67,8 @@
-
+ + -
+ +