+
Settings
+
-
-
{% endblock %}
diff -Nru maas-0.1+bzr415+dfsg/src/maasserver/testing/factory.py maas-0.1+bzr462+dfsg/src/maasserver/testing/factory.py
--- maas-0.1+bzr415+dfsg/src/maasserver/testing/factory.py 2012-04-04 18:59:25.000000000 +0000
+++ maas-0.1+bzr462+dfsg/src/maasserver/testing/factory.py 2012-04-12 20:11:10.000000000 +0000
@@ -29,6 +29,11 @@
from maasserver.testing import get_data
from maasserver.testing.enum import map_enum
import maastesting.factory
+from metadataserver.models import NodeCommissionResult
+
+# We have a limited number of public keys:
+# src/maasserver/tests/data/test_rsa{0, 1, 2, 3, 4}.pub
+MAX_PUBLIC_KEYS = 5
class Factory(maastesting.factory.Factory):
@@ -71,6 +76,17 @@
node.save(skip_check=True)
return node
+ def make_node_commission_result(self, node=None, name=None, data=None):
+ if node is None:
+ node = self.make_node()
+ if name is None:
+ name = "ncrname-" + self.getRandomString(92)
+ if data is None:
+ data = "ncrdata-" + self.getRandomString(1000)
+ ncr = NodeCommissionResult(node=node, name=name, data=data)
+ ncr.save()
+ return ncr
+
def make_mac_address(self, address):
"""Create a MAC address."""
node = Node()
@@ -89,26 +105,33 @@
return User.objects.create_user(
username=username, password=password, email=email)
- def make_sshkey(self, user):
- key_string = get_data('data/test_rsa.pub')
+ def make_sshkey(self, user, key_string=None):
+ if key_string is None:
+ key_string = get_data('data/test_rsa0.pub')
key = SSHKey(key=key_string, user=user)
key.save()
return key
- def make_user_with_keys(self, n_keys=2, **kwargs):
- """Create a user with n `SSHKey`.
+ def make_user_with_keys(self, n_keys=2, user=None, **kwargs):
+ """Create a user with n `SSHKey`. If user is not None, use this user
+ instead of creating one.
Additional keyword arguments are passed to `make_user()`.
-
- Keys will have a comment of the form:
-key- where i
- is the key index.
"""
- user = self.make_user(**kwargs)
+ if n_keys > MAX_PUBLIC_KEYS:
+ raise RuntimeError(
+ "Cannot create more than %d public keys. If you need more: "
+ "add more keys in src/maasserver/tests/data/."
+ % MAX_PUBLIC_KEYS)
+ if user is None:
+ user = self.make_user(**kwargs)
+ keys = []
for i in range(n_keys):
- SSHKey(
- user=user,
- key='ssh-rsa KEY %s-key-%d' % (user.username, i)).save()
- return user
+ key_string = get_data('data/test_rsa%d.pub' % i)
+ key = SSHKey(user=user, key=key_string)
+ key.save()
+ keys.append(key)
+ return user, keys
def make_admin(self, username=None, password=None, email=None):
admin = self.make_user(
diff -Nru maas-0.1+bzr415+dfsg/src/maasserver/testing/testcase.py maas-0.1+bzr462+dfsg/src/maasserver/testing/testcase.py
--- maas-0.1+bzr415+dfsg/src/maasserver/testing/testcase.py 2012-04-04 18:59:25.000000000 +0000
+++ maas-0.1+bzr462+dfsg/src/maasserver/testing/testcase.py 2012-04-12 20:11:10.000000000 +0000
@@ -10,6 +10,7 @@
__metaclass__ = type
__all__ = [
+ 'AdminLoggedInTestCase',
'LoggedInTestCase',
'TestCase',
'TestModelTestCase',
@@ -43,3 +44,10 @@
"""Promote the logged-in user to admin."""
self.logged_in_user.is_superuser = True
self.logged_in_user.save()
+
+
+class AdminLoggedInTestCase(LoggedInTestCase):
+
+ def setUp(self):
+ super(AdminLoggedInTestCase, self).setUp()
+ self.become_admin()
diff -Nru maas-0.1+bzr415+dfsg/src/maasserver/tests/data/test_rsa0.pub maas-0.1+bzr462+dfsg/src/maasserver/tests/data/test_rsa0.pub
--- maas-0.1+bzr415+dfsg/src/maasserver/tests/data/test_rsa0.pub 1970-01-01 00:00:00.000000000 +0000
+++ maas-0.1+bzr462+dfsg/src/maasserver/tests/data/test_rsa0.pub 2012-04-12 20:11:10.000000000 +0000
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDdrzzDZNwyMVBvBTT6kBnrfPZv/AUbkxj7G5CaMTdw6xkKthV22EntD3lxaQxRKzQTfCc2d/CC1K4ushCcRs1S6SQ2zJ2jDq1UmOUkDMgvNh4JVhJYSKc6mu8i3s7oGSmBado5wvtlpSzMrscOpf8Qe/wmT5fH12KB9ipJqoFNQMVbVcVarE/v6wpn3GZC62YRb5iaz9/M+t92Qhu50W2u+KfouqtKB2lwIDDKZMww38ExtdMouh2FZpxaoh4Uey5bRp3tM3JgnWcX6fyUOp2gxJRPIlD9rrZhX5IkEkZM8MQbdPTQLgIf98oFph5RG6w1t02BvI9nJKM7KkKEfBHt ubuntu@server-7476
diff -Nru maas-0.1+bzr415+dfsg/src/maasserver/tests/data/test_rsa1.pub maas-0.1+bzr462+dfsg/src/maasserver/tests/data/test_rsa1.pub
--- maas-0.1+bzr415+dfsg/src/maasserver/tests/data/test_rsa1.pub 1970-01-01 00:00:00.000000000 +0000
+++ maas-0.1+bzr462+dfsg/src/maasserver/tests/data/test_rsa1.pub 2012-04-12 20:11:10.000000000 +0000
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC6Gkj1y8/0T7q/FqBSr9xRBO9GzT+JeoWNXaqhUBg179Zd53XM4qblVwz/rsMa70te8CYNIFU+GbcNY1tNCo78NlHjQA8H98COnbVWKxvABECHrJ8nbYB4lWH9wI8/uvR0um6yUb/tZYbiSqnQxhoGAF/uQQfhqzc+tc7uTjnsa6krrNqQCdpFbAVVy+vZzvcJl6CX8nu5uJ8jedWfXOZJFcQPH+VwkUT0oV+1zVeLpE4LFkRO52JrC9Dy1xgrYM0EhcrShBdD1GQx9IXdW4Z9PIaVcq/y4Qv574yHMvi+6hwG6xpCtRXmy0lG0LiG60c1yOredkO6U0MJIVbeZ/+r ubuntu@server-7493
diff -Nru maas-0.1+bzr415+dfsg/src/maasserver/tests/data/test_rsa2.pub maas-0.1+bzr462+dfsg/src/maasserver/tests/data/test_rsa2.pub
--- maas-0.1+bzr415+dfsg/src/maasserver/tests/data/test_rsa2.pub 1970-01-01 00:00:00.000000000 +0000
+++ maas-0.1+bzr462+dfsg/src/maasserver/tests/data/test_rsa2.pub 2012-04-12 20:11:10.000000000 +0000
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDKVdMk4Q+13uUvXjb6iU+oB2Auk0HpaILZ8Pw/V63PTJ+QXtEp0vTe6DEvr9uF2vl6tF+AosiG4krEwqBNGx/h8MmFO7BgNTxn9eU2VwfHzmQ2nqkXHsXgp66cNT0Yd0nfvVV/fsMpKN9fUaYrXjAlFxvC9iQ33Rp6vj/X+oqDvYf3xZjbuZy+BxdJnmiTAJcFouTyrdy1Em1EZITq5M4EXw93/O2vAPYSFPAeELBE+mIMJxOCY1Fm101oAqO0qof3Rb2hZxc2WINjmqZIxoi+sviU0ny/dIFknhYEg1Xh2hObPn0nN5+4VHjBTdRmpRXqggotc53sYC5udVmFsW8B ubuntu@server-7493
diff -Nru maas-0.1+bzr415+dfsg/src/maasserver/tests/data/test_rsa3.pub maas-0.1+bzr462+dfsg/src/maasserver/tests/data/test_rsa3.pub
--- maas-0.1+bzr415+dfsg/src/maasserver/tests/data/test_rsa3.pub 1970-01-01 00:00:00.000000000 +0000
+++ maas-0.1+bzr462+dfsg/src/maasserver/tests/data/test_rsa3.pub 2012-04-12 20:11:10.000000000 +0000
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDai2ir5yxckoYTHUbFL6pe01Kx+Dy6nw9p7LhFaBixUOh8G7eIgFBguYcir2ZKBfM/lbTnW+MSiGF2VMlXX0+X9Ux2iwPSJa2wIA7Cc5prCz/RnMRKQ+2S1JJuORoi8tDI0p1R0sGWMXCwaj30oRN0THWz884+d3YlDD/O39h74gnLNEx/TQig/r/Aev3VfeKO6dlbbX81vSad2JVncislyMq1TgJdhn2/JI8t+LW0xVc6ZgQr94YB2M2DNjFSisP2vDrV5LWM+IqiF8T/YHkcSsANr8WWvZWa79uHyRBU3xr2qZZqMjMVL0B/NOJYXyGBIJ7HQnlVLmqFenKl8ZtL ubuntu@server-7493
diff -Nru maas-0.1+bzr415+dfsg/src/maasserver/tests/data/test_rsa4.pub maas-0.1+bzr462+dfsg/src/maasserver/tests/data/test_rsa4.pub
--- maas-0.1+bzr415+dfsg/src/maasserver/tests/data/test_rsa4.pub 1970-01-01 00:00:00.000000000 +0000
+++ maas-0.1+bzr462+dfsg/src/maasserver/tests/data/test_rsa4.pub 2012-04-12 20:11:10.000000000 +0000
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDNDA4vXVTxHuKikIXeA6/K/X7hKpJcOJV0HcXUHlSNa9phNW0f8vbci+BxcLAqIz/U+BPiQ9lCxz7so+qCTFrM4poOdkTyup8VUxUqntiaxgiCJZ1of+eMe39+S9XQk6RogiCpExanhD9xPLkK/mLr5phnQwDjEDJwD4OOF0rYsbYoqje/0Pd+Tm0PIepq/qwsu5PAKPJU8dfnp8BWLCuIJ+DA2lfRUjmxWwLczfM/4hu1bZlYp1mzJJgMIOY92/pUToYxvBiIiKs3qWh6HC5Vxo5Vz4w5WLnTnIPDvpYBvWj8LGXJwHuhqlzed2icwPk8krip2BzwsHotru3UXtKf ubuntu@server-7493
diff -Nru maas-0.1+bzr415+dfsg/src/maasserver/tests/data/test_rsa.pub maas-0.1+bzr462+dfsg/src/maasserver/tests/data/test_rsa.pub
--- maas-0.1+bzr415+dfsg/src/maasserver/tests/data/test_rsa.pub 2012-04-04 18:59:25.000000000 +0000
+++ maas-0.1+bzr462+dfsg/src/maasserver/tests/data/test_rsa.pub 1970-01-01 00:00:00.000000000 +0000
@@ -1 +0,0 @@
-ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDdrzzDZNwyMVBvBTT6kBnrfPZv/AUbkxj7G5CaMTdw6xkKthV22EntD3lxaQxRKzQTfCc2d/CC1K4ushCcRs1S6SQ2zJ2jDq1UmOUkDMgvNh4JVhJYSKc6mu8i3s7oGSmBado5wvtlpSzMrscOpf8Qe/wmT5fH12KB9ipJqoFNQMVbVcVarE/v6wpn3GZC62YRb5iaz9/M+t92Qhu50W2u+KfouqtKB2lwIDDKZMww38ExtdMouh2FZpxaoh4Uey5bRp3tM3JgnWcX6fyUOp2gxJRPIlD9rrZhX5IkEkZM8MQbdPTQLgIf98oFph5RG6w1t02BvI9nJKM7KkKEfBHt ubuntu@server-7476
diff -Nru maas-0.1+bzr415+dfsg/src/maasserver/tests/test_api.py maas-0.1+bzr462+dfsg/src/maasserver/tests/test_api.py
--- maas-0.1+bzr415+dfsg/src/maasserver/tests/test_api.py 2012-04-04 18:59:25.000000000 +0000
+++ maas-0.1+bzr462+dfsg/src/maasserver/tests/test_api.py 2012-04-12 20:11:10.000000000 +0000
@@ -45,6 +45,7 @@
from maasserver.testing.factory import factory
from maasserver.testing.oauthclient import OAuthAuthenticatedClient
from maasserver.testing.testcase import (
+ AdminLoggedInTestCase,
LoggedInTestCase,
TestCase,
)
@@ -94,8 +95,11 @@
extract_constraints(QueryDict('name=%s' % name)))
-class AnonymousEnlistmentAPITest(APIv10TestMixin, TestCase):
- # Nodes can be enlisted anonymously.
+class EnlistmentAPITest(APIv10TestMixin):
+ # This is a mixin containing enlistement tests. We will run this for:
+ # an anonymous user, a simple (non-admin) user and an admin user.
+ # XXX: rvb 2012-04-10 bug=978035: It would be better to use
+ # testscenarios for this.
def test_POST_new_creates_node(self):
# The API allows a Node to be created.
@@ -116,28 +120,10 @@
self.assertEqual('diane', parsed_result['hostname'])
self.assertNotEqual(0, len(parsed_result.get('system_id')))
[diane] = Node.objects.filter(hostname='diane')
- self.assertEqual(2, diane.after_commissioning_action)
+ # XXX JeroenVermeulen 2012-04-12, bug=979539: re-enable.
+ #self.assertEqual(2, diane.after_commissioning_action)
self.assertEqual(architecture, diane.architecture)
- def test_POST_new_anonymous_creates_node_in_declared_state(self):
- # Upon anonymous enlistment, a node goes into the Declared
- # state. Deliberate approval is required before we start
- # reinstalling the system, wiping its disks etc.
- response = self.client.post(
- self.get_uri('nodes/'),
- {
- 'op': 'new',
- 'hostname': factory.getRandomString(),
- 'architecture': factory.getRandomChoice(ARCHITECTURE_CHOICES),
- 'after_commissioning_action': '2',
- 'mac_addresses': ['aa:bb:cc:dd:ee:ff'],
- })
- self.assertEqual(httplib.OK, response.status_code)
- system_id = json.loads(response.content)['system_id']
- self.assertEqual(
- NODE_STATUS.DECLARED,
- Node.objects.get(system_id=system_id).status)
-
def test_POST_new_power_type_defaults_to_asking_config(self):
architecture = factory.getRandomChoice(ARCHITECTURE_CHOICES)
response = self.client.post(
@@ -194,25 +180,6 @@
system_id=json.loads(response.content)['system_id'])
self.assertEqual('node-aabbccddeeff.local', node.hostname)
- def test_POST_returns_limited_fields(self):
- architecture = factory.getRandomChoice(ARCHITECTURE_CHOICES)
- response = self.client.post(
- self.get_uri('nodes/'),
- {
- 'op': 'new',
- 'hostname': 'diane',
- 'architecture': architecture,
- 'after_commissioning_action': '2',
- 'mac_addresses': ['aa:bb:cc:dd:ee:ff', '22:bb:cc:dd:ee:ff'],
- })
- parsed_result = json.loads(response.content)
- self.assertItemsEqual(
- [
- 'hostname', 'system_id', 'macaddress_set', 'architecture',
- 'status'
- ],
- list(parsed_result))
-
def test_POST_fails_without_operation(self):
# If there is no operation ('op=operation_name') specified in the
# request data, a 'Bad request' response is returned.
@@ -259,6 +226,7 @@
self.fail("post_save should not have been called")
post_save.connect(node_created, sender=Node)
+ self.addCleanup(post_save.disconnect, node_created, sender=Node)
self.client.post(
self.get_uri('nodes/'),
{
@@ -318,6 +286,34 @@
self.assertIn('application/json', response['Content-Type'])
self.assertItemsEqual(['architecture'], parsed_result)
+
+class NonAdminEnlistmentAPITest(EnlistmentAPITest):
+ # This is a mixin containing enlistement tests for non-admin users.
+
+ def test_POST_non_admin_creates_node_in_declared_state(self):
+ # Upon non-admin enlistment, a node goes into the Declared
+ # state. Deliberate approval is required before we start
+ # reinstalling the system, wiping its disks etc.
+ response = self.client.post(
+ self.get_uri('nodes/'),
+ {
+ 'op': 'new',
+ 'hostname': factory.getRandomString(),
+ 'architecture': factory.getRandomChoice(ARCHITECTURE_CHOICES),
+ 'after_commissioning_action': '2',
+ 'mac_addresses': ['aa:bb:cc:dd:ee:ff'],
+ })
+ self.assertEqual(httplib.OK, response.status_code)
+ system_id = json.loads(response.content)['system_id']
+ self.assertEqual(
+ NODE_STATUS.DECLARED,
+ Node.objects.get(system_id=system_id).status)
+
+
+class AnonymousEnlistmentAPITest(NonAdminEnlistmentAPITest, TestCase):
+ # This is an actual test case that uses the NonAdminEnlistmentAPITest
+ # mixin and adds enlistement tests specific to anonymous users.
+
def test_POST_accept_not_allowed(self):
# An anonymous user is not allowed to accept an anonymously
# enlisted node. That would defeat the whole purpose of holding
@@ -329,14 +325,161 @@
(httplib.UNAUTHORIZED, "You must be logged in to accept nodes."),
(response.status_code, response.content))
+ def test_POST_returns_limited_fields(self):
+ response = self.client.post(
+ self.get_uri('nodes/'),
+ {
+ 'op': 'new',
+ 'architecture': factory.getRandomChoice(ARCHITECTURE_CHOICES),
+ 'hostname': factory.getRandomString(),
+ 'after_commissioning_action': '2',
+ 'mac_addresses': ['aa:bb:cc:dd:ee:ff', '22:bb:cc:dd:ee:ff'],
+ })
+ parsed_result = json.loads(response.content)
+ self.assertItemsEqual(
+ [
+ 'hostname', 'system_id', 'macaddress_set', 'architecture',
+ 'status',
+ ],
+ list(parsed_result))
+
+
+class SimpleUserLoggedInEnlistmentAPITest(NonAdminEnlistmentAPITest,
+ LoggedInTestCase):
+ # This is an actual test case that uses the NonAdminEnlistmentAPITest
+ # mixin plus enlistement tests specific to simple (non-admin) users.
+
+ def test_POST_accept_not_allowed(self):
+ # An non-admin user is not allowed to accept an anonymously
+ # enlisted node. That would defeat the whole purpose of holding
+ # those nodes for approval.
+ node_id = factory.make_node(status=NODE_STATUS.DECLARED).system_id
+ response = self.client.post(
+ self.get_uri('nodes/'), {'op': 'accept', 'nodes': [node_id]})
+ self.assertEqual(
+ (httplib.FORBIDDEN,
+ "You don't have the required permission to accept the "
+ "following node(s): %s." % node_id),
+ (response.status_code, response.content))
+
+ def test_POST_returns_limited_fields(self):
+ response = self.client.post(
+ self.get_uri('nodes/'),
+ {
+ 'op': 'new',
+ 'hostname': factory.getRandomString(),
+ 'architecture': factory.getRandomChoice(ARCHITECTURE_CHOICES),
+ 'after_commissioning_action': '2',
+ 'mac_addresses': ['aa:bb:cc:dd:ee:ff', '22:bb:cc:dd:ee:ff'],
+ })
+ parsed_result = json.loads(response.content)
+ self.assertItemsEqual(
+ [
+ 'hostname', 'system_id', 'macaddress_set', 'architecture',
+ 'status', 'resource_uri',
+ ],
+ list(parsed_result))
+
+
+class AdminLoggedInEnlistmentAPITest(EnlistmentAPITest,
+ AdminLoggedInTestCase):
+ # This is an actual test case that uses the EnlistmentAPITest mixin
+ # and adds enlistement tests specific to admin users.
+
+ def test_POST_admin_creates_node_in_commissioning_state(self):
+ # When an admin user enlists a node, it goes into the
+ # Commissioning state.
+ response = self.client.post(
+ self.get_uri('nodes/'),
+ {
+ 'op': 'new',
+ 'hostname': factory.getRandomString(),
+ 'architecture': factory.getRandomChoice(ARCHITECTURE_CHOICES),
+ 'after_commissioning_action': '2',
+ 'mac_addresses': ['aa:bb:cc:dd:ee:ff'],
+ })
+ self.assertEqual(httplib.OK, response.status_code)
+ system_id = json.loads(response.content)['system_id']
+ self.assertEqual(
+ NODE_STATUS.COMMISSIONING,
+ Node.objects.get(system_id=system_id).status)
+
+ def test_POST_returns_limited_fields(self):
+ response = self.client.post(
+ self.get_uri('nodes/'),
+ {
+ 'op': 'new',
+ 'hostname': factory.getRandomString(),
+ 'architecture': factory.getRandomChoice(ARCHITECTURE_CHOICES),
+ 'after_commissioning_action': '2',
+ 'mac_addresses': ['aa:bb:cc:dd:ee:ff', '22:bb:cc:dd:ee:ff'],
+ })
+ parsed_result = json.loads(response.content)
+ self.assertItemsEqual(
+ [
+ 'hostname', 'system_id', 'macaddress_set', 'architecture',
+ 'status', 'resource_uri',
+ ],
+ list(parsed_result))
+
+
+class AnonymousIsRegisteredAPITest(APIv10TestMixin, TestCase):
+
+ def test_is_registered_returns_True_if_node_registered(self):
+ mac_address = factory.getRandomMACAddress()
+ factory.make_mac_address(mac_address)
+ response = self.client.get(
+ self.get_uri('nodes/'),
+ {'op': 'is_registered', 'mac_address': mac_address})
+ self.assertEqual(
+ (httplib.OK, "true"),
+ (response.status_code, response.content))
+
+ def test_is_registered_returns_False_if_mac_registered_node_retired(self):
+ mac_address = factory.getRandomMACAddress()
+ mac = factory.make_mac_address(mac_address)
+ mac.node.status = NODE_STATUS.RETIRED
+ mac.node.save()
+ response = self.client.get(
+ self.get_uri('nodes/'),
+ {'op': 'is_registered', 'mac_address': mac_address})
+ self.assertEqual(
+ (httplib.OK, "false"),
+ (response.status_code, response.content))
+
+ def test_is_registered_normalizes_mac_address(self):
+ # These two non-normalized MAC Addresses are the same.
+ non_normalized_mac_address = 'AA-bb-cc-dd-ee-ff'
+ non_normalized_mac_address2 = 'aabbccddeeff'
+ factory.make_mac_address(non_normalized_mac_address)
+ response = self.client.get(
+ self.get_uri('nodes/'),
+ {
+ 'op': 'is_registered',
+ 'mac_address': non_normalized_mac_address2
+ })
+ self.assertEqual(
+ (httplib.OK, "true"),
+ (response.status_code, response.content))
+
+ def test_is_registered_returns_False_if_node_not_registered(self):
+ mac_address = factory.getRandomMACAddress()
+ response = self.client.get(
+ self.get_uri('nodes/'),
+ {'op': 'is_registered', 'mac_address': mac_address})
+ self.assertEqual(
+ (httplib.OK, "false"),
+ (response.status_code, response.content))
+
class NodeAnonAPITest(APIv10TestMixin, TestCase):
def test_anon_nodes_GET(self):
- # Anonymous requests to the API are denied.
+ # Anonymous requests to the API without a specified operation
+ # get a "Bad Request" response.
response = self.client.get(self.get_uri('nodes/'))
- self.assertEqual(httplib.UNAUTHORIZED, response.status_code)
+ self.assertEqual(httplib.BAD_REQUEST, response.status_code)
def test_anon_api_doc(self):
# The documentation is accessible to anon users.
@@ -729,24 +872,6 @@
NODE_STATUS.DECLARED,
Node.objects.get(system_id=system_id).status)
- def test_POST_new_when_admin_creates_node_in_ready_state(self):
- # When an admin user enlists a node, it goes into the Ready state.
- # This will change once we start doing proper commissioning.
- self.become_admin()
- response = self.client.post(
- self.get_uri('nodes/'),
- {
- 'op': 'new',
- 'hostname': factory.getRandomString(),
- 'architecture': factory.getRandomChoice(ARCHITECTURE_CHOICES),
- 'after_commissioning_action': '2',
- 'mac_addresses': ['aa:bb:cc:dd:ee:ff'],
- })
- self.assertEqual(httplib.OK, response.status_code)
- system_id = json.loads(response.content)['system_id']
- self.assertEqual(
- NODE_STATUS.READY, Node.objects.get(system_id=system_id).status)
-
def test_GET_list_lists_nodes(self):
# The api allows for fetching the list of Nodes.
node1 = factory.make_node()
@@ -997,7 +1122,7 @@
# This will change when we add provisioning. Until then,
# acceptance gets a node straight to Ready state.
self.become_admin()
- target_state = NODE_STATUS.READY
+ target_state = NODE_STATUS.COMMISSIONING
node = factory.make_node(status=NODE_STATUS.DECLARED)
response = self.client.post(
@@ -1075,7 +1200,7 @@
# This will change when we add provisioning. Until then,
# acceptance gets a node straight to Ready state.
self.become_admin()
- target_state = NODE_STATUS.READY
+ target_state = NODE_STATUS.COMMISSIONING
nodes = [
factory.make_node(status=NODE_STATUS.DECLARED)
diff -Nru maas-0.1+bzr415+dfsg/src/maasserver/tests/test_components.py maas-0.1+bzr462+dfsg/src/maasserver/tests/test_components.py
--- maas-0.1+bzr415+dfsg/src/maasserver/tests/test_components.py 1970-01-01 00:00:00.000000000 +0000
+++ maas-0.1+bzr462+dfsg/src/maasserver/tests/test_components.py 2012-04-12 20:11:10.000000000 +0000
@@ -0,0 +1,83 @@
+# Copyright 2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Test maasserver components module."""
+
+from __future__ import (
+ print_function,
+ unicode_literals,
+ )
+
+__metaclass__ = type
+__all__ = []
+
+
+import random
+
+from maasserver import components
+from maasserver.components import (
+ COMPONENT,
+ discard_persistent_error,
+ get_persistent_errors,
+ register_persistent_error,
+ )
+from maasserver.testing.enum import map_enum
+from maasserver.testing.factory import factory
+from maasserver.testing.testcase import TestCase
+
+
+def simple_error_display(error):
+ return str(error)
+
+
+def get_random_component():
+ random.choice(map_enum(COMPONENT).values())
+
+
+class PersistentErrorsUtilitiesTest(TestCase):
+
+ def setUp(self):
+ super(PersistentErrorsUtilitiesTest, self).setUp()
+ self._PERSISTENT_ERRORS = {}
+ self.patch(components, '_PERSISTENT_ERRORS', self._PERSISTENT_ERRORS)
+
+ def test_register_persistent_error_registers_error(self):
+ error_message = factory.getRandomString()
+ component = get_random_component()
+ register_persistent_error(component, error_message)
+ self.assertItemsEqual(
+ {component: error_message}, self._PERSISTENT_ERRORS)
+
+ def test_register_persistent_error_stores_last_error(self):
+ error_message = factory.getRandomString()
+ error_message2 = factory.getRandomString()
+ component = get_random_component()
+ register_persistent_error(component, error_message)
+ register_persistent_error(component, error_message2)
+ self.assertItemsEqual(
+ {component: error_message2}, self._PERSISTENT_ERRORS)
+
+ def test_discard_persistent_error_discards_error(self):
+ error_message = factory.getRandomString()
+ component = get_random_component()
+ register_persistent_error(component, error_message)
+ discard_persistent_error(component)
+ self.assertItemsEqual({}, self._PERSISTENT_ERRORS)
+
+ def test_discard_persistent_error_can_be_called_many_times(self):
+ error_message = factory.getRandomString()
+ component = get_random_component()
+ register_persistent_error(component, error_message)
+ discard_persistent_error(component)
+ discard_persistent_error(component)
+ self.assertItemsEqual({}, self._PERSISTENT_ERRORS)
+
+ def get_persistent_errors_returns_text_for_error_codes(self):
+ errors, components = [], []
+ for i in range(3):
+ error_message = factory.getRandomString()
+ component = get_random_component()
+ register_persistent_error(component, error_message)
+ errors.append(error_message)
+ components.append(component)
+ self.assertItemsEqual(errors, get_persistent_errors())
diff -Nru maas-0.1+bzr415+dfsg/src/maasserver/tests/test_forms.py maas-0.1+bzr462+dfsg/src/maasserver/tests/test_forms.py
--- maas-0.1+bzr415+dfsg/src/maasserver/tests/test_forms.py 2012-04-04 18:59:25.000000000 +0000
+++ maas-0.1+bzr462+dfsg/src/maasserver/tests/test_forms.py 2012-04-12 20:11:10.000000000 +0000
@@ -184,7 +184,11 @@
form = UINodeEditForm()
self.assertEqual(
- ['hostname', 'after_commissioning_action'], list(form.fields))
+ [
+ 'hostname',
+ # XXX JeroenVermeulen 2012-04-12, bug=979539: re-enable.
+ #'after_commissioning_action',
+ ], list(form.fields))
def test_UINodeEditForm_changes_node(self):
node = factory.make_node()
@@ -201,14 +205,20 @@
form.save()
self.assertEqual(hostname, node.hostname)
- self.assertEqual(
- after_commissioning_action, node.after_commissioning_action)
+ # XXX JeroenVermeulen 2012-04-12, bug=979539: re-enable.
+ #self.assertEqual(
+ # after_commissioning_action, node.after_commissioning_action)
def test_UIAdminNodeEditForm_contains_limited_set_of_fields(self):
form = UIAdminNodeEditForm()
- self.assertSequenceEqual(
- ['hostname', 'after_commissioning_action', 'power_type'],
+ self.assertEqual(
+ [
+ 'hostname',
+ # XXX JeroenVermeulen 2012-04-12, bug=979539: re-enable.
+ #'after_commissioning_action',
+ 'power_type',
+ ],
list(form.fields))
def test_UIAdminNodeEditForm_changes_node(self):
@@ -227,8 +237,9 @@
form.save()
self.assertEqual(hostname, node.hostname)
- self.assertEqual(
- after_commissioning_action, node.after_commissioning_action)
+ # XXX JeroenVermeulen 2012-04-12, bug=979539: re-enable.
+ #self.assertEqual(
+ # after_commissioning_action, node.after_commissioning_action)
self.assertEqual(power_type, node.power_type)
@@ -242,9 +253,10 @@
def test_NODE_ACTIONS_dict(self):
actions = sum(NODE_ACTIONS.values(), [])
+ keys = ['permission', 'display', 'execute', 'message']
self.assertThat(
[sorted(action.keys()) for action in actions],
- AllMatch(Equals(sorted(['permission', 'display', 'execute']))))
+ AllMatch(Equals(sorted(keys))))
class TestNodeActionForm(TestCase):
@@ -257,7 +269,7 @@
form = get_action_form(admin)(node)
actions = form.available_action_methods(node, admin)
self.assertEqual(
- ["Accept Enlisted node", "Commission node"],
+ ["Accept & commission"],
[action['display'] for action in actions])
# All permissions should be ADMIN.
self.assertEqual(
@@ -292,11 +304,7 @@
form = get_action_form(admin)(node)
self.assertItemsEqual(
- {"Accept Enlisted node": (
- 'accept_enlistment', NODE_PERMISSION.ADMIN),
- "Commission node": (
- 'start_commissioning', NODE_PERMISSION.ADMIN),
- },
+ ["Accept & commission"],
form.action_dict)
def test_get_action_form_for_user(self):
@@ -312,10 +320,10 @@
admin = factory.make_admin()
node = factory.make_node(status=NODE_STATUS.DECLARED)
form = get_action_form(admin)(
- node, {NodeActionForm.input_name: "Accept Enlisted node"})
+ node, {NodeActionForm.input_name: "Accept & commission"})
form.save()
- self.assertEqual(NODE_STATUS.READY, node.status)
+ self.assertEqual(NODE_STATUS.COMMISSIONING, node.status)
def test_get_action_form_for_user_save(self):
user = factory.make_user()
diff -Nru maas-0.1+bzr415+dfsg/src/maasserver/tests/test_models.py maas-0.1+bzr462+dfsg/src/maasserver/tests/test_models.py
--- maas-0.1+bzr415+dfsg/src/maasserver/tests/test_models.py 2012-04-04 18:59:25.000000000 +0000
+++ maas-0.1+bzr462+dfsg/src/maasserver/tests/test_models.py 2012-04-12 20:11:10.000000000 +0000
@@ -24,6 +24,7 @@
PermissionDenied,
ValidationError,
)
+from django.db import IntegrityError
from django.utils.safestring import SafeUnicode
from fixtures import TestWithFixtures
from maasserver.exceptions import (
@@ -58,7 +59,10 @@
from maasserver.testing.enum import map_enum
from maasserver.testing.factory import factory
from maasserver.testing.testcase import TestCase
-from metadataserver.models import NodeUserData
+from metadataserver.models import (
+ NodeCommissionResult,
+ NodeUserData,
+ )
from piston.models import (
Consumer,
KEY_SIZE,
@@ -203,13 +207,10 @@
def test_accept_enlistment_gets_node_out_of_declared_state(self):
# If called on a node in Declared state, accept_enlistment()
# changes the node's status, and returns the node.
-
- # This will change when we add commissioning. Until then,
- # acceptance gets a node straight to Ready state.
- target_state = NODE_STATUS.READY
+ target_state = NODE_STATUS.COMMISSIONING
node = factory.make_node(status=NODE_STATUS.DECLARED)
- return_value = node.accept_enlistment()
+ return_value = node.accept_enlistment(factory.make_user())
self.assertEqual((node, target_state), (return_value, node.status))
def test_accept_enlistment_does_nothing_if_already_accepted(self):
@@ -225,7 +226,7 @@
for status in accepted_states}
return_values = {
- status: node.accept_enlistment()
+ status: node.accept_enlistment(factory.make_user())
for status, node in nodes.items()}
self.assertEqual(
@@ -253,7 +254,7 @@
exceptions = {status: False for status in unacceptable_states}
for status, node in nodes.items():
try:
- node.accept_enlistment()
+ node.accept_enlistment(factory.make_user())
except NodeStateViolation:
exceptions[status] = True
@@ -276,6 +277,42 @@
power_status = get_provisioning_api_proxy().power_status
self.assertEqual('start', power_status[node.system_id])
+ def test_start_commissioning_sets_user_data(self):
+ node = factory.make_node(status=NODE_STATUS.DECLARED)
+ node.start_commissioning(factory.make_admin())
+ path = settings.COMMISSIONING_SCRIPT
+ with open(path, 'r') as f:
+ commissioning_user_data = f.read()
+ self.assertEqual(
+ commissioning_user_data,
+ NodeUserData.objects.get_user_data(node))
+
+ def test_missing_commissioning_script(self):
+ self.patch(
+ settings, 'COMMISSIONING_SCRIPT',
+ '/etc/' + factory.getRandomString(10))
+ node = factory.make_node(status=NODE_STATUS.DECLARED)
+ self.assertRaises(
+ ValidationError,
+ node.start_commissioning, factory.make_admin())
+
+ def test_start_commissioning_clears_node_commissioning_results(self):
+ node = factory.make_node(status=NODE_STATUS.DECLARED)
+ NodeCommissionResult.objects.store_data(
+ node, factory.getRandomString(), factory.getRandomString())
+ node.start_commissioning(factory.make_admin())
+ self.assertItemsEqual([], node.nodecommissionresult_set.all())
+
+ def test_start_commissioning_ignores_other_commissioning_results(self):
+ node = factory.make_node()
+ filename = factory.getRandomString()
+ text = factory.getRandomString()
+ NodeCommissionResult.objects.store_data(node, filename, text)
+ other_node = factory.make_node(status=NODE_STATUS.DECLARED)
+ other_node.start_commissioning(factory.make_admin())
+ self.assertEqual(
+ text, NodeCommissionResult.objects.get_data(node, filename))
+
def test_full_clean_checks_status_transition_and_raises_if_invalid(self):
# RETIRED -> ALLOCATED is an invalid transition.
node = factory.make_node(
@@ -759,7 +796,7 @@
class SSHKeyValidatorTest(TestCase):
def test_validates_rsa_public_key(self):
- key_string = get_data('data/test_rsa.pub')
+ key_string = get_data('data/test_rsa0.pub')
validate_ssh_public_key(key_string)
# No ValidationError.
@@ -933,7 +970,7 @@
"""Testing for the :class:`SSHKey`."""
def test_sshkey_validation_with_valid_key(self):
- key_string = get_data('data/test_rsa.pub')
+ key_string = get_data('data/test_rsa0.pub')
user = factory.make_user()
key = SSHKey(key=key_string, user=user)
key.full_clean()
@@ -948,7 +985,7 @@
def test_sshkey_display_with_real_life_key(self):
# With a real-life ssh-rsa key, the key_string part is cropped.
- key_string = get_data('data/test_rsa.pub')
+ key_string = get_data('data/test_rsa0.pub')
user = factory.make_user()
key = SSHKey(key=key_string, user=user)
display = key.display_html()
@@ -956,12 +993,40 @@
'ssh-rsa AAAAB3NzaC1yc2E… ubuntu@server-7476', display)
def test_sshkey_display_is_marked_as_HTML_safe(self):
- key_string = get_data('data/test_rsa.pub')
+ key_string = get_data('data/test_rsa0.pub')
user = factory.make_user()
key = SSHKey(key=key_string, user=user)
display = key.display_html()
self.assertIsInstance(display, SafeUnicode)
+ def test_sshkey_user_and_key_unique_together(self):
+ key_string = get_data('data/test_rsa0.pub')
+ user = factory.make_user()
+ key = SSHKey(key=key_string, user=user)
+ key.save()
+ key2 = SSHKey(key=key_string, user=user)
+ self.assertRaises(
+ ValidationError, key2.full_clean)
+
+ def test_sshkey_user_and_key_unique_together_db_level(self):
+ key_string = get_data('data/test_rsa0.pub')
+ user = factory.make_user()
+ key = SSHKey(key=key_string, user=user)
+ key.save()
+ key2 = SSHKey(key=key_string, user=user)
+ self.assertRaises(
+ IntegrityError, key2.save, skip_check=True)
+
+ def test_sshkey_same_key_can_be_used_by_different_users(self):
+ key_string = get_data('data/test_rsa0.pub')
+ user = factory.make_user()
+ key = SSHKey(key=key_string, user=user)
+ key.save()
+ user2 = factory.make_user()
+ key2 = SSHKey(key=key_string, user=user2)
+ key2.full_clean()
+ # No ValidationError.
+
class SSHKeyManagerTest(TestCase):
"""Testing for the :class:`SSHKeyManager` model manager."""
@@ -972,15 +1037,12 @@
self.assertItemsEqual([], keys)
def test_get_keys_for_user_with_keys(self):
- user1 = factory.make_user_with_keys(n_keys=3, username='user1')
+ user1, created_keys = factory.make_user_with_keys(
+ n_keys=3, username='user1')
# user2
factory.make_user_with_keys(n_keys=2)
keys = SSHKey.objects.get_keys_for_user(user1)
- self.assertItemsEqual([
- 'ssh-rsa KEY user1-key-0',
- 'ssh-rsa KEY user1-key-1',
- 'ssh-rsa KEY user1-key-2',
- ], keys)
+ self.assertItemsEqual([key.key for key in created_keys], keys)
class FileStorageTest(TestCase):
diff -Nru maas-0.1+bzr415+dfsg/src/maasserver/tests/test_provisioning.py maas-0.1+bzr462+dfsg/src/maasserver/tests/test_provisioning.py
--- maas-0.1+bzr415+dfsg/src/maasserver/tests/test_provisioning.py 2012-04-04 18:59:25.000000000 +0000
+++ maas-0.1+bzr462+dfsg/src/maasserver/tests/test_provisioning.py 2012-04-12 20:11:10.000000000 +0000
@@ -12,27 +12,40 @@
__all__ = []
from abc import ABCMeta
-from urlparse import parse_qs
+from base64 import b64decode
from xmlrpclib import Fault
from django.conf import settings
-from maasserver import provisioning
+from maasserver import (
+ components,
+ provisioning,
+ )
+from maasserver.components import (
+ COMPONENT,
+ get_persistent_errors,
+ register_persistent_error,
+ )
from maasserver.exceptions import MAASAPIException
from maasserver.models import (
ARCHITECTURE,
- ARCHITECTURE_CHOICES,
Config,
Node,
NODE_AFTER_COMMISSIONING_ACTION,
NODE_STATUS,
+ NODE_STATUS_CHOICES,
)
from maasserver.provisioning import (
- compose_metadata,
+ compose_cloud_init_preseed,
+ compose_commissioning_preseed,
+ compose_preseed,
+ DETAILED_PRESENTATIONS,
get_metadata_server_url,
name_arch_in_cobbler_style,
+ present_detailed_user_friendly_fault,
present_user_friendly_fault,
- PRESENTATIONS,
+ ProvisioningTransport,
select_profile_for_node,
+ SHORT_PRESENTATIONS,
)
from maasserver.testing.enum import map_enum
from maasserver.testing.factory import factory
@@ -44,8 +57,187 @@
)
from provisioningserver.testing.factory import ProvisioningFakeFactory
from testtools.deferredruntest import AsynchronousDeferredRunTest
+from testtools.matchers import (
+ KeysEqual,
+ StartsWith,
+ )
from testtools.testcase import ExpectedException
from twisted.internet.defer import inlineCallbacks
+import yaml
+
+
+class TestHelpers(TestCase):
+ """Tests for helpers that don't actually need any kind of pserv."""
+
+ def test_metadata_server_url_refers_to_own_metadata_service(self):
+ self.assertEqual(
+ "%s/metadata/"
+ % Config.objects.get_config('maas_url').rstrip('/'),
+ get_metadata_server_url())
+
+ def test_metadata_server_url_includes_script_name(self):
+ self.patch(settings, "FORCE_SCRIPT_NAME", "/MAAS")
+ self.assertEqual(
+ "%s/MAAS/metadata/"
+ % Config.objects.get_config('maas_url').rstrip('/'),
+ get_metadata_server_url())
+
+ def test_compose_preseed_for_commissioning_node_produces_yaml(self):
+ node = factory.make_node(status=NODE_STATUS.COMMISSIONING)
+ preseed = yaml.load(compose_preseed(node))
+ self.assertIn('datasource', preseed)
+ self.assertIn('MAAS', preseed['datasource'])
+ self.assertThat(
+ preseed['datasource']['MAAS'],
+ KeysEqual(
+ 'metadata_url', 'consumer_key', 'token_key', 'token_secret'))
+
+ def test_compose_preseed_for_commissioning_node_has_header(self):
+ node = factory.make_node(status=NODE_STATUS.COMMISSIONING)
+ self.assertThat(compose_preseed(node), StartsWith("#cloud-config\n"))
+
+ def test_compose_preseed_includes_metadata_url(self):
+ node = factory.make_node(status=NODE_STATUS.READY)
+ self.assertIn(get_metadata_server_url(), compose_preseed(node))
+
+ def test_compose_preseed_for_commissioning_includes_metadata_url(self):
+ node = factory.make_node(status=NODE_STATUS.COMMISSIONING)
+ preseed = yaml.load(compose_preseed(node))
+ self.assertEqual(
+ get_metadata_server_url(),
+ preseed['datasource']['MAAS']['metadata_url'])
+
+ def test_compose_preseed_includes_node_oauth_token(self):
+ node = factory.make_node(status=NODE_STATUS.READY)
+ preseed = compose_preseed(node)
+ token = NodeKey.objects.get_token_for_node(node)
+ self.assertIn('oauth_consumer_key=%s' % token.consumer.key, preseed)
+ self.assertIn('oauth_token_key=%s' % token.key, preseed)
+ self.assertIn('oauth_token_secret=%s' % token.secret, preseed)
+
+ def test_compose_preseed_for_commissioning_includes_auth_token(self):
+ node = factory.make_node(status=NODE_STATUS.COMMISSIONING)
+ preseed = yaml.load(compose_preseed(node))
+ maas_dict = preseed['datasource']['MAAS']
+ token = NodeKey.objects.get_token_for_node(node)
+ self.assertEqual(token.consumer.key, maas_dict['consumer_key'])
+ self.assertEqual(token.key, maas_dict['token_key'])
+ self.assertEqual(token.secret, maas_dict['token_secret'])
+
+ def test_present_detailed_user_friendly_fault_describes_pserv_fault(self):
+ self.assertIn(
+ "provisioning server",
+ present_user_friendly_fault(Fault(8002, 'error')).message)
+
+ def test_present_detailed_fault_covers_all_pserv_faults(self):
+ all_pserv_faults = set(map_enum(PSERV_FAULT).values())
+ presentable_pserv_faults = set(DETAILED_PRESENTATIONS.keys())
+ self.assertItemsEqual([], all_pserv_faults - presentable_pserv_faults)
+
+ def test_present_detailed_fault_rerepresents_all_pserv_faults(self):
+ fault_string = factory.getRandomString()
+ for fault_code in map_enum(PSERV_FAULT).values():
+ original_fault = Fault(fault_code, fault_string)
+ new_fault = present_detailed_user_friendly_fault(original_fault)
+ self.assertNotEqual(fault_string, new_fault.message)
+
+ def test_present_detailed_fault_describes_cobbler_fault(self):
+ friendly_fault = present_detailed_user_friendly_fault(
+ Fault(PSERV_FAULT.NO_COBBLER, factory.getRandomString()))
+ friendly_text = friendly_fault.message
+ self.assertIn("unable to reach", friendly_text)
+ self.assertIn("Cobbler", friendly_text)
+
+ def test_present_detailed_fault_describes_cobbler_auth_fail(self):
+ friendly_fault = present_detailed_user_friendly_fault(
+ Fault(PSERV_FAULT.COBBLER_AUTH_FAILED, factory.getRandomString()))
+ friendly_text = friendly_fault.message
+ self.assertIn("failed to authenticate", friendly_text)
+ self.assertIn("Cobbler", friendly_text)
+
+ def test_present_detailed_fault_describes_cobbler_auth_error(self):
+ friendly_fault = present_detailed_user_friendly_fault(
+ Fault(PSERV_FAULT.COBBLER_AUTH_ERROR, factory.getRandomString()))
+ friendly_text = friendly_fault.message
+ self.assertIn("authentication token", friendly_text)
+ self.assertIn("Cobbler", friendly_text)
+
+ def test_present_detailed_fault_describes_missing_profile(self):
+ profile = factory.getRandomString()
+ friendly_fault = present_detailed_user_friendly_fault(
+ Fault(
+ PSERV_FAULT.NO_SUCH_PROFILE,
+ "invalid profile name: %s" % profile))
+ friendly_text = friendly_fault.message
+ self.assertIn(profile, friendly_text)
+ self.assertIn("maas-import-isos", friendly_text)
+
+ def test_present_detailed_fault_describes_generic_cobbler_fail(self):
+ error_text = factory.getRandomString()
+ friendly_fault = present_detailed_user_friendly_fault(
+ Fault(PSERV_FAULT.GENERIC_COBBLER_ERROR, error_text))
+ friendly_text = friendly_fault.message
+ self.assertIn("Cobbler", friendly_text)
+ self.assertIn(error_text, friendly_text)
+
+ def test_present_detailed_fault_returns_None_for_other_fault(self):
+ self.assertIsNone(
+ present_detailed_user_friendly_fault(Fault(9999, "!!!")))
+
+ def test_present_user_friendly_fault_describes_pserv_fault(self):
+ self.assertIn(
+ "provisioning server",
+ present_user_friendly_fault(Fault(8002, 'error')).message)
+
+ def test_present_user_friendly_fault_covers_all_pserv_faults(self):
+ all_pserv_faults = set(map_enum(PSERV_FAULT).values())
+ presentable_pserv_faults = set(SHORT_PRESENTATIONS.keys())
+ self.assertItemsEqual([], all_pserv_faults - presentable_pserv_faults)
+
+ def test_present_user_friendly_fault_rerepresents_all_pserv_faults(self):
+ fault_string = factory.getRandomString()
+ for fault_code in map_enum(PSERV_FAULT).values():
+ original_fault = Fault(fault_code, fault_string)
+ new_fault = present_user_friendly_fault(original_fault)
+ self.assertNotEqual(fault_string, new_fault.message)
+
+ def test_present_user_friendly_fault_describes_cobbler_fault(self):
+ friendly_fault = present_user_friendly_fault(
+ Fault(PSERV_FAULT.NO_COBBLER, factory.getRandomString()))
+ friendly_text = friendly_fault.message
+ self.assertIn("Unable to reach the Cobbler server", friendly_text)
+
+ def test_present_user_friendly_fault_describes_cobbler_auth_fail(self):
+ friendly_fault = present_user_friendly_fault(
+ Fault(PSERV_FAULT.COBBLER_AUTH_FAILED, factory.getRandomString()))
+ friendly_text = friendly_fault.message
+ self.assertIn(
+ "Failed to authenticate with the Cobbler server", friendly_text)
+
+ def test_present_user_friendly_fault_describes_cobbler_auth_error(self):
+ friendly_fault = present_user_friendly_fault(
+ Fault(PSERV_FAULT.COBBLER_AUTH_ERROR, factory.getRandomString()))
+ friendly_text = friendly_fault.message
+ self.assertIn(
+ "Failed to authenticate with the Cobbler server", friendly_text)
+
+ def test_present_user_friendly_fault_describes_missing_profile(self):
+ profile = factory.getRandomString()
+ friendly_fault = present_user_friendly_fault(
+ Fault(
+ PSERV_FAULT.NO_SUCH_PROFILE,
+ "invalid profile name: %s" % profile))
+ friendly_text = friendly_fault.message
+ self.assertIn(profile, friendly_text)
+
+ def test_present_user_friendly_fault_describes_generic_cobbler_fail(self):
+ error_text = factory.getRandomString()
+ friendly_fault = present_user_friendly_fault(
+ Fault(PSERV_FAULT.GENERIC_COBBLER_ERROR, error_text))
+ friendly_text = friendly_fault.message
+ self.assertIn(
+ "Unknown problem encountered with the Cobbler server.",
+ friendly_text)
class ProvisioningTests:
@@ -131,7 +323,7 @@
def raise_missing_profile(*args, **kwargs):
raise Fault(PSERV_FAULT.NO_SUCH_PROFILE, "Unknown profile.")
- self.papi.patch('add_node', raise_missing_profile)
+ self.patch(self.papi.proxy, 'add_node', raise_missing_profile)
with ExpectedException(MAASAPIException):
node = factory.make_node(architecture='amd32k')
provisioning.provision_post_save_Node(
@@ -142,7 +334,7 @@
def raise_fault(*args, **kwargs):
raise Fault(PSERV_FAULT.NO_COBBLER, factory.getRandomString())
- self.papi.patch('add_node', raise_fault)
+ self.patch(self.papi.proxy, 'add_node', raise_fault)
with ExpectedException(MAASAPIException):
node = factory.make_node(architecture='amd32k')
provisioning.provision_post_save_Node(
@@ -217,114 +409,90 @@
node = self.papi.get_nodes_by_name(["frank"])["frank"]
self.assertEqual([], node["mac_addresses"])
- def test_metadata_server_url_refers_to_own_metadata_service(self):
- self.assertEqual(
- "%s/metadata/"
- % Config.objects.get_config('maas_url').rstrip('/'),
- get_metadata_server_url())
-
- def test_metadata_server_url_includes_script_name(self):
- self.patch(settings, "FORCE_SCRIPT_NAME", "/MAAS")
- self.assertEqual(
- "%s/MAAS/metadata/"
- % Config.objects.get_config('maas_url').rstrip('/'),
- get_metadata_server_url())
-
- def test_compose_metadata_includes_metadata_url(self):
- node = factory.make_node()
- self.assertEqual(
- get_metadata_server_url(),
- compose_metadata(node)['maas-metadata-url'])
-
- def test_compose_metadata_includes_node_oauth_token(self):
- node = factory.make_node()
- metadata = compose_metadata(node)
- token = NodeKey.objects.get_token_for_node(node)
- self.assertEqual({
- 'oauth_consumer_key': [token.consumer.key],
- 'oauth_token_key': [token.key],
- 'oauth_token_secret': [token.secret],
- },
- parse_qs(metadata['maas-metadata-credentials']))
-
def test_papi_xmlrpc_faults_are_reported_helpfully(self):
def raise_fault(*args, **kwargs):
raise Fault(8002, factory.getRandomString())
- self.papi.patch('add_node', raise_fault)
+ self.patch(self.papi.proxy, 'add_node', raise_fault)
with ExpectedException(MAASAPIException, ".*provisioning server.*"):
- self.papi.add_node('node', 'profile', 'power', {})
+ self.papi.add_node('node', 'profile', 'power', '')
def test_provisioning_errors_are_reported_helpfully(self):
def raise_provisioning_error(*args, **kwargs):
raise Fault(PSERV_FAULT.NO_COBBLER, factory.getRandomString())
- self.papi.patch('add_node', raise_provisioning_error)
+ self.patch(self.papi.proxy, 'add_node', raise_provisioning_error)
with ExpectedException(MAASAPIException, ".*Cobbler.*"):
- self.papi.add_node('node', 'profile', 'power', {})
+ self.papi.add_node('node', 'profile', 'power', '')
- def test_present_user_friendly_fault_describes_pserv_fault(self):
- self.assertIn(
- "provisioning server",
- present_user_friendly_fault(Fault(8002, 'error')).message)
-
- def test_present_user_friendly_fault_covers_all_pserv_faults(self):
- all_pserv_faults = set(map_enum(PSERV_FAULT).values())
- presentable_pserv_faults = set(PRESENTATIONS.keys())
- self.assertItemsEqual([], all_pserv_faults - presentable_pserv_faults)
-
- def test_present_user_friendly_fault_rerepresents_all_pserv_faults(self):
- fault_string = factory.getRandomString()
- for fault_code in map_enum(PSERV_FAULT).values():
- original_fault = Fault(fault_code, fault_string)
- new_fault = present_user_friendly_fault(original_fault)
- self.assertNotEqual(fault_string, new_fault.message)
-
- def test_present_user_friendly_fault_describes_cobbler_fault(self):
- friendly_fault = present_user_friendly_fault(
- Fault(PSERV_FAULT.NO_COBBLER, factory.getRandomString()))
- friendly_text = friendly_fault.message
- self.assertIn("unable to reach", friendly_text)
- self.assertIn("Cobbler", friendly_text)
-
- def test_present_user_friendly_fault_describes_cobbler_auth_fail(self):
- friendly_fault = present_user_friendly_fault(
- Fault(PSERV_FAULT.COBBLER_AUTH_FAILED, factory.getRandomString()))
- friendly_text = friendly_fault.message
- self.assertIn("failed to authenticate", friendly_text)
- self.assertIn("Cobbler", friendly_text)
-
- def test_present_user_friendly_fault_describes_cobbler_auth_error(self):
- friendly_fault = present_user_friendly_fault(
- Fault(PSERV_FAULT.COBBLER_AUTH_ERROR, factory.getRandomString()))
- friendly_text = friendly_fault.message
- self.assertIn("authentication token", friendly_text)
- self.assertIn("Cobbler", friendly_text)
+ def patch_and_call_papi_method(self, fault_code, papi_method='add_node'):
+ # Patch papi method to make it raise a Fault of the provided
+ # fault_code. Then call this method.
+ def raise_provisioning_error(*args, **kwargs):
+ raise Fault(fault_code, factory.getRandomString())
- def test_present_user_friendly_fault_describes_missing_profile(self):
- profile = factory.getRandomString()
- friendly_fault = present_user_friendly_fault(
- Fault(
- PSERV_FAULT.NO_SUCH_PROFILE,
- "invalid profile name: %s" % profile))
- friendly_text = friendly_fault.message
- self.assertIn(profile, friendly_text)
- self.assertIn("maas-import-isos", friendly_text)
+ self.patch(self.papi.proxy, papi_method, raise_provisioning_error)
- def test_present_user_friendly_fault_describes_generic_cobbler_fail(self):
- error_text = factory.getRandomString()
- friendly_fault = present_user_friendly_fault(
- Fault(PSERV_FAULT.GENERIC_COBBLER_ERROR, error_text))
- friendly_text = friendly_fault.message
- self.assertIn("Cobbler", friendly_text)
- self.assertIn(error_text, friendly_text)
+ try:
+ method = getattr(self.papi, papi_method)
+ method()
+ except MAASAPIException:
+ pass
+
+ def test_error_registered_when_NO_COBBLER_raised(self):
+ self.patch(components, '_PERSISTENT_ERRORS', {})
+ self.patch_and_call_papi_method(PSERV_FAULT.NO_COBBLER)
+ errors = get_persistent_errors()
+ self.assertEqual(1, len(errors))
+ self.assertIn(
+ "The provisioning server was unable to reach the Cobbler",
+ errors[0])
- def test_present_user_friendly_fault_returns_None_for_other_fault(self):
- self.assertIsNone(present_user_friendly_fault(Fault(9999, "!!!")))
+ def test_error_registered_can_handle_all_the_exceptions(self):
+ for fault_code in map_enum(PSERV_FAULT).values():
+ self.patch(components, '_PERSISTENT_ERRORS', {})
+ self.patch_and_call_papi_method(fault_code)
+ errors = get_persistent_errors()
+ self.assertEqual(1, len(errors))
+
+ def test_failing_components_cleared_if_add_node_works(self):
+ self.patch(components, '_PERSISTENT_ERRORS', {})
+ register_persistent_error(COMPONENT.PSERV, factory.getRandomString())
+ register_persistent_error(COMPONENT.COBBLER, factory.getRandomString())
+ register_persistent_error(
+ COMPONENT.IMPORT_ISOS, factory.getRandomString())
+ self.papi.add_node('node', 'hostname', 'profile', 'power', '')
+ self.assertEqual([], get_persistent_errors())
+
+ def test_only_failing_components_are_cleared_if_modify_nodes_works(self):
+ # Only the components listed in METHOD_COMPONENTS[method_name]
+ # are cleared with the run of method_name is successfull.
+ self.patch(components, '_PERSISTENT_ERRORS', {})
+ other_error = factory.getRandomString()
+ other_component = factory.getRandomString()
+ register_persistent_error(other_component, other_error)
+ self.papi.modify_nodes({})
+ self.assertEqual([other_error], get_persistent_errors())
+
+ def test_failing_components_cleared_if_modify_nodes_works(self):
+ self.patch(components, '_PERSISTENT_ERRORS', {})
+ register_persistent_error(COMPONENT.PSERV, factory.getRandomString())
+ register_persistent_error(COMPONENT.COBBLER, factory.getRandomString())
+ self.papi.modify_nodes({})
+ self.assertEqual([], get_persistent_errors())
+
+ def test_failing_components_cleared_if_delete_nodes_by_name_works(self):
+ self.patch(components, '_PERSISTENT_ERRORS', {})
+ register_persistent_error(COMPONENT.PSERV, factory.getRandomString())
+ register_persistent_error(COMPONENT.COBBLER, factory.getRandomString())
+ other_error = factory.getRandomString()
+ register_persistent_error(factory.getRandomString(), other_error)
+ self.papi.delete_nodes_by_name([])
+ self.assertEqual([other_error], get_persistent_errors())
class TestProvisioningWithFake(ProvisioningTests, ProvisioningFakeFactory,
@@ -336,3 +504,71 @@
def setUp(self):
super(TestProvisioningWithFake, self).setUp()
self.papi = provisioning.get_provisioning_api_proxy()
+
+ def test_provision_post_save_Node_set_netboot_enabled(self):
+ # When a node is under MAAS's control - i.e. not allocated and not
+ # retired - it is always configured for netbooting. When the node is
+ # allocated, netbooting is left alone; its state may change in
+ # response to interactions between the node and the provisioning
+ # server and MAAS ought to leave that alone. When the node is retired
+ # netbooting is disabled.
+ expected = {
+ NODE_STATUS.DECLARED: False,
+ NODE_STATUS.COMMISSIONING: True,
+ NODE_STATUS.FAILED_TESTS: True,
+ NODE_STATUS.MISSING: True,
+ NODE_STATUS.READY: True,
+ NODE_STATUS.RESERVED: True,
+ NODE_STATUS.ALLOCATED: None, # No setting.
+ NODE_STATUS.RETIRED: False,
+ }
+ nodes = {
+ status: factory.make_node(status=status)
+ for status, title in NODE_STATUS_CHOICES
+ }
+ pserv_nodes = {
+ status: node.system_id
+ for status, node in nodes.items()
+ }
+ observed = {
+ status: self.papi.nodes[pserv_node].get("netboot_enabled")
+ for status, pserv_node in pserv_nodes.items()
+ }
+ self.assertEqual(expected, observed)
+
+ def test_commissioning_node_gets_commissioning_preseed(self):
+ node = factory.make_node(status=NODE_STATUS.DECLARED)
+ token = NodeKey.objects.get_token_for_node(node)
+ node.start_commissioning(factory.make_admin())
+ preseed = self.papi.nodes[node.system_id]['ks_meta']['MAAS_PRESEED']
+ self.assertEqual(
+ compose_commissioning_preseed(token), b64decode(preseed))
+
+ def test_non_commissioning_node_gets_cloud_init_preseed(self):
+ node = factory.make_node(status=NODE_STATUS.READY)
+ token = NodeKey.objects.get_token_for_node(node)
+ preseed = self.papi.nodes[node.system_id]['ks_meta']['MAAS_PRESEED']
+ self.assertEqual(
+ compose_cloud_init_preseed(token), b64decode(preseed))
+
+ def test_node_gets_cloud_init_preseed_after_commissioning(self):
+ node = factory.make_node(status=NODE_STATUS.DECLARED)
+ token = NodeKey.objects.get_token_for_node(node)
+ node.start_commissioning(factory.make_admin())
+ node.status = NODE_STATUS.READY
+ node.save()
+ preseed = self.papi.nodes[node.system_id]['ks_meta']['MAAS_PRESEED']
+ self.assertEqual(
+ compose_cloud_init_preseed(token), b64decode(preseed))
+
+
+class TestProvisioningTransport(TestCase):
+ """Tests for :class:`ProvisioningTransport`."""
+
+ def test_make_connection(self):
+ transport = ProvisioningTransport()
+ connection = transport.make_connection("nowhere.example.com")
+ # The connection has not yet been established.
+ self.assertIsNone(connection.sock)
+ # The desired timeout has been modified.
+ self.assertEqual(transport.timeout, connection.timeout)
diff -Nru maas-0.1+bzr415+dfsg/src/maasserver/tests/test_views.py maas-0.1+bzr462+dfsg/src/maasserver/tests/test_views.py
--- maas-0.1+bzr415+dfsg/src/maasserver/tests/test_views.py 2012-04-04 18:59:25.000000000 +0000
+++ maas-0.1+bzr462+dfsg/src/maasserver/tests/test_views.py 2012-04-12 20:11:10.000000000 +0000
@@ -15,20 +15,25 @@
import httplib
import os
import urllib2
+from xmlrpclib import Fault
from django.conf import settings
from django.conf.urls.defaults import patterns
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
+from django.utils.html import escape
from lxml.html import fromstring
from maasserver import (
+ components,
messages,
views,
)
+from maasserver.components import register_persistent_error
from maasserver.exceptions import NoRabbit
from maasserver.forms import NodeActionForm
from maasserver.models import (
Config,
+ Node,
NODE_AFTER_COMMISSIONING_ACTION,
NODE_STATUS,
POWER_TYPE_CHOICES,
@@ -39,6 +44,7 @@
get_data,
reload_object,
)
+from maasserver.testing.enum import map_enum
from maasserver.testing.factory import factory
from maasserver.testing.testcase import (
LoggedInTestCase,
@@ -54,6 +60,11 @@
proxy_to_longpoll,
)
from maastesting.rabbit import uses_rabbit_fixture
+from provisioningserver.enum import PSERV_FAULT
+from testtools.matchers import (
+ Contains,
+ MatchesAll,
+ )
def get_prefixed_form_data(prefix, data):
@@ -148,7 +159,8 @@
self.assertTemplateExistsAndContains(
response.content, '#add-node', 'input#id_hostname')
- def test_after_commissioning_action_snippet(self):
+ # XXX JeroenVermeulen 2012-04-12, bug=979539: re-enable.
+ def t_e_s_t_after_commissioning_action_snippet(self):
response = self.client.get('/')
self.assertTemplateExistsAndContains(
response.content, '#add-node',
@@ -386,17 +398,14 @@
add_key_link = reverse('prefs-add-sshkey')
self.assertIn(add_key_link, get_content_links(response))
- def create_keys_for_user(self, user):
- return [factory.make_sshkey(self.logged_in_user) for i in range(3)]
-
def test_prefs_displays_compact_representation_of_users_keys(self):
- keys = self.create_keys_for_user(self.logged_in_user)
+ _, keys = factory.make_user_with_keys(user=self.logged_in_user)
response = self.client.get('/account/prefs/')
for key in keys:
self.assertIn(key.display_html(), response.content)
def test_prefs_displays_link_to_delete_ssh_keys(self):
- keys = self.create_keys_for_user(self.logged_in_user)
+ _, keys = factory.make_user_with_keys(user=self.logged_in_user)
response = self.client.get('/account/prefs/')
links = get_content_links(response)
for key in keys:
@@ -419,13 +428,38 @@
'#content form')])
def test_add_key_POST_adds_key(self):
- key_string = get_data('data/test_rsa.pub')
+ key_string = get_data('data/test_rsa0.pub')
response = self.client.post(
reverse('prefs-add-sshkey'), {'key': key_string})
self.assertEqual(httplib.FOUND, response.status_code)
self.assertTrue(SSHKey.objects.filter(key=key_string).exists())
+ def test_add_key_POST_fails_if_key_already_exists_for_the_user(self):
+ key_string = get_data('data/test_rsa0.pub')
+ key = SSHKey(user=self.logged_in_user, key=key_string)
+ key.save()
+ response = self.client.post(
+ reverse('prefs-add-sshkey'), {'key': key_string})
+
+ self.assertEqual(httplib.OK, response.status_code)
+ self.assertIn(
+ "This key has already been added for this user.",
+ response.content)
+ self.assertItemsEqual([key], SSHKey.objects.filter(key=key_string))
+
+ def test_key_can_be_added_if_same_key_already_setup_for_other_user(self):
+ key_string = get_data('data/test_rsa0.pub')
+ key = SSHKey(user=factory.make_user(), key=key_string)
+ key.save()
+ response = self.client.post(
+ reverse('prefs-add-sshkey'), {'key': key_string})
+ new_key = SSHKey.objects.get(key=key_string, user=self.logged_in_user)
+
+ self.assertEqual(httplib.FOUND, response.status_code)
+ self.assertItemsEqual(
+ [key, new_key], SSHKey.objects.filter(key=key_string))
+
def test_delete_key_GET(self):
# The 'Delete key' page displays a confirmation page with a form.
key = factory.make_sshkey(self.logged_in_user)
@@ -468,10 +502,10 @@
self.logged_in_user.save()
-def get_content_links(response):
- """Extract links from :class:`HttpResponse` HTML body."""
+def get_content_links(response, element='#content'):
+ """Extract links from :class:`HttpResponse` #content element."""
doc = fromstring(response.content)
- [content_node] = doc.cssselect('#content')
+ [content_node] = doc.cssselect(element)
return [elem.get('href') for elem in content_node.cssselect('a')]
@@ -483,6 +517,24 @@
node_link = reverse('node-view', args=[node.system_id])
self.assertIn(node_link, get_content_links(response))
+ def test_node_list_displays_sorted_list_of_nodes(self):
+ # Nodes are sorted on the node list page, newest first.
+ nodes = [factory.make_node() for i in range(3)]
+ nodes.reverse()
+ # Modify one node to make sure that the default db ordering
+ # (by modification date) is not used.
+ node = nodes[1]
+ node.hostname = factory.getRandomString()
+ node.save()
+ response = self.client.get(reverse('node-list'))
+ node_links = [
+ reverse('node-view', args=[node.system_id])
+ for node in nodes]
+ self.assertEqual(
+ node_links,
+ [link for link in get_content_links(response)
+ if link.startswith('/nodes')])
+
def test_view_node_displays_node_info(self):
# The node page features the basic information about the node.
node = factory.make_node(owner=self.logged_in_user)
@@ -524,6 +576,20 @@
response = self.client.get(node_delete_link)
self.assertEqual(httplib.FORBIDDEN, response.status_code)
+ def test_view_node_shows_message_for_commissioning_node(self):
+ statuses_with_message = (
+ NODE_STATUS.READY, NODE_STATUS.COMMISSIONING)
+ help_link = "https://wiki.ubuntu.com/ServerTeam/MAAS/AvahiBoot"
+ for status in map_enum(NODE_STATUS).values():
+ node = factory.make_node(status=status)
+ node_link = reverse('node-view', args=[node.system_id])
+ response = self.client.get(node_link)
+ links = get_content_links(response, '#flash-messages')
+ if status in statuses_with_message:
+ self.assertIn(help_link, links)
+ else:
+ self.assertNotIn(help_link, links)
+
def test_view_node_shows_link_to_delete_node_for_admin(self):
self.become_admin()
node = factory.make_node()
@@ -538,7 +604,7 @@
node_delete_link = reverse('node-delete', args=[node.system_id])
response = self.client.post(node_delete_link, {'post': 'yes'})
self.assertEqual(httplib.FOUND, response.status_code)
- self.assertFalse(User.objects.filter(id=node.id).exists())
+ self.assertFalse(Node.objects.filter(id=node.id).exists())
def test_allocated_node_view_page_says_node_cannot_be_deleted(self):
self.become_admin()
@@ -600,8 +666,9 @@
node_edit_link = reverse('node-edit', args=[node.system_id])
params = {
'hostname': factory.getRandomString(),
- 'after_commissioning_action': factory.getRandomEnum(
- NODE_AFTER_COMMISSIONING_ACTION),
+ # XXX JeroenVermeulen 2012-04-12, bug=979539: re-enable.
+ #'after_commissioning_action': factory.getRandomEnum(
+ # NODE_AFTER_COMMISSIONING_ACTION),
}
response = self.client.post(node_edit_link, params)
@@ -609,35 +676,6 @@
self.assertEqual(httplib.FOUND, response.status_code)
self.assertAttributes(node, params)
- def test_view_node_admin_has_button_to_accept_enlistement(self):
- self.logged_in_user.is_superuser = True
- self.logged_in_user.save()
- node = factory.make_node(status=NODE_STATUS.DECLARED)
- node_link = reverse('node-view', args=[node.system_id])
- response = self.client.get(node_link)
- doc = fromstring(response.content)
- inputs = [
- input for input in doc.cssselect('form#node_actions input')
- if input.name == NodeActionForm.input_name]
-
- self.assertIn(
- "Accept Enlisted node", [input.value for input in inputs])
-
- def test_view_node_POST_admin_can_enlist_node(self):
- self.logged_in_user.is_superuser = True
- self.logged_in_user.save()
- node = factory.make_node(status=NODE_STATUS.DECLARED)
- node_link = reverse('node-view', args=[node.system_id])
- response = self.client.post(
- node_link,
- data={
- NodeActionForm.input_name: "Accept Enlisted node",
- })
-
- self.assertEqual(httplib.FOUND, response.status_code)
- self.assertEqual(
- NODE_STATUS.READY, reload_object(node).status)
-
def test_view_node_has_button_to_accept_enlistement_for_user(self):
# A simple user can't see the button to enlist a declared node.
node = factory.make_node(status=NODE_STATUS.DECLARED)
@@ -666,19 +704,55 @@
self.assertNotIn("Error output", content_text)
def test_view_node_POST_admin_can_start_commissioning_node(self):
- self.logged_in_user.is_superuser = True
- self.logged_in_user.save()
+ self.become_admin()
node = factory.make_node(status=NODE_STATUS.DECLARED)
node_link = reverse('node-view', args=[node.system_id])
response = self.client.post(
node_link,
data={
- NodeActionForm.input_name: "Commission node",
+ NodeActionForm.input_name: "Accept & commission",
})
self.assertEqual(httplib.FOUND, response.status_code)
self.assertEqual(
NODE_STATUS.COMMISSIONING, reload_object(node).status)
+ def perform_action_and_get_node_page(self, node, action_name):
+ node_link = reverse('node-view', args=[node.system_id])
+ self.client.post(
+ node_link,
+ data={
+ NodeActionForm.input_name: action_name,
+ })
+ response = self.client.get(node_link)
+ return response
+
+ def test_start_commisionning_displays_message(self):
+ self.become_admin()
+ node = factory.make_node(status=NODE_STATUS.DECLARED)
+ response = self.perform_action_and_get_node_page(
+ node, "Accept & commission")
+ self.assertIn(
+ "Node commissioning started.",
+ [message.message for message in response.context['messages']])
+
+ def test_start_node_from_ready_displays_message(self):
+ node = factory.make_node(
+ status=NODE_STATUS.READY, owner=self.logged_in_user)
+ response = self.perform_action_and_get_node_page(
+ node, "Start node")
+ self.assertIn(
+ "Node started.",
+ [message.message for message in response.context['messages']])
+
+ def test_start_node_from_allocated_displays_message(self):
+ node = factory.make_node(
+ status=NODE_STATUS.ALLOCATED, owner=self.logged_in_user)
+ response = self.perform_action_and_get_node_page(
+ node, "Start node")
+ self.assertEqual(
+ ["Node started."],
+ [message.message for message in response.context['messages']])
+
class AdminNodeViewsTest(AdminLoggedInTestCase):
@@ -687,8 +761,9 @@
node_edit_link = reverse('node-edit', args=[node.system_id])
params = {
'hostname': factory.getRandomString(),
- 'after_commissioning_action': factory.getRandomEnum(
- NODE_AFTER_COMMISSIONING_ACTION),
+ # XXX JeroenVermeulen 2012-04-12, bug=979539: re-enable.
+ #'after_commissioning_action': factory.getRandomEnum(
+ # NODE_AFTER_COMMISSIONING_ACTION),
'power_type': factory.getRandomChoice(POWER_TYPE_CHOICES),
}
response = self.client.post(node_edit_link, params)
@@ -940,3 +1015,32 @@
content_text = doc.cssselect('#content')[0].text_content()
self.assertIn(user.username, content_text)
self.assertIn(user.email, content_text)
+
+
+class PermanentErrorDisplayTest(LoggedInTestCase):
+
+ def test_permanent_error_displayed(self):
+ self.patch(components, '_PERSISTENT_ERRORS', {})
+ pserv_fault = set(map_enum(PSERV_FAULT).values())
+ errors = []
+ for fault in pserv_fault:
+ # Create component with getRandomString to be sure
+ # to display all the errors.
+ component = factory.getRandomString()
+ error_message = factory.getRandomString()
+ error = Fault(fault, error_message)
+ errors.append(error)
+ register_persistent_error(component, error_message)
+ links = [
+ reverse('index'),
+ reverse('node-list'),
+ reverse('prefs'),
+ ]
+ for link in links:
+ response = self.client.get(link)
+ self.assertThat(
+ response.content,
+ MatchesAll(
+ *[Contains(
+ escape(error.faultString))
+ for error in errors]))
diff -Nru maas-0.1+bzr415+dfsg/src/maasserver/views.py maas-0.1+bzr462+dfsg/src/maasserver/views.py
--- maas-0.1+bzr415+dfsg/src/maasserver/views.py 2012-04-04 18:59:25.000000000 +0000
+++ maas-0.1+bzr462+dfsg/src/maasserver/views.py 2012-04-12 20:11:10.000000000 +0000
@@ -31,6 +31,7 @@
import mimetypes
import os
import urllib2
+from django.utils.safestring import mark_safe
from convoy.combo import (
combine_files,
@@ -110,6 +111,17 @@
return dj_logout(request, next_page=reverse('login'))
+# Info message displayed on the node page for COMMISSIONING
+# or READY nodes.
+NODE_BOOT_INFO = mark_safe("""
+You can boot this node using Avahi enabled boot media or an
+adequately configured dhcp server, see
+
+https://wiki.ubuntu.com/ServerTeam/MAAS/AvahiBoot for
+details.
+""")
+
+
class NodeView(UpdateView):
template_name = 'maasserver/node_view.html'
@@ -124,7 +136,7 @@
return node
def get_form_class(self):
- return get_action_form(self.request.user)
+ return get_action_form(self.request.user, self.request)
def get_context_data(self, **kwargs):
context = super(NodeView, self).get_context_data(**kwargs)
@@ -133,6 +145,8 @@
NODE_PERMISSION.EDIT, node)
context['can_delete'] = self.request.user.has_perm(
NODE_PERMISSION.ADMIN, node)
+ if node.status in (NODE_STATUS.COMMISSIONING, NODE_STATUS.READY):
+ messages.info(self.request, NODE_BOOT_INFO)
return context
def get_success_url(self):
@@ -205,8 +219,10 @@
context_object_name = "node_list"
def get_queryset(self):
+ # Return node list sorted, newest first.
return Node.objects.get_nodes(
- user=self.request.user, perm=NODE_PERMISSION.VIEW)
+ user=self.request.user,
+ perm=NODE_PERMISSION.VIEW).order_by('-id')
def get_context_data(self, **kwargs):
context = super(NodeListView, self).get_context_data(**kwargs)
diff -Nru maas-0.1+bzr415+dfsg/src/metadataserver/api.py maas-0.1+bzr462+dfsg/src/metadataserver/api.py
--- maas-0.1+bzr415+dfsg/src/metadataserver/api.py 2012-04-04 18:59:25.000000000 +0000
+++ maas-0.1+bzr462+dfsg/src/metadataserver/api.py 2012-04-12 20:11:10.000000000 +0000
@@ -36,6 +36,7 @@
SSHKey,
)
from metadataserver.models import (
+ NodeCommissionResult,
NodeKey,
NodeUserData,
)
@@ -126,6 +127,12 @@
shown_fields.remove('user-data')
return make_list_response(sorted(shown_fields))
+ def _store_commissioning_results(self, node, request):
+ """Store commissioning result files for `node`."""
+ for name, uploaded_file in request.FILES.items():
+ contents = uploaded_file.read().decode('utf-8')
+ NodeCommissionResult.objects.store_data(node, name, contents)
+
@api_exported('signal', 'POST')
def signal(self, request, version=None):
"""Signal commissioning status.
@@ -161,6 +168,8 @@
# Already registered. Nothing to be done.
return rc.ALL_OK
+ self._store_commissioning_results(node, request)
+
target_status = self.signaling_statuses.get(status)
if target_status in (None, node.status):
# No status change. Nothing to be done.
diff -Nru maas-0.1+bzr415+dfsg/src/metadataserver/migrations/0002_add_nodecommissionresult.py maas-0.1+bzr462+dfsg/src/metadataserver/migrations/0002_add_nodecommissionresult.py
--- maas-0.1+bzr415+dfsg/src/metadataserver/migrations/0002_add_nodecommissionresult.py 1970-01-01 00:00:00.000000000 +0000
+++ maas-0.1+bzr462+dfsg/src/metadataserver/migrations/0002_add_nodecommissionresult.py 2012-04-12 20:11:10.000000000 +0000
@@ -0,0 +1,137 @@
+# 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 model 'NodeCommissionResult'
+ db.create_table('metadataserver_nodecommissionresult', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('node', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['maasserver.Node'])),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=100)),
+ ('data', self.gf('django.db.models.fields.CharField')(max_length=1048576)),
+ ))
+ db.send_create_signal('metadataserver', ['NodeCommissionResult'])
+
+ # Adding unique constraint on 'NodeCommissionResult', fields ['node', 'name']
+ db.create_unique('metadataserver_nodecommissionresult', ['node_id', 'name'])
+
+
+ def backwards(self, orm):
+
+ # Removing unique constraint on 'NodeCommissionResult', fields ['node', 'name']
+ db.delete_unique('metadataserver_nodecommissionresult', ['node_id', 'name'])
+
+ # Deleting model 'NodeCommissionResult'
+ db.delete_table('metadataserver_nodecommissionresult')
+
+
+ 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.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-b9edb888-839c-11e1-965e-002215205ce8'", 'unique': 'True', 'max_length': '41'}),
+ 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'null': 'True'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ 'metadataserver.nodecommissionresult': {
+ 'Meta': {'unique_together': "((u'node', u'name'),)", 'object_name': 'NodeCommissionResult'},
+ 'data': ('django.db.models.fields.CharField', [], {'max_length': '1048576'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['maasserver.Node']"})
+ },
+ 'metadataserver.nodekey': {
+ 'Meta': {'object_name': 'NodeKey'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '18'}),
+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['maasserver.Node']", 'unique': 'True'}),
+ 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'unique': 'True'})
+ },
+ 'metadataserver.nodeuserdata': {
+ 'Meta': {'object_name': 'NodeUserData'},
+ 'data': ('metadataserver.fields.BinaryField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['maasserver.Node']", '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': '1334124495L'}),
+ '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 = ['metadataserver']
diff -Nru maas-0.1+bzr415+dfsg/src/metadataserver/models.py maas-0.1+bzr462+dfsg/src/metadataserver/models.py
--- maas-0.1+bzr415+dfsg/src/metadataserver/models.py 2012-04-04 18:59:25.000000000 +0000
+++ maas-0.1+bzr462+dfsg/src/metadataserver/models.py 2012-04-12 20:11:10.000000000 +0000
@@ -20,6 +20,7 @@
Manager,
Model,
)
+from django.shortcuts import get_object_or_404
from maasserver.models import (
create_auth_token,
Node,
@@ -177,3 +178,46 @@
node = ForeignKey(Node, null=False, editable=False, unique=True)
data = BinaryField(null=False)
+
+
+class NodeCommissionResultManager(Manager):
+ """Utility to manage a collection of :class:`NodeCommissionResult`s."""
+
+ def clear_results(self, node):
+ """Remove all existing results for a node."""
+ self.filter(node=node).delete()
+
+ def store_data(self, node, name, data):
+ """Store data about a node."""
+ existing, created = self.get_or_create(
+ node=node, name=name, defaults=dict(data=data))
+ if not created:
+ existing.data = data
+ existing.save()
+
+ def get_data(self, node, name):
+ """Get data about a node."""
+ ncr = get_object_or_404(NodeCommissionResult, node=node, name=name)
+ return ncr.data
+
+
+class NodeCommissionResult(Model):
+ """Storage for data returned from node commissioning.
+
+ Commissioning a node results in various bits of data that need to be
+ stored, such as lshw output. This model allows storing of this data
+ as unicode text, with an arbitrary name, for later retrieval.
+
+ :ivar node: The context :class:`Node`.
+ :ivar name: A unique name to use for the data being stored.
+ :ivar data: The file's actual data, unicode only.
+ """
+
+ objects = NodeCommissionResultManager()
+
+ node = ForeignKey(Node, null=False, editable=False, unique=False)
+ name = CharField(max_length=100, unique=False, editable=False)
+ data = CharField(max_length=1024 * 1024, editable=True)
+
+ class Meta:
+ unique_together = ('node', 'name')
diff -Nru maas-0.1+bzr415+dfsg/src/metadataserver/tests/test_api.py maas-0.1+bzr462+dfsg/src/metadataserver/tests/test_api.py
--- maas-0.1+bzr415+dfsg/src/metadataserver/tests/test_api.py 2012-04-04 18:59:25.000000000 +0000
+++ maas-0.1+bzr462+dfsg/src/metadataserver/tests/test_api.py 2012-04-12 20:11:10.000000000 +0000
@@ -13,10 +13,13 @@
from collections import namedtuple
import httplib
-from textwrap import dedent
+from io import BytesIO
from maasserver.exceptions import Unauthorized
-from maasserver.models import NODE_STATUS
+from maasserver.models import (
+ NODE_STATUS,
+ SSHKey,
+ )
from maasserver.provisioning import get_provisioning_api_proxy
from maasserver.testing import reload_object
from maasserver.testing.factory import factory
@@ -31,6 +34,7 @@
UnknownMetadataVersion,
)
from metadataserver.models import (
+ NodeCommissionResult,
NodeKey,
NodeUserData,
)
@@ -118,6 +122,30 @@
client = self.client
return client.get(self.make_url(path))
+ def call_signal(self, client=None, version='latest', files={}, **kwargs):
+ """Call the API's signal method.
+
+ :param client: Optional client to POST with. If omitted, will create
+ one for a commissioning node.
+ :param version: API version to post on. Defaults to "latest".
+ :param files: Optional dict of files to attach. Maps file name to
+ file contents.
+ :param **kwargs: Any other keyword parameters are passed on directly
+ to the "signal" call.
+ """
+ if client is None:
+ client = self.make_node_client(factory.make_node(
+ status=NODE_STATUS.COMMISSIONING))
+ params = {
+ 'op': 'signal',
+ 'status': 'OK',
+ }
+ params.update(kwargs)
+ for name, content in files.items():
+ params[name] = BytesIO(content)
+ params[name].name = name
+ return client.post(self.make_url('/%s/' % version), params)
+
def test_no_anonymous_access(self):
self.assertEqual(httplib.UNAUTHORIZED, self.get('/').status_code)
@@ -151,7 +179,7 @@
def test_meta_data_view_lists_fields(self):
# Some fields only are returned if there is data related to them.
- user = factory.make_user_with_keys(n_keys=2, username='my-user')
+ user, _ = factory.make_user_with_keys(n_keys=2, username='my-user')
node = factory.make_node(owner=user)
client = self.make_node_client(node=node)
response = self.get('/latest/meta-data/', client)
@@ -214,7 +242,7 @@
'public-keys', response.content.decode('ascii').split('\n'))
def test_public_keys_listed_for_node_with_public_keys(self):
- user = factory.make_user_with_keys(n_keys=2, username='my-user')
+ user, _ = factory.make_user_with_keys(n_keys=2, username='my-user')
node = factory.make_node(owner=user)
response = self.get(
'/latest/meta-data/', self.make_node_client(node=node))
@@ -227,22 +255,22 @@
self.assertEqual(httplib.NOT_FOUND, response.status_code)
def test_public_keys_for_node_returns_list_of_keys(self):
- user = factory.make_user_with_keys(n_keys=2, username='my-user')
+ user, _ = factory.make_user_with_keys(n_keys=2, username='my-user')
node = factory.make_node(owner=user)
response = self.get(
'/latest/meta-data/public-keys', self.make_node_client(node=node))
self.assertEqual(httplib.OK, response.status_code)
- self.assertEquals(dedent("""\
- ssh-rsa KEY my-user-key-0
- ssh-rsa KEY my-user-key-1"""),
+ keys = SSHKey.objects.filter(user=user).values_list('key', flat=True)
+ expected_response = '\n'.join(keys)
+ self.assertItemsEqual(
+ expected_response,
response.content.decode('ascii'))
self.assertIn('text/plain', response['Content-Type'])
def test_other_user_than_node_cannot_signal_commissioning_result(self):
node = factory.make_node(status=NODE_STATUS.COMMISSIONING)
client = OAuthAuthenticatedClient(factory.make_user())
- response = client.post(
- self.make_url('/latest/'), {'op': 'signal', 'status': 'OK'})
+ response = self.call_signal(client)
self.assertEqual(httplib.FORBIDDEN, response.status_code)
self.assertEqual(
NODE_STATUS.COMMISSIONING, reload_object(node).status)
@@ -251,8 +279,7 @@
node = factory.make_node(status=NODE_STATUS.COMMISSIONING)
client = self.make_node_client(
node=factory.make_node(status=NODE_STATUS.COMMISSIONING))
- response = client.post(
- self.make_url('/latest/'), {'op': 'signal', 'status': 'OK'})
+ response = self.call_signal(client, status='OK')
self.assertEqual(httplib.OK, response.status_code)
self.assertEqual(
NODE_STATUS.COMMISSIONING, reload_object(node).status)
@@ -264,18 +291,13 @@
self.assertEqual(httplib.BAD_REQUEST, response.status_code)
def test_signaling_rejects_unknown_status_code(self):
- node = factory.make_node(status=NODE_STATUS.COMMISSIONING)
- client = self.make_node_client(node=node)
- response = client.post(
- self.make_url('/latest/'),
- {'op': 'signal', 'status': factory.getRandomString()})
+ response = self.call_signal(status=factory.getRandomString())
self.assertEqual(httplib.BAD_REQUEST, response.status_code)
def test_signaling_refuses_if_node_in_unexpected_state(self):
node = factory.make_node(status=NODE_STATUS.DECLARED)
client = self.make_node_client(node=node)
- response = client.post(
- self.make_url('/latest/'), {'op': 'signal', 'status': 'OK'})
+ response = self.call_signal(client)
self.assertEqual(
(
httplib.CONFLICT,
@@ -286,8 +308,7 @@
def test_signaling_accepts_WORKING_status(self):
node = factory.make_node(status=NODE_STATUS.COMMISSIONING)
client = self.make_node_client(node=node)
- response = client.post(
- self.make_url('/latest/'), {'op': 'signal', 'status': 'WORKING'})
+ response = self.call_signal(client, status='WORKING')
self.assertEqual(httplib.OK, response.status_code)
self.assertEqual(
NODE_STATUS.COMMISSIONING, reload_object(node).status)
@@ -295,8 +316,7 @@
def test_signaling_commissioning_success_makes_node_Ready(self):
node = factory.make_node(status=NODE_STATUS.COMMISSIONING)
client = self.make_node_client(node=node)
- response = client.post(
- self.make_url('/latest/'), {'op': 'signal', 'status': 'OK'})
+ response = self.call_signal(client, status='OK')
self.assertEqual(httplib.OK, response.status_code)
self.assertEqual(NODE_STATUS.READY, reload_object(node).status)
@@ -310,8 +330,7 @@
node.save()
papi.modify_nodes({node.system_id: {'profile': commissioning_profile}})
client = self.make_node_client(node=node)
- response = client.post(
- self.make_url('/latest/'), {'op': 'signal', 'status': 'OK'})
+ response = self.call_signal(client, status='OK')
self.assertEqual(httplib.OK, response.status_code)
node_data = papi.get_nodes_by_name([node.system_id])[node.system_id]
self.assertEqual(original_profile, node_data['profile'])
@@ -319,28 +338,23 @@
def test_signaling_commissioning_success_is_idempotent(self):
node = factory.make_node(status=NODE_STATUS.COMMISSIONING)
client = self.make_node_client(node=node)
- url = self.make_url('/latest/')
- success_params = {'op': 'signal', 'status': 'OK'}
- client.post(url, success_params)
- response = client.post(url, success_params)
+ self.call_signal(client, status='OK')
+ response = self.call_signal(client, status='OK')
self.assertEqual(httplib.OK, response.status_code)
self.assertEqual(NODE_STATUS.READY, reload_object(node).status)
def test_signaling_commissioning_failure_makes_node_Failed_Tests(self):
node = factory.make_node(status=NODE_STATUS.COMMISSIONING)
client = self.make_node_client(node=node)
- response = client.post(
- self.make_url('/latest/'), {'op': 'signal', 'status': 'FAILED'})
+ response = self.call_signal(client, status='FAILED')
self.assertEqual(httplib.OK, response.status_code)
self.assertEqual(NODE_STATUS.FAILED_TESTS, reload_object(node).status)
def test_signaling_commissioning_failure_is_idempotent(self):
node = factory.make_node(status=NODE_STATUS.COMMISSIONING)
client = self.make_node_client(node=node)
- url = self.make_url('/latest/')
- failure_params = {'op': 'signal', 'status': 'FAILED'}
- client.post(url, failure_params)
- response = client.post(url, failure_params)
+ self.call_signal(client, status='FAILED')
+ response = self.call_signal(client, status='FAILED')
self.assertEqual(httplib.OK, response.status_code)
self.assertEqual(NODE_STATUS.FAILED_TESTS, reload_object(node).status)
@@ -348,13 +362,7 @@
node = factory.make_node(status=NODE_STATUS.COMMISSIONING)
client = self.make_node_client(node=node)
error_text = factory.getRandomString()
- response = client.post(
- self.make_url('/latest/'),
- {
- 'op': 'signal',
- 'status': 'FAILED',
- 'error': error_text,
- })
+ response = self.call_signal(client, status='FAILED', error=error_text)
self.assertEqual(httplib.OK, response.status_code)
self.assertEqual(error_text, reload_object(node).error)
@@ -362,7 +370,74 @@
node = factory.make_node(
status=NODE_STATUS.COMMISSIONING, error=factory.getRandomString())
client = self.make_node_client(node=node)
- response = client.post(
- self.make_url('/latest/'), {'op': 'signal', 'status': 'OK'})
+ response = self.call_signal(client)
self.assertEqual(httplib.OK, response.status_code)
self.assertEqual('', reload_object(node).error)
+
+ def test_signalling_stores_files_for_any_status(self):
+ statuses = ['WORKING', 'OK', 'FAILED']
+ filename = factory.getRandomString()
+ nodes = {
+ status: factory.make_node(status=NODE_STATUS.COMMISSIONING)
+ for status in statuses}
+ for status, node in nodes.items():
+ client = self.make_node_client(node=node)
+ self.call_signal(
+ client, status=status,
+ files={filename: factory.getRandomString().encode('ascii')})
+ self.assertEqual(
+ {status: filename for status in statuses},
+ {
+ status: NodeCommissionResult.objects.get(node=node).name
+ for status, node in nodes.items()})
+
+ def test_signal_stores_file_contents(self):
+ node = factory.make_node(status=NODE_STATUS.COMMISSIONING)
+ client = self.make_node_client(node=node)
+ text = factory.getRandomString().encode('ascii')
+ response = self.call_signal(client, files={'file.txt': text})
+ self.assertEqual(httplib.OK, response.status_code)
+ self.assertEqual(
+ text, NodeCommissionResult.objects.get_data(node, 'file.txt'))
+
+ def test_signal_decodes_file_from_UTF8(self):
+ unicode_text = '<\u2621>'
+ node = factory.make_node(status=NODE_STATUS.COMMISSIONING)
+ client = self.make_node_client(node=node)
+ response = self.call_signal(
+ client, files={'file.txt': unicode_text.encode('utf-8')})
+ self.assertEqual(httplib.OK, response.status_code)
+ self.assertEqual(
+ unicode_text,
+ NodeCommissionResult.objects.get_data(node, 'file.txt'))
+
+ def test_signal_stores_multiple_files(self):
+ contents = {
+ factory.getRandomString(): factory.getRandomString().encode(
+ 'ascii')
+ for counter in range(3)}
+ node = factory.make_node(status=NODE_STATUS.COMMISSIONING)
+ client = self.make_node_client(node=node)
+ response = self.call_signal(client, files=contents)
+ self.assertEqual(httplib.OK, response.status_code)
+ self.assertEqual(
+ contents,
+ {
+ result.name: result.data
+ for result in node.nodecommissionresult_set.all()
+ })
+
+ def test_signal_stores_files_up_to_documented_size_limit(self):
+ # The documented size limit for commissioning result files:
+ # one megabyte. What happens above this limit is none of
+ # anybody's business, but files up to this size should work.
+ size_limit = 2 ** 20
+ contents = factory.getRandomString(size_limit, spaces=True)
+ node = factory.make_node(status=NODE_STATUS.COMMISSIONING)
+ client = self.make_node_client(node=node)
+ response = self.call_signal(
+ client, files={'output.txt': contents.encode('utf-8')})
+ self.assertEqual(httplib.OK, response.status_code)
+ stored_data = NodeCommissionResult.objects.get_data(
+ node, 'output.txt')
+ self.assertEqual(size_limit, len(stored_data))
diff -Nru maas-0.1+bzr415+dfsg/src/metadataserver/tests/test_models.py maas-0.1+bzr462+dfsg/src/metadataserver/tests/test_models.py
--- maas-0.1+bzr415+dfsg/src/metadataserver/tests/test_models.py 2012-04-04 18:59:25.000000000 +0000
+++ maas-0.1+bzr462+dfsg/src/metadataserver/tests/test_models.py 2012-04-12 20:11:10.000000000 +0000
@@ -11,9 +11,12 @@
__metaclass__ = type
__all__ = []
+from django.db import IntegrityError
+from django.http import Http404
from maasserver.testing.factory import factory
from maastesting.testcase import TestCase
from metadataserver.models import (
+ NodeCommissionResult,
NodeKey,
NodeUserData,
)
@@ -131,3 +134,94 @@
node = factory.make_node()
NodeUserData.objects.set_user_data(node, b"This node has user data.")
self.assertTrue(NodeUserData.objects.has_user_data(node))
+
+
+class TestNodeCommissionResult(TestCase):
+ """Test the NodeCommissionResult model."""
+
+ def test_can_store_data(self):
+ node = factory.make_node()
+ name = factory.getRandomString(100)
+ data = factory.getRandomString(1025)
+ factory.make_node_commission_result(node=node, name=name, data=data)
+
+ ncr = NodeCommissionResult.objects.get(name=name)
+ self.assertAttributes(ncr, dict(node=node, data=data))
+
+ def test_node_name_uniqueness(self):
+ # You cannot have two result rows with the same name for the
+ # same node.
+ node = factory.make_node()
+ factory.make_node_commission_result(node=node, name="foo")
+ self.assertRaises(
+ IntegrityError,
+ factory.make_node_commission_result, node=node, name="foo")
+
+ def test_different_nodes_can_have_same_data_name(self):
+ node = factory.make_node()
+ ncr1 = factory.make_node_commission_result(node=node, name="foo")
+ node2 = factory.make_node()
+ ncr2 = factory.make_node_commission_result(node=node2, name="foo")
+ self.assertEqual(ncr1.name, ncr2.name)
+
+
+class TestNodeCommissionResultManager(TestCase):
+ """Test the manager utility for NodeCommissionResult."""
+
+ def test_clear_results_removes_rows(self):
+ # clear_results should remove all a node's results.
+ node = factory.make_node()
+ factory.make_node_commission_result(node=node)
+ factory.make_node_commission_result(node=node)
+ factory.make_node_commission_result(node=node)
+
+ NodeCommissionResult.objects.clear_results(node)
+ self.assertItemsEqual(
+ [],
+ NodeCommissionResult.objects.filter(node=node))
+
+ def test_clear_results_ignores_other_nodes(self):
+ # clear_results should only remove results for the supplied
+ # node.
+ node1 = factory.make_node()
+ factory.make_node_commission_result(node=node1)
+ node2 = factory.make_node()
+ factory.make_node_commission_result(node=node2)
+
+ NodeCommissionResult.objects.clear_results(node1)
+ self.assertTrue(
+ NodeCommissionResult.objects.filter(node=node2).exists())
+
+ def test_store_data(self):
+ node = factory.make_node()
+ name = factory.getRandomString(100)
+ data = factory.getRandomString(1024 * 1024)
+ NodeCommissionResult.objects.store_data(
+ node, name=name, data=data)
+
+ results = NodeCommissionResult.objects.filter(node=node)
+ [ncr] = results
+ self.assertAttributes(ncr, dict(name=name, data=data))
+
+ def test_store_data_updates_existing(self):
+ node = factory.make_node()
+ name = factory.getRandomString(100)
+ factory.make_node_commission_result(node=node, name=name)
+ data = factory.getRandomString(1024 * 1024)
+ NodeCommissionResult.objects.store_data(
+ node, name=name, data=data)
+
+ results = NodeCommissionResult.objects.filter(node=node)
+ [ncr] = results
+ self.assertAttributes(ncr, dict(name=name, data=data))
+
+ def test_get_data(self):
+ ncr = factory.make_node_commission_result()
+ result = NodeCommissionResult.objects.get_data(ncr.node, ncr.name)
+ self.assertEqual(ncr.data, result)
+
+ def test_get_data_404s_when_not_found(self):
+ ncr = factory.make_node_commission_result()
+ self.assertRaises(
+ Http404,
+ NodeCommissionResult.objects.get_data, ncr.node, "bad name")
diff -Nru maas-0.1+bzr415+dfsg/src/provisioningserver/api.py maas-0.1+bzr462+dfsg/src/provisioningserver/api.py
--- maas-0.1+bzr415+dfsg/src/provisioningserver/api.py 2012-04-04 18:59:25.000000000 +0000
+++ maas-0.1+bzr462+dfsg/src/provisioningserver/api.py 2012-04-12 20:11:10.000000000 +0000
@@ -58,8 +58,9 @@
"mac_addresses": [
mac_address.strip()
for mac_address in mac_addresses
- if not mac_address.isspace()
+ if mac_address and not mac_address.isspace()
],
+ "netboot_enabled": data.get("netboot_enabled"),
"power_type": data["power_type"],
}
@@ -134,6 +135,15 @@
in izip(eth_names, mac_addresses, dns_names)
}
+ # If we're removing all MAC addresses, we need to leave one unconfigured
+ # interface behind to satisfy Cobbler's data model constraints.
+ if len(mac_addresses) == 0:
+ interfaces_to["eth0"] = {
+ "interface": "eth0",
+ "mac_address": "",
+ "dns_name": "",
+ }
+
# Go through interfaces, generating deltas from `interfaces_from` to
# `interfaces_to`. This is done in sorted order to make testing easier.
interface_names = set().union(interfaces_from, interfaces_to)
@@ -152,18 +162,6 @@
pass # No change.
-# 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', '%(maas-metadata-url)s'),
- ('maas-metadata-credentials', 'string', '%(maas-metadata-credentials)s'),
- ]
-metadata_preseed = '\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)
-
-
class ProvisioningAPI:
implements(IProvisioningAPI)
@@ -193,17 +191,16 @@
returnValue(profile.name)
@inlineCallbacks
- def add_node(self, name, hostname, profile, power_type, metadata):
+ def add_node(self, name, hostname, profile, power_type, preseed_data):
assert isinstance(name, basestring)
assert isinstance(hostname, basestring)
assert isinstance(profile, basestring)
assert power_type in (POWER_TYPE.VIRSH, POWER_TYPE.WAKE_ON_LAN)
- assert isinstance(metadata, dict)
- preseed = b64encode(metadata_preseed % metadata)
+ assert isinstance(preseed_data, basestring)
attributes = {
"hostname": hostname,
"profile": profile,
- "ks_meta": {"MAAS_PRESEED": preseed},
+ "ks_meta": {"MAAS_PRESEED": b64encode(preseed_data)},
"power_type": power_type,
}
system = yield CobblerSystem.new(self.session, name, attributes)
diff -Nru maas-0.1+bzr415+dfsg/src/provisioningserver/cobblerclient.py maas-0.1+bzr462+dfsg/src/provisioningserver/cobblerclient.py
--- maas-0.1+bzr415+dfsg/src/provisioningserver/cobblerclient.py 2012-04-04 18:59:25.000000000 +0000
+++ maas-0.1+bzr462+dfsg/src/provisioningserver/cobblerclient.py 2012-04-12 20:11:10.000000000 +0000
@@ -31,9 +31,13 @@
]
from functools import partial
+from urlparse import urlparse
import xmlrpclib
-from provisioningserver.cobblercatcher import convert_cobbler_exception
+from provisioningserver.cobblercatcher import (
+ convert_cobbler_exception,
+ ProvisioningError,
+ )
from provisioningserver.enum import PSERV_FAULT
from twisted.internet import reactor as default_reactor
from twisted.internet.defer import (
@@ -41,6 +45,7 @@
inlineCallbacks,
returnValue,
)
+from twisted.internet.error import DNSLookupError
from twisted.python.log import msg
from twisted.web.xmlrpc import Proxy
@@ -248,6 +253,11 @@
self.proxy.callRemote(method, *args))
except xmlrpclib.Fault as e:
raise convert_cobbler_exception(e)
+ except DNSLookupError as e:
+ hostname = urlparse(self.url).hostname
+ raise ProvisioningError(
+ faultCode=PSERV_FAULT.COBBLER_DNS_LOOKUP_ERROR,
+ faultString=hostname)
returnValue(result)
@inlineCallbacks
@@ -726,6 +736,7 @@
'mac_address',
'profile',
'dns_name',
+ 'netboot_enabled',
]
@classmethod
diff -Nru maas-0.1+bzr415+dfsg/src/provisioningserver/enum.py maas-0.1+bzr462+dfsg/src/provisioningserver/enum.py
--- maas-0.1+bzr415+dfsg/src/provisioningserver/enum.py 2012-04-04 18:59:25.000000000 +0000
+++ maas-0.1+bzr462+dfsg/src/provisioningserver/enum.py 2012-04-12 20:11:10.000000000 +0000
@@ -31,6 +31,9 @@
# Profile does not exist.
NO_SUCH_PROFILE = 5
+ # Error looking up cobbler server.
+ COBBLER_DNS_LOOKUP_ERROR = 6
+
# Non-specific error inside Cobbler.
GENERIC_COBBLER_ERROR = 99
diff -Nru maas-0.1+bzr415+dfsg/src/provisioningserver/interfaces.py maas-0.1+bzr462+dfsg/src/provisioningserver/interfaces.py
--- maas-0.1+bzr415+dfsg/src/provisioningserver/interfaces.py 2012-04-04 18:59:25.000000000 +0000
+++ maas-0.1+bzr462+dfsg/src/provisioningserver/interfaces.py 2012-04-12 20:11:10.000000000 +0000
@@ -50,19 +50,17 @@
:return: The name of the new profile.
"""
- def add_node(name, hostname, profile, power_type, metadata):
+ def add_node(name, hostname, profile, power_type, preseed_data):
"""Add a node with the given `name`.
:param hostname: The fully-qualified hostname for the node.
:param profile: Name of profile to associate the node with.
:param power_type: A choice of power-control method, as in
:class:`POWER_TYPE`.
- :param metadata: Dict of ks_meta items to pre-seed into the node.
- Should include maas-metadata-url (URL for the metadata service)
- and maas-metadata-credentials (OAuth token for accessing the
- metadata service). The maas-metadata-credentials entry details
- oauth_consumer_key, oauth_token_key, and oauth_token_secret
- encoded as a URL query section suitable for urlparse.parse_qs.
+ :param preseed_data: Data to pre-seed into the node as MAAS_PRESEED.
+ Should include the URL for the metadata service, and credentials
+ for accessing it. The credentials consist of oauth_consumer_key,
+ oauth_token_key, and oauth_token_secret.
:return: The name of the new node.
"""
diff -Nru maas-0.1+bzr415+dfsg/src/provisioningserver/plugin.py maas-0.1+bzr462+dfsg/src/provisioningserver/plugin.py
--- maas-0.1+bzr415+dfsg/src/provisioningserver/plugin.py 2012-04-04 18:59:25.000000000 +0000
+++ maas-0.1+bzr462+dfsg/src/provisioningserver/plugin.py 2012-04-12 20:11:10.000000000 +0000
@@ -35,15 +35,71 @@
IServiceMaker,
MultiService,
)
+from twisted.cred.checkers import ICredentialsChecker
+from twisted.cred.credentials import IUsernamePassword
+from twisted.cred.error import UnauthorizedLogin
+from twisted.cred.portal import (
+ IRealm,
+ Portal,
+ )
+from twisted.internet.defer import (
+ inlineCallbacks,
+ returnValue,
+ )
from twisted.plugin import IPlugin
from twisted.python import (
log,
usage,
)
-from twisted.web.resource import Resource
+from twisted.web.guard import (
+ BasicCredentialFactory,
+ HTTPAuthSessionWrapper,
+ )
+from twisted.web.resource import (
+ IResource,
+ Resource,
+ )
from twisted.web.server import Site
import yaml
-from zope.interface import implements
+from zope.interface import implementer
+
+
+@implementer(ICredentialsChecker)
+class SingleUsernamePasswordChecker:
+ """An `ICredentialsChecker` for a single username and password."""
+
+ credentialInterfaces = [IUsernamePassword]
+
+ def __init__(self, username, password):
+ super(SingleUsernamePasswordChecker, self).__init__()
+ self.username = username
+ self.password = password
+
+ @inlineCallbacks
+ def requestAvatarId(self, credentials):
+ """See `ICredentialsChecker`."""
+ if credentials.username == self.username:
+ matched = yield credentials.checkPassword(self.password)
+ if matched:
+ returnValue(credentials.username)
+ raise UnauthorizedLogin(credentials.username)
+
+
+@implementer(IRealm)
+class ProvisioningRealm:
+ """The `IRealm` for the Provisioning API."""
+
+ noop = staticmethod(lambda: None)
+
+ def __init__(self, resource):
+ super(ProvisioningRealm, self).__init__()
+ self.resource = resource
+
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ """See `IRealm`."""
+ if IResource in interfaces:
+ return (IResource, self.resource, self.noop)
+ raise NotImplementedError()
class ConfigOops(Schema):
@@ -89,7 +145,10 @@
if_key_missing = None
+ interface = String(if_empty=b"", if_missing=b"127.0.0.1")
port = Int(min=1, max=65535, if_missing=5241)
+ username = String(not_empty=True, if_missing=getuser())
+ password = String(not_empty=True)
logfile = String(if_empty=b"pserv.log", if_missing=b"pserv.log")
oops = ConfigOops
broker = ConfigBroker
@@ -98,7 +157,7 @@
@classmethod
def parse(cls, stream):
"""Load a YAML configuration from `stream` and validate."""
- return cls().to_python(yaml.load(stream))
+ return cls.to_python(yaml.load(stream))
@classmethod
def load(cls, filename):
@@ -115,67 +174,98 @@
]
+@implementer(IServiceMaker, IPlugin)
class ProvisioningServiceMaker(object):
"""Create a service for the Twisted plugin."""
- implements(IServiceMaker, IPlugin)
-
options = Options
def __init__(self, name, description):
self.tapname = name
self.description = description
- def makeService(self, options):
- """Construct a service."""
- services = MultiService()
-
- config_file = options["config-file"]
- config = Config.load(config_file)
-
- log_service = LogService(config["logfile"])
- log_service.setServiceParent(services)
+ def _makeLogService(self, config):
+ """Create the log service."""
+ return LogService(config["logfile"])
- oops_config = config["oops"]
+ def _makeOopsService(self, log_service, oops_config):
+ """Create the oops service."""
oops_dir = oops_config["directory"]
oops_reporter = oops_config["reporter"]
- oops_service = OOPSService(log_service, oops_dir, oops_reporter)
- oops_service.setServiceParent(services)
+ return OOPSService(log_service, oops_dir, oops_reporter)
- broker_config = config["broker"]
+ def _makeCobblerSession(self, cobbler_config):
+ """Create a :class:`CobblerSession`."""
+ return CobblerSession(
+ cobbler_config["url"], cobbler_config["username"],
+ cobbler_config["password"])
+
+ def _makeProvisioningAPI(self, cobbler_session, config):
+ """Construct an :class:`IResource` for the Provisioning API."""
+ papi_xmlrpc = ProvisioningAPI_XMLRPC(cobbler_session)
+ papi_realm = ProvisioningRealm(papi_xmlrpc)
+ papi_checker = SingleUsernamePasswordChecker(
+ config["username"], config["password"])
+ papi_portal = Portal(papi_realm, [papi_checker])
+ papi_creds = BasicCredentialFactory(b"MAAS Provisioning API")
+ papi_root = HTTPAuthSessionWrapper(papi_portal, [papi_creds])
+ return papi_root
+
+ def _makeSiteService(self, papi_xmlrpc, config):
+ """Create the site service."""
+ site_root = Resource()
+ site_root.putChild("api", papi_xmlrpc)
+ site = Site(site_root)
+ site_port = config["port"]
+ site_interface = config["interface"]
+ site_service = TCPServer(site_port, site, interface=site_interface)
+ site_service.setName("site")
+ return site_service
+
+ def _makeBroker(self, broker_config):
+ """Create the messaging broker."""
broker_port = broker_config["port"]
broker_host = broker_config["host"]
broker_username = broker_config["username"]
broker_password = broker_config["password"]
broker_vhost = broker_config["vhost"]
+ cb_connected = lambda ignored: None # TODO
+ cb_disconnected = lambda ignored: None # TODO
+ cb_failed = lambda connector_and_reason: (
+ log.err(connector_and_reason[1], "Connection failed"))
+ client_factory = AMQFactory(
+ broker_username, broker_password, broker_vhost,
+ cb_connected, cb_disconnected, cb_failed)
+ client_service = TCPClient(
+ broker_host, broker_port, client_factory)
+ client_service.setName("amqp")
+ return client_service
+
+ def makeService(self, options):
+ """Construct a service."""
+ services = MultiService()
+ config = Config.load(options["config-file"])
+
+ log_service = self._makeLogService(config)
+ log_service.setServiceParent(services)
+
+ oops_service = self._makeOopsService(log_service, config["oops"])
+ oops_service.setServiceParent(services)
+
+ broker_config = config["broker"]
# Connecting to RabbitMQ is not yet a required component of a running
# MAAS installation; skip unless the password has been set explicitly.
- if broker_password is not b"test":
- cb_connected = lambda ignored: None # TODO
- cb_disconnected = lambda ignored: None # TODO
- cb_failed = lambda connector_and_reason: (
- log.err(connector_and_reason[1], "Connection failed"))
- client_factory = AMQFactory(
- broker_username, broker_password, broker_vhost,
- cb_connected, cb_disconnected, cb_failed)
- client_service = TCPClient(
- broker_host, broker_port, client_factory)
- client_service.setName("amqp")
+ if broker_config["password"] != b"test":
+ client_service = self._makeBroker(broker_config)
client_service.setServiceParent(services)
cobbler_config = config["cobbler"]
- cobbler_session = CobblerSession(
- cobbler_config["url"], cobbler_config["username"],
- cobbler_config["password"])
- papi_xmlrpc = ProvisioningAPI_XMLRPC(cobbler_session)
+ cobbler_session = self._makeCobblerSession(cobbler_config)
- site_root = Resource()
- site_root.putChild("api", papi_xmlrpc)
- site = Site(site_root)
- site_port = config["port"]
- site_service = TCPServer(site_port, site)
- site_service.setName("site")
+ papi_root = self._makeProvisioningAPI(cobbler_session, config)
+
+ site_service = self._makeSiteService(papi_root, config)
site_service.setServiceParent(services)
return services
diff -Nru maas-0.1+bzr415+dfsg/src/provisioningserver/testing/factory.py maas-0.1+bzr462+dfsg/src/provisioningserver/testing/factory.py
--- maas-0.1+bzr415+dfsg/src/provisioningserver/testing/factory.py 2012-04-04 18:59:25.000000000 +0000
+++ maas-0.1+bzr462+dfsg/src/provisioningserver/testing/factory.py 2012-04-12 20:11:10.000000000 +0000
@@ -10,7 +10,6 @@
__metaclass__ = type
__all__ = [
- 'fake_node_metadata',
'ProvisioningFakeFactory',
]
@@ -34,14 +33,6 @@
return next(names)
-def fake_node_metadata():
- """Produce fake metadata parameters for adding a node."""
- return {
- 'maas-metadata-url': 'http://%s:5240/metadata/' % fake_name(),
- 'maas-metadata-credentials': 'fake/%s' % fake_name(),
- }
-
-
class ProvisioningFakeFactory:
"""Mixin for test cases: factory of fake provisioning objects.
@@ -110,14 +101,12 @@
@inlineCallbacks
def add_node(self, papi, name=None, hostname=None, profile_name=None,
- power_type=None, metadata=None):
+ power_type=None, preseed_data=None):
"""Creates a new node object via `papi`.
Arranges for it to be deleted during test clean-up. If `name` is not
specified, `fake_name` will be called to obtain one. If `profile_name`
- is not specified, one will be obtained by calling `add_profile`. If
- `metadata` is not specified, it will be obtained by calling
- `fake_node_metadata`.
+ is not specified, one will be obtained by calling `add_profile`.
"""
if name is None:
name = fake_name()
@@ -127,10 +116,10 @@
profile_name = yield self.add_profile(papi)
if power_type is None:
power_type = POWER_TYPE.WAKE_ON_LAN
- if metadata is None:
- metadata = fake_node_metadata()
+ if preseed_data is None:
+ preseed_data = ""
node_name = yield papi.add_node(
- name, hostname, profile_name, power_type, metadata)
+ name, hostname, profile_name, power_type, preseed_data)
self.addCleanup(
self.clean_up_objects,
papi.delete_nodes_by_name,
diff -Nru maas-0.1+bzr415+dfsg/src/provisioningserver/testing/fakeapi.py maas-0.1+bzr462+dfsg/src/provisioningserver/testing/fakeapi.py
--- maas-0.1+bzr415+dfsg/src/provisioningserver/testing/fakeapi.py 2012-04-04 18:59:25.000000000 +0000
+++ maas-0.1+bzr462+dfsg/src/provisioningserver/testing/fakeapi.py 2012-04-12 20:11:10.000000000 +0000
@@ -22,6 +22,7 @@
"FakeSynchronousProvisioningAPI",
]
+from base64 import b64encode
from functools import wraps
from provisioningserver.interfaces import IProvisioningAPI
@@ -88,11 +89,13 @@
self.profiles[name]["distro"] = distro
return name
- def add_node(self, name, hostname, profile, power_type, metadata):
+ def add_node(self, name, hostname, profile, power_type, preseed_data):
self.nodes[name]["hostname"] = hostname
self.nodes[name]["profile"] = profile
self.nodes[name]["mac_addresses"] = []
- self.nodes[name]["metadata"] = metadata
+ self.nodes[name]["ks_meta"] = {
+ "MAAS_PRESEED": b64encode(preseed_data),
+ }
self.nodes[name]["power_type"] = power_type
return name
diff -Nru maas-0.1+bzr415+dfsg/src/provisioningserver/tests/test_api.py maas-0.1+bzr462+dfsg/src/provisioningserver/tests/test_api.py
--- maas-0.1+bzr415+dfsg/src/provisioningserver/tests/test_api.py 2012-04-04 18:59:25.000000000 +0000
+++ maas-0.1+bzr462+dfsg/src/provisioningserver/tests/test_api.py 2012-04-12 20:11:10.000000000 +0000
@@ -34,10 +34,7 @@
from provisioningserver.cobblerclient import CobblerSystem
from provisioningserver.enum import POWER_TYPE
from provisioningserver.interfaces import IProvisioningAPI
-from provisioningserver.testing.factory import (
- fake_node_metadata,
- ProvisioningFakeFactory,
- )
+from provisioningserver.testing.factory import ProvisioningFakeFactory
from provisioningserver.testing.fakeapi import FakeAsynchronousProvisioningAPI
from provisioningserver.testing.fakecobbler import make_fake_cobbler_session
from provisioningserver.testing.realcobbler import RealCobbler
@@ -69,8 +66,12 @@
"hostname": "dystopia",
"interfaces": {
"eth0": {"mac_address": "12:34:56:78:9a:bc"},
+ "eth1": {"mac_address": " "},
+ "eth2": {"mac_address": ""},
+ "eth3": {"mac_address": None},
},
"power_type": "virsh",
+ "netboot_enabled": False,
"ju": "nk",
}
expected = {
@@ -78,6 +79,7 @@
"profile": "earth",
"hostname": "dystopia",
"mac_addresses": ["12:34:56:78:9a:bc"],
+ "netboot_enabled": False,
"power_type": "virsh",
}
observed = cobbler_to_papi_node(data)
@@ -89,6 +91,7 @@
"profile": "earth",
"hostname": "darksaga",
"power_type": "ether_wake",
+ "netboot_enabled": True,
"ju": "nk",
}
expected = {
@@ -96,6 +99,7 @@
"profile": "earth",
"hostname": "darksaga",
"mac_addresses": [],
+ "netboot_enabled": True,
"power_type": "ether_wake",
}
observed = cobbler_to_papi_node(data)
@@ -257,6 +261,31 @@
current_interfaces, hostname, mac_addresses)
self.assertItemsEqual(expected, observed)
+ def test_gen_cobbler_interface_deltas_remove_all_macs(self):
+ # Removing all MAC addresses results in a delta to remove all but the
+ # first interface. The first interface is instead deconfigured; this
+ # is necessary to satisfy the Cobbler data model.
+ current_interfaces = {
+ "eth0": {
+ "mac_address": "11:11:11:11:11:11",
+ },
+ "eth1": {
+ "mac_address": "22:22:22:22:22:22",
+ },
+ }
+ hostname = "empiricism"
+ mac_addresses = []
+ expected = [
+ {"interface": "eth0",
+ "mac_address": "",
+ "dns_name": ""},
+ {"interface": "eth1",
+ "delete_interface": True},
+ ]
+ observed = gen_cobbler_interface_deltas(
+ current_interfaces, hostname, mac_addresses)
+ self.assertItemsEqual(expected, observed)
+
class ProvisioningAPITests(ProvisioningFakeFactory):
"""Tests for `provisioningserver.api.ProvisioningAPI`.
@@ -394,6 +423,30 @@
[mac_address2], values[node_name]["mac_addresses"])
@inlineCallbacks
+ def test_modify_nodes_set_netboot_enabled(self):
+ papi = self.get_provisioning_api()
+ node_name = yield self.add_node(papi)
+ yield papi.modify_nodes({node_name: {"netboot_enabled": False}})
+ values = yield papi.get_nodes_by_name([node_name])
+ self.assertFalse(values[node_name]["netboot_enabled"])
+ yield papi.modify_nodes({node_name: {"netboot_enabled": True}})
+ values = yield papi.get_nodes_by_name([node_name])
+ self.assertTrue(values[node_name]["netboot_enabled"])
+
+ @inlineCallbacks
+ def test_modify_nodes_remove_all_mac_addresses(self):
+ papi = self.get_provisioning_api()
+ node_name = yield self.add_node(papi)
+ mac_address = factory.getRandomMACAddress()
+ yield papi.modify_nodes(
+ {node_name: {"mac_addresses": [mac_address]}})
+ yield papi.modify_nodes(
+ {node_name: {"mac_addresses": []}})
+ values = yield papi.get_nodes_by_name([node_name])
+ self.assertEqual(
+ [], values[node_name]["mac_addresses"])
+
+ @inlineCallbacks
def test_delete_distros_by_name(self):
# Create a distro via the Provisioning API.
papi = self.get_provisioning_api()
@@ -545,6 +598,15 @@
self.assertItemsEqual(
dict(zip(power_types, power_types)), cobbler_power_types)
+ @inlineCallbacks
+ def test_add_node_provides_preseed(self):
+ papi = self.get_provisioning_api()
+ preseed_data = factory.getRandomString()
+ node_name = yield self.add_node(papi, preseed_data=preseed_data)
+ attrs = yield CobblerSystem(papi.session, node_name).get_values()
+ self.assertEqual(
+ preseed_data, b64decode(attrs['ks_meta']['MAAS_PRESEED']))
+
class TestFakeProvisioningAPI(ProvisioningAPITests, TestCase):
"""Test :class:`FakeAsynchronousProvisioningAPI`.
@@ -569,17 +631,6 @@
"""Return a real ProvisioningAPI, but using a fake Cobbler session."""
return ProvisioningAPI(make_fake_cobbler_session())
- @inlineCallbacks
- def test_add_node_preseeds_metadata(self):
- papi = self.get_provisioning_api()
- metadata = fake_node_metadata()
- node_name = yield self.add_node(papi, metadata=metadata)
- attrs = yield CobblerSystem(papi.session, node_name).get_values()
- preseed = attrs['ks_meta']['MAAS_PRESEED']
- preseed = b64decode(preseed)
- self.assertIn(metadata['maas-metadata-url'], preseed)
- self.assertIn(metadata['maas-metadata-credentials'], preseed)
-
class TestProvisioningAPIWithRealCobbler(ProvisioningAPITests,
ProvisioningAPITestsWithCobbler,
diff -Nru maas-0.1+bzr415+dfsg/src/provisioningserver/tests/test_cobblersession.py maas-0.1+bzr462+dfsg/src/provisioningserver/tests/test_cobblersession.py
--- maas-0.1+bzr415+dfsg/src/provisioningserver/tests/test_cobblersession.py 2012-04-04 18:59:25.000000000 +0000
+++ maas-0.1+bzr462+dfsg/src/provisioningserver/tests/test_cobblersession.py 2012-04-12 20:11:10.000000000 +0000
@@ -12,11 +12,14 @@
__all__ = []
from random import Random
+import re
from xmlrpclib import Fault
import fixtures
+from maastesting.factory import factory
from provisioningserver import cobblerclient
from provisioningserver.cobblercatcher import ProvisioningError
+from provisioningserver.enum import PSERV_FAULT
from provisioningserver.testing.fakecobbler import (
fake_auth_failure_string,
fake_object_not_found_string,
@@ -34,6 +37,7 @@
)
from twisted.internet import defer
from twisted.internet.defer import inlineCallbacks
+from twisted.internet.error import DNSLookupError
from twisted.internet.task import Clock
@@ -342,6 +346,24 @@
self.assertEqual(
[('authenticate_me_first', session.token)], session.proxy.calls)
+ @inlineCallbacks
+ def test_dns_lookup_exception_handled(self):
+ url = factory.getRandomString()
+ session_args = (
+ 'http://%s/%d' % (url, pick_number()),
+ factory.getRandomString(), # username.
+ factory.getRandomString(), # password.
+ )
+ session = make_recording_session(session_args=session_args)
+ failure = DNSLookupError(factory.getRandomString())
+ session.proxy.set_return_values([failure])
+ expected_exception = ProvisioningError(
+ faultCode=PSERV_FAULT.COBBLER_DNS_LOOKUP_ERROR,
+ faultString=url.lower())
+ expected_exception_re = re.escape(unicode(expected_exception))
+ with ExpectedException(ProvisioningError, expected_exception_re):
+ yield session.call('failing_method')
+
class TestConnectionTimeouts(TestCase, fixtures.TestWithFixtures):
"""Tests for connection timeouts on `CobblerSession`."""
diff -Nru maas-0.1+bzr415+dfsg/src/provisioningserver/tests/test_plugin.py maas-0.1+bzr462+dfsg/src/provisioningserver/tests/test_plugin.py
--- maas-0.1+bzr415+dfsg/src/provisioningserver/tests/test_plugin.py 2012-04-04 18:59:25.000000000 +0000
+++ maas-0.1+bzr462+dfsg/src/provisioningserver/tests/test_plugin.py 2012-04-12 20:11:10.000000000 +0000
@@ -11,24 +11,44 @@
__metaclass__ = type
__all__ = []
+from base64 import b64encode
from functools import partial
from getpass import getuser
+import httplib
import os
+from StringIO import StringIO
+import xmlrpclib
from fixtures import TempDir
import formencode
+from maastesting.factory import factory
from provisioningserver.plugin import (
Config,
Options,
+ ProvisioningRealm,
ProvisioningServiceMaker,
+ SingleUsernamePasswordChecker,
)
+from provisioningserver.testing.fakecobbler import make_fake_cobbler_session
from testtools import TestCase
+from testtools.deferredruntest import (
+ assert_fails_with,
+ AsynchronousDeferredRunTest,
+ )
from testtools.matchers import (
MatchesException,
Raises,
)
+from twisted.application.internet import TCPServer
from twisted.application.service import MultiService
+from twisted.cred.credentials import UsernamePassword
+from twisted.cred.error import UnauthorizedLogin
+from twisted.internet.defer import inlineCallbacks
from twisted.python.usage import UsageError
+from twisted.web.guard import HTTPAuthSessionWrapper
+from twisted.web.resource import IResource
+from twisted.web.server import NOT_DONE_YET
+from twisted.web.test.test_web import DummyRequest
import yaml
@@ -36,6 +56,9 @@
"""Tests for `provisioningserver.plugin.Config`."""
def test_defaults(self):
+ mandatory = {
+ 'password': 'killing_joke',
+ }
expected = {
'broker': {
'host': 'localhost',
@@ -54,14 +77,20 @@
'directory': '',
'reporter': '',
},
+ 'interface': '127.0.0.1',
'port': 5241,
+ 'username': getuser(),
}
- observed = Config.to_python({})
+ expected.update(mandatory)
+ observed = Config.to_python(mandatory)
self.assertEqual(expected, observed)
def test_parse(self):
# Configuration can be parsed from a snippet of YAML.
- observed = Config.parse(b'logfile: "/some/where.log"')
+ observed = Config.parse(
+ b'logfile: "/some/where.log"\n'
+ b'password: "black_sabbath"\n'
+ )
self.assertEqual("/some/where.log", observed["logfile"])
def test_load(self):
@@ -69,7 +98,8 @@
filename = os.path.join(
self.useFixture(TempDir()).path, "config.yaml")
with open(filename, "wb") as stream:
- stream.write(b'logfile: "/some/where.log"')
+ stream.write(b'logfile: "/some/where.log"\n')
+ stream.write(b'password: "megadeth"\n')
observed = Config.load(filename)
self.assertEqual("/some/where.log", observed["logfile"])
@@ -117,11 +147,14 @@
class TestProvisioningServiceMaker(TestCase):
"""Tests for `provisioningserver.plugin.ProvisioningServiceMaker`."""
+ run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=5)
+
def setUp(self):
super(TestProvisioningServiceMaker, self).setUp()
self.tempdir = self.useFixture(TempDir()).path
def write_config(self, config):
+ config.setdefault("password", factory.getRandomString())
config_filename = os.path.join(self.tempdir, "config.yaml")
with open(config_filename, "wb") as stream:
yaml.dump(config, stream)
@@ -165,3 +198,110 @@
self.assertEqual(
len(service.namedServices), len(service.services),
"Not all services are named.")
+
+ def test_makeService_api_requires_credentials(self):
+ """
+ The site service's /api resource requires credentials from clients.
+ """
+ options = Options()
+ options["config-file"] = self.write_config({})
+ service_maker = ProvisioningServiceMaker("Harry", "Hill")
+ service = service_maker.makeService(options)
+ self.assertIsInstance(service, MultiService)
+ site_service = service.getServiceNamed("site")
+ self.assertIsInstance(site_service, TCPServer)
+ port, site = site_service.args
+ self.assertIn("api", site.resource.listStaticNames())
+ api = site.resource.getStaticEntity("api")
+ # HTTPAuthSessionWrapper demands credentials from an HTTP request.
+ self.assertIsInstance(api, HTTPAuthSessionWrapper)
+
+ def exercise_api_credentials(self, config_file, username, password):
+ """
+ Create a new service with :class:`ProvisioningServiceMaker` and
+ attempt to access the API with the given credentials.
+ """
+ options = Options()
+ options["config-file"] = config_file
+ service_maker = ProvisioningServiceMaker("Morecombe", "Wise")
+ # Terminate the service in a fake Cobbler session.
+ service_maker._makeCobblerSession = (
+ lambda config: make_fake_cobbler_session())
+ service = service_maker.makeService(options)
+ port, site = service.getServiceNamed("site").args
+ api = site.resource.getStaticEntity("api")
+ # Create an XML-RPC request with valid credentials.
+ request = DummyRequest([])
+ request.method = "POST"
+ request.content = StringIO(xmlrpclib.dumps((), "get_nodes"))
+ request.prepath = ["api"]
+ request.headers["authorization"] = (
+ "Basic %s" % b64encode(b"%s:%s" % (username, password)))
+ # The credential check and resource rendering is deferred, but
+ # NOT_DONE_YET is returned from render(). The request signals
+ # completion with the aid of notifyFinish().
+ finished = request.notifyFinish()
+ self.assertEqual(NOT_DONE_YET, api.render(request))
+ return finished.addCallback(lambda ignored: request)
+
+ @inlineCallbacks
+ def test_makeService_api_accepts_valid_credentials(self):
+ """
+ The site service's /api resource accepts valid credentials.
+ """
+ config = {"username": "orange", "password": "goblin"}
+ request = yield self.exercise_api_credentials(
+ self.write_config(config), "orange", "goblin")
+ # A valid XML-RPC response has been written.
+ self.assertEqual(None, request.responseCode) # None implies 200.
+ xmlrpclib.loads(b"".join(request.written))
+
+ @inlineCallbacks
+ def test_makeService_api_rejects_invalid_credentials(self):
+ """
+ The site service's /api resource rejects invalid credentials.
+ """
+ config = {"username": "orange", "password": "goblin"}
+ request = yield self.exercise_api_credentials(
+ self.write_config(config), "abigail", "williams")
+ # The request has not been authorized.
+ self.assertEqual(httplib.UNAUTHORIZED, request.responseCode)
+
+
+class TestSingleUsernamePasswordChecker(TestCase):
+ """Tests for `SingleUsernamePasswordChecker`."""
+
+ run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=5)
+
+ @inlineCallbacks
+ def test_requestAvatarId_okay(self):
+ credentials = UsernamePassword("frank", "zappa")
+ checker = SingleUsernamePasswordChecker("frank", "zappa")
+ avatar = yield checker.requestAvatarId(credentials)
+ self.assertEqual("frank", avatar)
+
+ def test_requestAvatarId_bad(self):
+ credentials = UsernamePassword("frank", "zappa")
+ checker = SingleUsernamePasswordChecker("zap", "franka")
+ d = checker.requestAvatarId(credentials)
+ return assert_fails_with(d, UnauthorizedLogin)
+
+
+class TestProvisioningRealm(TestCase):
+ """Tests for `ProvisioningRealm`."""
+
+ def test_requestAvatar_okay(self):
+ resource = object()
+ realm = ProvisioningRealm(resource)
+ avatar = realm.requestAvatar(
+ "irrelevant", "also irrelevant", IResource)
+ self.assertEqual((IResource, resource, realm.noop), avatar)
+
+ def test_requestAvatar_bad(self):
+ # If IResource is not amongst the interfaces passed to requestAvatar,
+ # NotImplementedError is raised.
+ resource = object()
+ realm = ProvisioningRealm(resource)
+ self.assertRaises(
+ NotImplementedError, realm.requestAvatar,
+ "irrelevant", "also irrelevant")
diff -Nru maas-0.1+bzr415+dfsg/templates/doc.txt maas-0.1+bzr462+dfsg/templates/doc.txt
--- maas-0.1+bzr415+dfsg/templates/doc.txt 1970-01-01 00:00:00.000000000 +0000
+++ maas-0.1+bzr462+dfsg/templates/doc.txt 2012-04-12 20:11:10.000000000 +0000
@@ -0,0 +1,36 @@
+.. -*- mode: rst -*-
+
+**************
+Document Title
+**************
+
+
+Section
+=======
+
+
+Subsection
+----------
+
+Leave two empty lines before section headings.
+
+
+Subsubsection
+^^^^^^^^^^^^^
+
+Typically 2 spaces are used to indent blocks.
+
+ Like this.
+
+Building is simple::
+
+ $ make doc
+
+will do everything, even in a fresh checkout, but on subsequent
+iterations::
+
+ $ bin/sphinx
+
+is quicker.
+
+Also see `the Sphinx docs `_.
diff -Nru maas-0.1+bzr415+dfsg/vdenv/api-list.py maas-0.1+bzr462+dfsg/vdenv/api-list.py
--- maas-0.1+bzr415+dfsg/vdenv/api-list.py 2012-04-04 18:59:25.000000000 +0000
+++ maas-0.1+bzr462+dfsg/vdenv/api-list.py 2012-04-12 20:11:10.000000000 +0000
@@ -17,7 +17,7 @@
host = "192.168.123.2"
user = "cobbler"
-password = "cobbler"
+password = "xcobbler"
if len(sys.argv) >= 2:
host = sys.argv[1]
if len(sys.argv) >= 3:
diff -Nru maas-0.1+bzr415+dfsg/vdenv/TODO maas-0.1+bzr462+dfsg/vdenv/TODO
--- maas-0.1+bzr415+dfsg/vdenv/TODO 2012-04-04 18:59:25.000000000 +0000
+++ maas-0.1+bzr462+dfsg/vdenv/TODO 2012-04-12 20:11:10.000000000 +0000
@@ -7,7 +7,7 @@
- start ssh connection to remote system with a bunch of ports
forwarded for vnc connections and http to the zimmer box
ssh -C home-jimbo \
- $(t=98; for((i=0;i<5;i++)); do p=$(printf "%02d" "$i"); echo -L $t$p:localhost:59$p; done ; echo -L${t}80:192.168.123.2:80)
+ $(t=98; for((i=0;i<5;i++)); do p=$(printf "%02d" "$i"); echo -L $t$p:localhost:59$p; done ; echo -L${t}80:192.168.123.2:80 -L${t}81:localhost:5240)
- tell orchestra to point to a different proxy server
- document or fix annoying ssh key entries (juju prompt for add and change)
- get serial consoles to log file for domains
diff -Nru maas-0.1+bzr415+dfsg/vdenv/zimmer-build/build maas-0.1+bzr462+dfsg/vdenv/zimmer-build/build
--- maas-0.1+bzr415+dfsg/vdenv/zimmer-build/build 2012-04-04 18:59:25.000000000 +0000
+++ maas-0.1+bzr462+dfsg/vdenv/zimmer-build/build 2012-04-12 20:11:10.000000000 +0000
@@ -56,7 +56,7 @@
--save D put pristine copies of things in D
default: $DEF_SAVE_D
--ud-file F use user-data file F
- default: $DEF_USER_DATA
+ default: $DEF_UD_FILE
--import-keys K import ssh keys
values are 'auto', 'lp:', or path to file
EOF
diff -Nru maas-0.1+bzr415+dfsg/vdenv/zimmer-build/ud-build.txt maas-0.1+bzr462+dfsg/vdenv/zimmer-build/ud-build.txt
--- maas-0.1+bzr415+dfsg/vdenv/zimmer-build/ud-build.txt 2012-04-04 18:59:25.000000000 +0000
+++ maas-0.1+bzr462+dfsg/vdenv/zimmer-build/ud-build.txt 2012-04-12 20:11:10.000000000 +0000
@@ -16,7 +16,6 @@
echo === $(date) ====
debconf-set-selections <> /etc/init/cobbler.conf <<"ENDCOB"
+ #### added by zimmer-build ######
+ pre-start script
+ #set -x; exec >/tmp/cobbler-pre.out 2>/tmp/cobbler-pre.err
+ found=""
+ fn="/etc/cobbler/cobbler-server"
+
+ # if user wants to manage this, then just remove that file
+ # it is expected to either have an IP address or 'auto'
+ [ -f "$fn" ] || exit 0
+ read found < "$fn" || :
+ [ -n "$found" -a "$found" != "auto" ] ||
+ found=$(ifconfig eth0 2>/dev/null |
+ awk '$0 ~ /inet addr:/ { sub(/.*:/,"",$2); print $2; }')
+
+ [ -z "$found" ] && exit 0
+
+ sed -i.start -e "s/^next_server: .*$/next_server: $found/" \
+ -e "s/^server: *..*..*..*$/server: $found/" \
+ /etc/cobbler/settings
+
+ # if the above sed did something, then we leave the orig
+ # file around for post-start to cleanup.
+ cmp /etc/cobbler/settings.start /etc/cobbler/settings >/dev/null &&
+ rm -f /etc/cobbler/settings.start || :
+ end script
+
+ post-start script
+ #set -x; exec >/tmp/cobbler-start.out 2>/tmp/cobbler-start.err
+ set +e # upstart uses 'set -e' by default
+ [ -f /etc/cobbler/settings.start ] || exit 0
+ rm -f /etc/cobbler/settings.start
+
+ # now basically wait around until cobbler is available
+ while : ; do
+ # will exit 155 if "cobblerd does not appear to be running"
+ cobbler system list >/dev/null
+ [ $? -eq 155 ] || break
+ sleep 1
+ done
+
+ # we're allowing cobbler to start even if these fail
+ cobbler sync
+ maas-import-isos --update
+ exit 0
+ end script
+ ENDCOB
+
+ echo "auto" >> /etc/cobbler/cobbler-server
+
cat >> /etc/maas/import_isos <