Merge lp:~mpontillo/maas/allow-commissioning-non-lts into lp:~maas-committers/maas/trunk

Proposed by Mike Pontillo
Status: Rejected
Rejected by: MAAS Lander
Proposed branch: lp:~mpontillo/maas/allow-commissioning-non-lts
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 235 lines (+82/-25)
6 files modified
src/provisioningserver/config.py (+14/-6)
src/provisioningserver/drivers/osystem/tests/test_ubuntu.py (+30/-0)
src/provisioningserver/drivers/osystem/ubuntu.py (+10/-2)
src/provisioningserver/tests/test_config.py (+0/-7)
src/provisioningserver/utils/config.py (+5/-4)
src/provisioningserver/utils/tests/test_config.py (+23/-6)
To merge this branch: bzr merge lp:~mpontillo/maas/allow-commissioning-non-lts
Reviewer Review Type Date Requested Status
Andres Rodriguez (community) Needs Information
Gavin Panella (community) Approve
Review via email: mp+307904@code.launchpad.net

Commit message

Add option to rackd.conf that allows commissioning on non-LTS Ubuntu releases.

 * New option: allow_commissioning_non_lts: <bool>
 * Drive-by fix for a bug that prevented usages of some validators
   in the configuration subsystem, such as the StringBool.
   (values were being returned as strings rather than bool)

Description of the change

If someone wants to test with a development release, currently they need to hack the MAAS code. This change allows a simple rackd.conf change (followed by reloading the global settings page) to relax the LTS-only restriction and allow that testing.

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

Fixing the to_python/from_python mix-up is great, but it's had some fallout. Try running bin/test.rack in full and you'll see what I mean. I have a fix for you though: https://code.launchpad.net/~allenap/maas/mpontillo--allow-commissioning-non-lts/+merge/307918. I haven't run bin/test.region though; life's too short for that ;)

I think the code here is grand, but I wonder if it's a good idea to put this into the rack configuration file. I'm not against the principle but I wonder if it will get abused. In that regard it's good that it cannot be configured with `maas-rack config`, and I think it should stay that way. My concern is probably premature though: let's see if we get bugs arising from misuse of this option and then consider what to do.

One question: on the region, when compiling the list of commissioning releases, do we intersect the responses from all racks? This setting may then be confusing in environments with multiple rack controllers, because non-LTS releases won't appear until all racks have been configured to allow them.

review: Approve
Revision history for this message
Andres Rodriguez (andreserl) wrote :

What is the desire to have this configuration option ?

We made an agreement long ago to not allow commissioning with any other release than the LTS. This allows us to control the environment and prevents users from running into unsupported paths. We support hardware enablement kernels and as such, we should be allowing that instead of adding this option.

That said, it is not a good idea to put it in a configuration file, because, yes, it will get abused. We specifically did things the way we did for particular reasons. We should not be providing a path to anyone to do things different.

review: Needs Information
5461. By Mike Pontillo

Merge allenap's fixes from lp:~allenap/maas/mpontillo--allow-commissioning-non-lts.

Revision history for this message
Mike Pontillo (mpontillo) wrote :

@allenap, thanks for the fixes; I was going to ask you about that. I removed one of the test cases in that file because I wasn't sure that it was correct for the configuration store to allow an invalid directory to be returned. It seemed to me like we should catch that at the earliest possible time, so we don't propagate a directory that doesn't exist throughout the code and try to use it everywhere. But I can see the argument for both ways, and certainly there is less code change (and exception handling changes) required if we leave that scenario the way it is.

Also, the `maas-rack config` command has intentionally NOT been updated. Users will never see this option unless they look at the code.

I was curious about what we do in a multi-rack scenario, too. I didn't dwell on that too much, because this was strictly intended as an unsupported feature for QA/developers, and I figured if it misbehaved it's not the end of the world.

@Andres, I understand your concerns. But I think this feature is sufficiently hidden, and that the average MAAS user won't stumble upon it and try it out (at their peril). The only way they can enable this is to (1) know it exists, and (2) add it to their rackd.conf manually. If they found it by looking at the code, it should be clear from the surrounding text that it is NOT SUPPORTED and NOT RECOMMENDED. (I can call that out in clearer language, if you like.)

The reason you want to have this in a configuration file is because it makes it easier to do QA against MAAS; especially integration testing against the latest MAAS images. With this configuration option available, we can more easily test development releases in a CI environment, and thus catch problems that will occur with the *next* LTS sooner rather than later.

To be honest, I did this branch because I wanted to scratch an itch. I was trying to test out changes to the Yakkety images, and I was annoyed that I had to figure out where to hack the code each time I wanted to do that.

In conclusion, this should be something that can be easily enabled for QA or development purposes -- without hacking the Python files in the MAAS package, or using other workarounds.

Revision history for this message
Mike Pontillo (mpontillo) wrote :

I forgot to add: the other reason you want to have this is for supportability. If suddenly the LTS no longer commissions but a non-LTS image is fine (or already has the fix, whatever the case may be), you want to have a way to allow users to use a non-LTS image in an emergency situation.

The other idea I had was, we could rename the option to "allow_commissioning_unsupported_releases", just to make it crystal clear that users who do this will get no support.

Revision history for this message
MAAS Lander (maas-lander) wrote :

Transitioned to Git.

lp:maas has now moved from Bzr to Git.
Please propose your branches with Launchpad using Git.

git clone https://git.launchpad.net/maas

Unmerged revisions

5461. By Mike Pontillo

Merge allenap's fixes from lp:~allenap/maas/mpontillo--allow-commissioning-non-lts.

5460. By Mike Pontillo

Fix tests, address incorrect assumption.

5459. By Mike Pontillo

Add configuraiton option for commissioning on a non-LTS release. Drive-by fix for a problem that prevented correct usage of python conversions in rackd.conf.

5458. By Mike Pontillo

Add a constant to allow commissioning non-LTS releases.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/provisioningserver/config.py'
--- src/provisioningserver/config.py 2016-09-27 22:26:08 +0000
+++ src/provisioningserver/config.py 2016-10-07 18:00:08 +0000
@@ -140,6 +140,7 @@
140from formencode.validators import (140from formencode.validators import (
141 Number,141 Number,
142 Set,142 Set,
143 StringBool,
143)144)
144from provisioningserver.path import get_tentative_path145from provisioningserver.path import get_tentative_path
145from provisioningserver.utils import typed146from provisioningserver.utils import typed
@@ -735,10 +736,13 @@
735 except KeyError:736 except KeyError:
736 return self.validator.if_missing737 return self.validator.if_missing
737 else:738 else:
738 return self.validator.from_python(value)739 return self.validator.to_python(value)
739740
740 def __set__(self, obj, value):741 def __set__(self, obj, value):
741 obj.store[self.name] = self.validator.to_python(value)742 entry = self.validator.from_python(value)
743 # Make sure we'll be able to get the value back out later.
744 self.validator.to_python(entry)
745 obj.store[self.name] = entry
742746
743 def __delete__(self, obj):747 def __delete__(self, obj):
744 del obj.store[self.name]748 del obj.store[self.name]
@@ -766,15 +770,19 @@
766 tftp_root = ConfigurationOption(770 tftp_root = ConfigurationOption(
767 "tftp_root", "The root directory for TFTP resources.",771 "tftp_root", "The root directory for TFTP resources.",
768 DirectoryString(772 DirectoryString(
769 # Don't validate values that are already stored.773 if_missing=get_tentative_path(
770 accept_python=True, if_missing=get_tentative_path(
771 "/var/lib/maas/boot-resources/current")))774 "/var/lib/maas/boot-resources/current")))
772775
776 allow_commissioning_non_lts = ConfigurationOption(
777 "allow_commissioning_non_lts",
778 "Allow commissioning using non-LTS Ubuntu releases. (Not generally "
779 "supported; only recommended for developers.)",
780 StringBool(if_missing=False))
781
773 # GRUB options.782 # GRUB options.
774
775 @property783 @property
776 def grub_root(self):784 def grub_root(self):
777 "The root directory for GRUB resources."785 """The root directory for GRUB resources."""
778 return os.path.join(self.tftp_root, "grub")786 return os.path.join(self.tftp_root, "grub")
779787
780 # NodeGroup UUID Option, used for migrating to rack controller788 # NodeGroup UUID Option, used for migrating to rack controller
781789
=== modified file 'src/provisioningserver/drivers/osystem/tests/test_ubuntu.py'
--- src/provisioningserver/drivers/osystem/tests/test_ubuntu.py 2016-07-06 05:37:58 +0000
+++ src/provisioningserver/drivers/osystem/tests/test_ubuntu.py 2016-10-07 18:00:08 +0000
@@ -14,6 +14,8 @@
14from maastesting.testcase import MAASTestCase14from maastesting.testcase import MAASTestCase
15from provisioningserver.drivers.osystem import BOOT_IMAGE_PURPOSE15from provisioningserver.drivers.osystem import BOOT_IMAGE_PURPOSE
16from provisioningserver.drivers.osystem.ubuntu import UbuntuOS16from provisioningserver.drivers.osystem.ubuntu import UbuntuOS
17from provisioningserver.testing.config import ClusterConfigurationFixture
18from testtools.matchers import Contains
1719
1820
19class TestUbuntuOS(MAASTestCase):21class TestUbuntuOS(MAASTestCase):
@@ -57,6 +59,8 @@
57 self.assertEqual(expected, self.get_lts_release())59 self.assertEqual(expected, self.get_lts_release())
5860
59 def test_get_supported_commissioning_releases(self):61 def test_get_supported_commissioning_releases(self):
62 self.useFixture(
63 ClusterConfigurationFixture(allow_commissioning_non_lts=False))
60 self.patch_autospec(UbuntuDistroInfo, "is_lts").return_value = True64 self.patch_autospec(UbuntuDistroInfo, "is_lts").return_value = True
61 self.patch_autospec(UbuntuDistroInfo, "supported").return_value = [65 self.patch_autospec(UbuntuDistroInfo, "supported").return_value = [
62 'precise', 'trusty', 'vivid', 'wily', 'xenial'66 'precise', 'trusty', 'vivid', 'wily', 'xenial'
@@ -68,6 +72,8 @@
68 ['trusty', 'vivid', 'wily', 'xenial'], releases)72 ['trusty', 'vivid', 'wily', 'xenial'], releases)
6973
70 def test_get_supported_commissioning_releases_excludes_non_lts(self):74 def test_get_supported_commissioning_releases_excludes_non_lts(self):
75 self.useFixture(
76 ClusterConfigurationFixture(allow_commissioning_non_lts=False))
71 supported = [77 supported = [
72 'precise', 'trusty', 'vivid', 'wily', 'xenial'78 'precise', 'trusty', 'vivid', 'wily', 'xenial'
73 ]79 ]
@@ -81,8 +87,30 @@
81 for release in non_lts_releases:87 for release in non_lts_releases:
82 self.assertNotIn(release, releases)88 self.assertNotIn(release, releases)
8389
90 def test_get_supported_commissioning_releases_non_lts_option(self):
91 self.useFixture(
92 ClusterConfigurationFixture(allow_commissioning_non_lts=True))
93 supported = [
94 'precise', 'trusty', 'vivid', 'wily', 'xenial'
95 ]
96 self.patch_autospec(
97 UbuntuDistroInfo, "supported").return_value = supported
98 osystem = UbuntuOS()
99 releases = osystem.get_supported_commissioning_releases()
100 self.assertIsInstance(releases, list)
101 udi = UbuntuDistroInfo()
102 non_lts_releases = [name for name in supported if not udi.is_lts(name)]
103 # With the configuration option set, we should include all non-LTS
104 # releases, plus LTS as well.
105 for release in non_lts_releases:
106 self.assertIn(release, releases)
107 self.assertThat(releases, Contains("trusty"))
108 self.assertThat(releases, Contains("xenial"))
109
84 def test_get_supported_commissioning_releases_excludes_precise(self):110 def test_get_supported_commissioning_releases_excludes_precise(self):
85 """Make sure we remove 'precise' from the list."""111 """Make sure we remove 'precise' from the list."""
112 self.useFixture(
113 ClusterConfigurationFixture(allow_commissioning_non_lts=False))
86 self.patch_autospec(UbuntuDistroInfo, "supported").return_value = [114 self.patch_autospec(UbuntuDistroInfo, "supported").return_value = [
87 'precise', 'trusty', 'vivid', 'wily', 'xenial'115 'precise', 'trusty', 'vivid', 'wily', 'xenial'
88 ]116 ]
@@ -93,6 +121,8 @@
93121
94 def test_get_supported_commissioning_releases_excludes_unsupported_lts(122 def test_get_supported_commissioning_releases_excludes_unsupported_lts(
95 self):123 self):
124 self.useFixture(
125 ClusterConfigurationFixture(allow_commissioning_non_lts=False))
96 self.patch_autospec(UbuntuDistroInfo, "supported").return_value = [126 self.patch_autospec(UbuntuDistroInfo, "supported").return_value = [
97 'precise', 'trusty', 'vivid', 'wily', 'xenial'127 'precise', 'trusty', 'vivid', 'wily', 'xenial'
98 ]128 ]
99129
=== modified file 'src/provisioningserver/drivers/osystem/ubuntu.py'
--- src/provisioningserver/drivers/osystem/ubuntu.py 2016-07-06 05:37:58 +0000
+++ src/provisioningserver/drivers/osystem/ubuntu.py 2016-10-07 18:00:08 +0000
@@ -63,9 +63,17 @@
63 system that supports commissioning.63 system that supports commissioning.
64 """64 """
65 unsupported_releases = ['precise']65 unsupported_releases = ['precise']
66 return [name for name in self.ubuntu_distro_info.supported()66 with ClusterConfiguration.open() as config:
67 if config.allow_commissioning_non_lts is True:
68 return [
69 name for name in self.ubuntu_distro_info.supported()
70 if name not in unsupported_releases
71 ]
72 return [
73 name for name in self.ubuntu_distro_info.supported()
67 if name not in unsupported_releases74 if name not in unsupported_releases
68 if self.ubuntu_distro_info.is_lts(name)]75 if self.ubuntu_distro_info.is_lts(name)
76 ]
6977
70 def get_default_commissioning_release(self):78 def get_default_commissioning_release(self):
71 """Gets the default commissioning release for Ubuntu. This only exists79 """Gets the default commissioning release for Ubuntu. This only exists
7280
=== modified file 'src/provisioningserver/tests/test_config.py'
--- src/provisioningserver/tests/test_config.py 2016-08-11 01:50:21 +0000
+++ src/provisioningserver/tests/test_config.py 2016-10-07 18:00:08 +0000
@@ -212,13 +212,6 @@
212 config = self.make_config()212 config = self.make_config()
213 self.assertIs(sentinel.missing, config.something)213 self.assertIs(sentinel.missing, config.something)
214214
215 def test_getting_something_is_not_validated(self):
216 # The value in the database is trusted.
217 config = self.make_config()
218 example_value = factory.make_name('not-an-ip-address')
219 config.store[config.__class__.something.name] = example_value
220 self.assertEqual(example_value, config.something)
221
222 def test_setting_something(self):215 def test_setting_something(self):
223 config = self.make_config()216 config = self.make_config()
224 example_value = factory.make_ipv4_address()217 example_value = factory.make_ipv4_address()
225218
=== modified file 'src/provisioningserver/utils/config.py'
--- src/provisioningserver/utils/config.py 2016-08-19 10:22:29 +0000
+++ src/provisioningserver/utils/config.py 2016-10-07 18:00:08 +0000
@@ -124,11 +124,12 @@
124 "notDir": "%(value)r does not exist or is not a directory",124 "notDir": "%(value)r does not exist or is not a directory",
125 }125 }
126126
127 def _validate_other(self, value, state=None):127 def from_python(self, value, state=None):
128 # Only validate on the way _in_; it's not the store's fault if it128 # Only validate on the way _in_; it's not the store's fault if it
129 # contains a directory which has since been removed.129 # contains a directory which has since been removed. Check that the
130 if os.path.isdir(value):130 # original as-Python value represents a directory.
131 return value131 if self.is_empty(value) or os.path.isdir(value):
132 return super(DirectoryString, self).from_python(value, state)
132 else:133 else:
133 raise formencode.Invalid(134 raise formencode.Invalid(
134 self.message("notDir", state, value=value),135 self.message("notDir", state, value=value),
135136
=== modified file 'src/provisioningserver/utils/tests/test_config.py'
--- src/provisioningserver/utils/tests/test_config.py 2016-08-19 10:22:29 +0000
+++ src/provisioningserver/utils/tests/test_config.py 2016-10-07 18:00:08 +0000
@@ -146,22 +146,39 @@
146class TestDirectory(MAASTestCase):146class TestDirectory(MAASTestCase):
147 """Tests for `DirectoryString`."""147 """Tests for `DirectoryString`."""
148148
149 def test__validation_succeeds_when_directory_exists(self):149 def test__converting_succeeds_when_directory_exists(self):
150 directory = self.make_dir()150 directory = self.make_dir()
151 validator = config.DirectoryString(accept_python=False)151 validator = config.DirectoryString()
152 self.assertEqual(directory, validator.from_python(directory))152 self.assertEqual(directory, validator.from_python(directory))
153 self.assertEqual(directory, validator.to_python(directory))153 self.assertEqual(directory, validator.to_python(directory))
154154
155 def test__validation_fails_when_directory_does_not_exist(self):155 def test__converting_from_python_fails_when_directory_not_exists(self):
156 directory = os.path.join(self.make_dir(), "not-here")156 directory = os.path.join(self.make_dir(), "not-here")
157 validator = config.DirectoryString(accept_python=False)157 validator = config.DirectoryString()
158 expected_exception = ExpectedException(158 expected_exception = ExpectedException(
159 formencode.validators.Invalid, "^%s$" % re.escape(159 formencode.validators.Invalid, "^%s$" % re.escape(
160 "%r does not exist or is not a directory" % directory))160 "%r does not exist or is not a directory" % directory))
161 with expected_exception:161 with expected_exception:
162 validator.from_python(directory)162 validator.from_python(directory)
163 with expected_exception:163
164 validator.to_python(directory)164 def test__converting_to_python_succeeds_when_directory_not_exists(self):
165 directory = os.path.join(self.make_dir(), "not-here")
166 validator = config.DirectoryString()
167 self.assertEqual(directory, validator.to_python(directory))
168
169 def test__converting_succeeds_when_empty(self):
170 validator = config.DirectoryString()
171 self.assertIsNone(validator.from_python(None))
172 self.assertIsNone(validator.to_python(None))
173
174 def test__converting_fails_when_empty_and_not_empty_set(self):
175 validator = config.DirectoryString(not_empty=True)
176 expected_exception = ExpectedException(
177 formencode.validators.Invalid, "^Please enter a value$")
178 with expected_exception:
179 self.assertIsNone(validator.from_python(None))
180 with expected_exception:
181 self.assertIsNone(validator.to_python(None))
165182
166183
167class TestExtendedURL(MAASTestCase):184class TestExtendedURL(MAASTestCase):