Merge lp:~blake-rouse/maas/add-boot-type-to-node into lp:~maas-committers/maas/trunk

Proposed by Blake Rouse
Status: Merged
Approved by: Blake Rouse
Approved revision: no longer in the source branch.
Merged at revision: 2574
Proposed branch: lp:~blake-rouse/maas/add-boot-type-to-node
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 760 lines (+399/-177)
6 files modified
src/maasserver/enum.py (+14/-0)
src/maasserver/migrations/0091_add_boot_type_to_node.py (+338/-0)
src/maasserver/models/node.py (+15/-42)
src/maasserver/models/tests/test_node.py (+14/-66)
src/maasserver/node_action.py (+7/-29)
src/maasserver/tests/test_node_action.py (+11/-40)
To merge this branch: bzr merge lp:~blake-rouse/maas/add-boot-type-to-node
Reviewer Review Type Date Requested Status
Raphaël Badin (community) Approve
Review via email: mp+227234@code.launchpad.net

Commit message

Converts the use-fastpath-installer tag into a property that is set on the Node.

Description of the change

Adds a boot_type field to the node model. This field is used instead of the "use-fastpath-installer" tag, to determine what boot method the node should be using. Since fastpath is now the default installer, it is set as the default for the field.

The migration handles reading the "use-fastpath-installer" tag on the node, and setting the correct boot_type.

To post a comment you must log in.
Revision history for this message
Raphaël Badin (rvb) wrote :

Looks good. I've got a couple of comment (see inline) but more importantly:

Now that boot_type is a property on the node, we don't really need the should_use_fastpath_installer/should_use_traditional_installer encapsulations anymore.

Actually, if you exclude the tests, should_use_fastpath_installer is used only once and should_use_traditional_installer is not even used anymore (!).

review: Approve
Revision history for this message
Raphaël Badin (rvb) wrote :

btw, we probably want to make sure boot_type is exposed on the API (read/write).

Revision history for this message
Blake Rouse (blake-rouse) wrote :

should_use_fastpath_installer/should_use_traditional_installer where removed in the following branch. Made the review very large, to remove it in this one. Wanted to keep the review as small as possible, :)

I will do a branch to make sure that boot_type is exposed over the API.

Thanks!

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

We just hit trouble with this migration: http://paste.ubuntu.com/7841190/

I suspect the error may be because the migration updates a field on the model which, in the schema revision the migration works on, hasn't actually been added yet.

If it weren't for that, I think the migration would still fail for an additional reason: as it turns out you can't add a column to a database table and then start populating that column in the same transaction. You either do it right away by setting a default, or you do it in a separate transaction. (A migration runs as a transaction, of course.)

On a final nitpick, do we know that updating every node individually and querying the tags table for each will perform sufficiently well on very large numbers of nodes?

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Filed that problem as bug 1343425.

Revision history for this message
Blake Rouse (blake-rouse) wrote :

Thanks. Shows you are working on it. Is that correct?
On Jul 23, 2014 5:28 PM, "Jeroen T. Vermeulen" <email address hidden> wrote:

> Filed that problem as bug 1343425.
> --
>
> https://code.launchpad.net/~blake-rouse/maas/add-boot-type-to-node/+merge/227234
> You are the owner of lp:~blake-rouse/maas/add-boot-type-to-node.
>

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Yes, I'm working on that bug.

By the way, some important things to be aware of when working on database migrations: the normal things you do in development, such as "make sampledata," perform the migrations on an empty database. So any code you have in there for existing data (“for all existing nodes...”) simply won't be exercised. Yet an existing installation will run through them on upgrade.

Django/South let you step manually through the consecutive schema versions — forwards and, where the migrations themselves support it, backwards. So there is a way to verify that a migration works on existing data: ‘make sampledata’ to populate some of the tables. Then start a development instance of your branch in one shell, using ‘make run’. Ensure that you have data that will exercise the migration properly, whether through the UI or a postgres shell by running ‘make dbharness’. For convenience I'll show how to migrate backwards first and then forwards, so you'll start with data to exercise the backwards migration. Once you're sure your data is right, step through your migration backwards using maas-region-admin's ‘migrate’ sub-command, such as:

    ./bin/maas-region-admin migrate maasserver src/maasserver/migrations/0090_initialise_nodegroupinterface_name

That command will try to take you back to schema version 0090, which precedes your migration 0091. (As it happens this crashes because the backwards step in migration 0091 gets the return type from get_or_create wrong, and passes a tuple instead of a model object.)

Now you're in a state similar to what an installed maas would have before installing your upgrade. Again verify that the data is as you would expect, pre-migration, and that migrating forwards will exercise your migration code well. Then, apply your migration with a command like:

    ./bin/maas-region-admin migrate maasserver src/maasserver/migrations/0091_add_boot_type_to_node.py

Now you're in your post-migration state. Verify that the migration did what you expected it to. (In this case it would crash for the reasons I gave in my earlier comment.)

Another possible problem just occurred to me: the migration code uses node.tags — which is regular model-based ORM. However while the schema migrations walk you through consecutive database schema versions, they don't change the model code. The model code will always be the current version as received with the migrations, which will usually be for a newer schema version than the version your migration was written for. Future model code won't necessarily work with that schema version. That's why we're supposed to use the ‘orm’ object for accessing the database in migrations, not the model code.

So if (arbitrary example) we were to rename node.tags to node.tags_set at the model level, I _think_ your migration would break again.

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Did some looking around the South documentation and the Internet at large, and it looks like node.tags is still the proper way to refer to a node's tags.

(In my version of the migration, I go the other way — tag.node_set — in order to eliminate the loop that queries tags for every node.)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/enum.py'
2--- src/maasserver/enum.py 2014-07-09 16:28:38 +0000
3+++ src/maasserver/enum.py 2014-07-18 14:52:34 +0000
4@@ -95,6 +95,20 @@
5 ADMIN = 'admin_node'
6
7
8+class NODE_BOOT:
9+ """Types of booting methods a node can use."""
10+ FASTPATH = 'fastpath' #: http://launchpad.net/curtin
11+ DEBIAN = 'di'
12+
13+
14+# Django choices for NODE_BOOT: sequence of tuples (key, UI
15+# representation).
16+NODE_BOOT_CHOICES = (
17+ (NODE_BOOT.FASTPATH, "Fastpath Installer"),
18+ (NODE_BOOT.DEBIAN, "Debian Installer"),
19+)
20+
21+
22 class PRESEED_TYPE:
23 """Types of preseed documents that can be generated."""
24 DEFAULT = ''
25
26=== added file 'src/maasserver/migrations/0091_add_boot_type_to_node.py'
27--- src/maasserver/migrations/0091_add_boot_type_to_node.py 1970-01-01 00:00:00 +0000
28+++ src/maasserver/migrations/0091_add_boot_type_to_node.py 2014-07-18 14:52:34 +0000
29@@ -0,0 +1,338 @@
30+from maasserver.enum import NODE_BOOT
31+from maasserver.models.timestampedmodel import now
32+from south.db import db
33+from south.v2 import SchemaMigration
34+
35+
36+class Migration(SchemaMigration):
37+
38+ def forwards(self, orm):
39+ # Adding field 'Node.boot_type'
40+ db.add_column(u'maasserver_node', 'boot_type',
41+ self.gf('django.db.models.fields.CharField')(default=u'fastpath', max_length=20),
42+ keep_default=False)
43+
44+ # Set the boot_type based on the tag set on Node.
45+ for node in orm['maasserver.Node'].objects.all():
46+ if node.tags.filter(name="use-fastpath-installer").exists():
47+ node.boot_type = NODE_BOOT.FASTPATH
48+ else:
49+ node.boot_type = NODE_BOOT.DEBIAN
50+ node.save()
51+
52+ # Delete the "use-fastpath-installer" tag, as it is no longer used
53+ orm['maasserver.Tag'].objects.filter(
54+ name="use-fastpath-installer").delete()
55+
56+
57+ def backwards(self, orm):
58+ # Create the "use-fastpath-installer" tag.
59+ current_time = now()
60+ fpi_tag = orm['maasserver.Tag'].objects.get_or_create(
61+ name="use-fastpath-installer", defaults={
62+ 'created': current_time,
63+ 'updated': current_time,
64+ })
65+
66+ # Add the "use-fastpath-installer" tag, to nodes that use that
67+ # boot_type.
68+ for node in orm['maasserver.Node'].objects.all():
69+ if node.boot_type == NODE_BOOT.FASTPATH:
70+ node.tags.add(fpi_tag)
71+ elif node.boot_type == NODE_BOOT.DEBIAN:
72+ if fpi_tag in node.tags.all():
73+ node.tags.remove(fpi_tag)
74+ node.save()
75+
76+ # Deleting field 'Node.boot_type'
77+ db.delete_column(u'maasserver_node', 'boot_type')
78+
79+
80+ models = {
81+ u'auth.group': {
82+ 'Meta': {'object_name': 'Group'},
83+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
84+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
85+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
86+ },
87+ u'auth.permission': {
88+ 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
89+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
90+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
91+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
92+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
93+ },
94+ u'auth.user': {
95+ 'Meta': {'object_name': 'User'},
96+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
97+ 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}),
98+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
99+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
100+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
101+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
102+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
103+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
104+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
105+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
106+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
107+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
108+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
109+ },
110+ u'contenttypes.contenttype': {
111+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
112+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
113+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
114+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
115+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
116+ },
117+ u'maasserver.bootimage': {
118+ 'Meta': {'unique_together': "((u'nodegroup', u'osystem', u'architecture', u'subarchitecture', u'release', u'purpose', u'label'),)", 'object_name': 'BootImage'},
119+ 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
120+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
121+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
122+ 'label': ('django.db.models.fields.CharField', [], {'default': "u'release'", 'max_length': '255'}),
123+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
124+ 'osystem': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
125+ 'purpose': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
126+ 'release': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
127+ 'subarchitecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
128+ 'supported_subarches': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
129+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
130+ 'xinstall_path': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'null': 'True', 'blank': 'True'}),
131+ 'xinstall_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '30', 'null': 'True', 'blank': 'True'})
132+ },
133+ u'maasserver.bootsource': {
134+ 'Meta': {'object_name': 'BootSource'},
135+ 'cluster': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}),
136+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
137+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
138+ 'keyring_data': ('maasserver.fields.EditableBinaryField', [], {'blank': 'True'}),
139+ 'keyring_filename': ('django.db.models.fields.FilePathField', [], {'max_length': '100', 'blank': 'True'}),
140+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
141+ 'url': ('django.db.models.fields.URLField', [], {'max_length': '200'})
142+ },
143+ u'maasserver.bootsourceselection': {
144+ 'Meta': {'object_name': 'BootSourceSelection'},
145+ 'arches': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'text'", 'null': 'True', 'blank': 'True'}),
146+ 'boot_source': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.BootSource']"}),
147+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
148+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
149+ 'labels': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'text'", 'null': 'True', 'blank': 'True'}),
150+ 'release': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'blank': 'True'}),
151+ 'subarches': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'text'", 'null': 'True', 'blank': 'True'}),
152+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
153+ },
154+ u'maasserver.componenterror': {
155+ 'Meta': {'object_name': 'ComponentError'},
156+ 'component': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
157+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
158+ 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000'}),
159+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
160+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
161+ },
162+ u'maasserver.config': {
163+ 'Meta': {'object_name': 'Config'},
164+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
165+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
166+ 'value': ('maasserver.fields.JSONObjectField', [], {'null': 'True'})
167+ },
168+ u'maasserver.dhcplease': {
169+ 'Meta': {'object_name': 'DHCPLease'},
170+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
171+ 'ip': ('maasserver.fields.MAASIPAddressField', [], {'unique': 'True', 'max_length': '39'}),
172+ 'mac': ('maasserver.fields.MACAddressField', [], {}),
173+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"})
174+ },
175+ u'maasserver.downloadprogress': {
176+ 'Meta': {'object_name': 'DownloadProgress'},
177+ 'bytes_downloaded': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
178+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
179+ 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'blank': 'True'}),
180+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
181+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
182+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
183+ 'size': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
184+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
185+ },
186+ u'maasserver.filestorage': {
187+ 'Meta': {'unique_together': "((u'filename', u'owner'),)", 'object_name': 'FileStorage'},
188+ 'content': ('metadataserver.fields.BinaryField', [], {'blank': 'True'}),
189+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
190+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
191+ 'key': ('django.db.models.fields.CharField', [], {'default': "u'a95c24b8-0dbb-11e4-a6c6-bcee7b78dc5b'", 'unique': 'True', 'max_length': '36'}),
192+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
193+ },
194+ u'maasserver.licensekey': {
195+ 'Meta': {'unique_together': "((u'osystem', u'distro_series'),)", 'object_name': 'LicenseKey'},
196+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
197+ 'distro_series': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
198+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
199+ 'license_key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
200+ 'osystem': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
201+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
202+ },
203+ u'maasserver.macaddress': {
204+ 'Meta': {'object_name': 'MACAddress'},
205+ 'cluster_interface': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['maasserver.NodeGroupInterface']", 'null': 'True', 'blank': 'True'}),
206+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
207+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
208+ 'ip_addresses': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.StaticIPAddress']", 'symmetrical': 'False', 'through': u"orm['maasserver.MACStaticIPAddressLink']", 'blank': 'True'}),
209+ 'mac_address': ('maasserver.fields.MACAddressField', [], {'unique': 'True'}),
210+ 'networks': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Network']", 'symmetrical': 'False', 'blank': 'True'}),
211+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}),
212+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
213+ },
214+ u'maasserver.macstaticipaddresslink': {
215+ 'Meta': {'unique_together': "((u'ip_address', u'mac_address'),)", 'object_name': 'MACStaticIPAddressLink'},
216+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
217+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
218+ 'ip_address': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.StaticIPAddress']", 'unique': 'True'}),
219+ 'mac_address': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.MACAddress']"}),
220+ 'nic_alias': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
221+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
222+ },
223+ u'maasserver.network': {
224+ 'Meta': {'object_name': 'Network'},
225+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
226+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
227+ 'ip': ('maasserver.fields.MAASIPAddressField', [], {'unique': 'True', 'max_length': '39'}),
228+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
229+ 'netmask': ('maasserver.fields.MAASIPAddressField', [], {'max_length': '39'}),
230+ 'vlan_tag': ('django.db.models.fields.PositiveSmallIntegerField', [], {'unique': 'True', 'null': 'True', 'blank': 'True'})
231+ },
232+ u'maasserver.node': {
233+ 'Meta': {'object_name': 'Node'},
234+ 'agent_name': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'null': 'True', 'blank': 'True'}),
235+ 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '31'}),
236+ 'boot_type': ('django.db.models.fields.CharField', [], {'default': "u'fastpath'", 'max_length': '20'}),
237+ 'cpu_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
238+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
239+ 'distro_series': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'blank': 'True'}),
240+ 'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
241+ 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'unique': 'True', 'max_length': '255', 'blank': 'True'}),
242+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
243+ 'license_key': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}),
244+ 'memory': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
245+ 'netboot': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
246+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}),
247+ 'osystem': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'blank': 'True'}),
248+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}),
249+ 'power_parameters': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
250+ 'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}),
251+ 'routers': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'macaddr'", 'null': 'True', 'blank': 'True'}),
252+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '10'}),
253+ 'storage': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
254+ 'system_id': ('django.db.models.fields.CharField', [], {'default': "u'node-a95b43ea-0dbb-11e4-a6c6-bcee7b78dc5b'", 'unique': 'True', 'max_length': '41'}),
255+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Tag']", 'symmetrical': 'False'}),
256+ 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Token']", 'null': 'True'}),
257+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
258+ 'zone': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Zone']", 'on_delete': 'models.SET_DEFAULT'})
259+ },
260+ u'maasserver.nodegroup': {
261+ 'Meta': {'object_name': 'NodeGroup'},
262+ 'api_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '18'}),
263+ 'api_token': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Token']", 'unique': 'True'}),
264+ 'cluster_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100', 'blank': 'True'}),
265+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
266+ 'dhcp_key': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
267+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
268+ 'maas_url': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
269+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
270+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
271+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
272+ 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'})
273+ },
274+ u'maasserver.nodegroupinterface': {
275+ 'Meta': {'unique_together': "((u'nodegroup', u'name'),)", 'object_name': 'NodeGroupInterface'},
276+ 'broadcast_ip': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
277+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
278+ 'foreign_dhcp_ip': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
279+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
280+ 'interface': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
281+ 'ip': ('maasserver.fields.MAASIPAddressField', [], {'max_length': '39'}),
282+ 'ip_range_high': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
283+ 'ip_range_low': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
284+ 'management': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
285+ 'name': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
286+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
287+ 'router_ip': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
288+ 'static_ip_range_high': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
289+ 'static_ip_range_low': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
290+ 'subnet_mask': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
291+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
292+ },
293+ u'maasserver.sshkey': {
294+ 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSHKey'},
295+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
296+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
297+ 'key': ('django.db.models.fields.TextField', [], {}),
298+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
299+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
300+ },
301+ u'maasserver.sslkey': {
302+ 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSLKey'},
303+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
304+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
305+ 'key': ('django.db.models.fields.TextField', [], {}),
306+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
307+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
308+ },
309+ u'maasserver.staticipaddress': {
310+ 'Meta': {'object_name': 'StaticIPAddress'},
311+ 'alloc_type': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
312+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
313+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
314+ 'ip': ('maasserver.fields.MAASIPAddressField', [], {'unique': 'True', 'max_length': '39'}),
315+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
316+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
317+ },
318+ u'maasserver.tag': {
319+ 'Meta': {'object_name': 'Tag'},
320+ 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
321+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
322+ 'definition': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
323+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
324+ 'kernel_opts': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
325+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
326+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
327+ },
328+ u'maasserver.userprofile': {
329+ 'Meta': {'object_name': 'UserProfile'},
330+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
331+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'})
332+ },
333+ u'maasserver.zone': {
334+ 'Meta': {'ordering': "[u'name']", 'object_name': 'Zone'},
335+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
336+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
337+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
338+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
339+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
340+ },
341+ u'piston.consumer': {
342+ 'Meta': {'object_name': 'Consumer'},
343+ 'description': ('django.db.models.fields.TextField', [], {}),
344+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
345+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
346+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
347+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
348+ 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '16'}),
349+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'consumers'", 'null': 'True', 'to': u"orm['auth.User']"})
350+ },
351+ u'piston.token': {
352+ 'Meta': {'object_name': 'Token'},
353+ 'callback': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
354+ 'callback_confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
355+ 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Consumer']"}),
356+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
357+ 'is_approved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
358+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
359+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
360+ 'timestamp': ('django.db.models.fields.IntegerField', [], {'default': '1405606037L'}),
361+ 'token_type': ('django.db.models.fields.IntegerField', [], {}),
362+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tokens'", 'null': 'True', 'to': u"orm['auth.User']"}),
363+ 'verifier': ('django.db.models.fields.CharField', [], {'max_length': '10'})
364+ }
365+ }
366+
367+ complete_apps = ['maasserver']
368
369=== modified file 'src/maasserver/models/node.py'
370--- src/maasserver/models/node.py 2014-07-16 02:34:10 +0000
371+++ src/maasserver/models/node.py 2014-07-18 14:52:34 +0000
372@@ -51,6 +51,8 @@
373 logger,
374 )
375 from maasserver.enum import (
376+ NODE_BOOT,
377+ NODE_BOOT_CHOICES,
378 NODE_PERMISSION,
379 NODE_STATUS,
380 NODE_STATUS_CHOICES,
381@@ -500,6 +502,8 @@
382 :ivar status: This `Node`'s status. See the vocabulary
383 :class:`NODE_STATUS`.
384 :ivar owner: This `Node`'s owner if it's in use, None otherwise.
385+ :ivar boot_type: This `Node`'s booting method. See the vocabulary
386+ :class:`NODE_BOOT`.
387 :ivar osystem: This `Node`'s booting operating system, if it's blank then
388 the default_osystem will be used.
389 :ivar distro_series: This `Node`'s booting distro series, if
390@@ -531,6 +535,9 @@
391 owner = ForeignKey(
392 User, default=None, blank=True, null=True, editable=False)
393
394+ boot_type = CharField(
395+ max_length=20, choices=NODE_BOOT_CHOICES, default=NODE_BOOT.FASTPATH)
396+
397 osystem = CharField(
398 max_length=20, blank=True, default='')
399
400@@ -1158,58 +1165,24 @@
401 def should_use_traditional_installer(self):
402 """Should this node be installed with the traditional installer?
403
404- By default, nodes should be installed with the traditional installer,
405- so this returns `True` when no `use-fastpath-installer` tag has been
406- defined.
407+ By default, nodes should be installed with the Fast Path installer.
408 """
409- return not self.should_use_fastpath_installer()
410+ return self.boot_type == NODE_BOOT.DEBIAN
411
412 def should_use_fastpath_installer(self):
413 """Should this node be installed with the Fast Path installer?
414
415- By default, nodes should be installed with the traditional installer,
416- so this returns `True` when the `use-fastpath-installer` has been
417- defined and `False` when it hasn't.
418+ By default, nodes should be installed with the Fast Path installer.
419 """
420- return self.tags.filter(name="use-fastpath-installer").exists()
421+ return self.boot_type == NODE_BOOT.FASTPATH
422
423 def use_traditional_installer(self):
424- """Set this node to be installed with the traditional installer.
425-
426- By default, nodes should be installed with the Traditional installer.
427-
428- :raises: :class:`RuntimeError` when the `use-traditional-installer`
429- tag is defined *with* an expression. The reason is that the tag
430- evaluation machinery will eventually ignore whatever changes you
431- make with this method.
432- """
433- uti_tag, _ = Tag.objects.get_or_create(
434- name="use-fastpath-installer")
435- if uti_tag.is_defined:
436- raise RuntimeError(
437- "The use-fastpath-installer tag is defined with an "
438- "expression. This expression must instead be updated to set "
439- "this node to install with the Debian installer.")
440- self.tags.remove(uti_tag)
441+ """Set this node to be installed with the traditional installer."""
442+ self.boot_type = NODE_BOOT.DEBIAN
443
444 def use_fastpath_installer(self):
445- """Set this node to be installed with the Fast Path Installer.
446-
447- By default, nodes should be installed with the Traditional Installer.
448-
449- :raises: :class:`RuntimeError` when the `use-fastpath-installer`
450- tag is defined *with* an expression. The reason is that the tag
451- evaluation machinery will eventually ignore whatever changes you
452- make with this method.
453- """
454- uti_tag, _ = Tag.objects.get_or_create(
455- name="use-fastpath-installer")
456- if uti_tag.is_defined:
457- raise RuntimeError(
458- "The use-fastpath-installer tag is defined with an "
459- "expression. This expression must instead be updated to set "
460- "this node to install with the fast installer.")
461- self.tags.add(uti_tag)
462+ """Set this node to be installed with the Fast Path Installer."""
463+ self.boot_type = NODE_BOOT.FASTPATH
464
465 def split_arch(self):
466 """Return architecture and subarchitecture, as a tuple."""
467
468=== modified file 'src/maasserver/models/tests/test_node.py'
469--- src/maasserver/models/tests/test_node.py 2014-07-16 14:12:13 +0000
470+++ src/maasserver/models/tests/test_node.py 2014-07-18 14:52:34 +0000
471@@ -23,6 +23,7 @@
472 from maasserver.clusterrpc.power_parameters import get_power_types
473 from maasserver.enum import (
474 IPADDRESS_TYPE,
475+ NODE_BOOT,
476 NODE_PERMISSION,
477 NODE_STATUS,
478 NODE_STATUS_CHOICES,
479@@ -41,7 +42,6 @@
480 MACAddress,
481 Node,
482 node as node_module,
483- Tag,
484 )
485 from maasserver.models.node import (
486 generate_hostname,
487@@ -1025,28 +1025,26 @@
488 expected_hostname = '%s.%s' % (hostname_without_domain, domain)
489 self.assertEqual(expected_hostname, node.fqdn)
490
491- def test_should_use_traditional_installer_by_default(self):
492+ def test_boot_type_has_fastpath_set_by_default(self):
493 node = factory.make_node()
494+ self.assertEqual(NODE_BOOT.FASTPATH, node.boot_type)
495+
496+ def test_should_use_traditional_installer_returns_True_for_debian(self):
497+ node = factory.make_node(boot_type=NODE_BOOT.DEBIAN)
498 self.assertTrue(node.should_use_traditional_installer())
499
500- def test_should_not_use_fastpath_installer_by_default(self):
501- node = factory.make_node()
502- self.assertFalse(node.should_use_fastpath_installer())
503-
504- def test_should_use_traditional_installer_not_when_tag_applies(self):
505- node = factory.make_node()
506- tag = factory.make_tag(name="use-fastpath-installer")
507- tag.save()
508- node.tags.add(tag)
509+ def test_should_use_traditional_installer_returns_False_for_fastpath(self):
510+ node = factory.make_node(boot_type=NODE_BOOT.FASTPATH)
511 self.assertFalse(node.should_use_traditional_installer())
512
513- def test_should_use_fastpath_installer_when_tag_applies(self):
514- node = factory.make_node()
515- tag = factory.make_tag(name="use-fastpath-installer")
516- tag.save()
517- node.tags.add(tag)
518+ def test_should_use_fastpath_installer_returns_True_for_fastpath(self):
519+ node = factory.make_node(boot_type=NODE_BOOT.FASTPATH)
520 self.assertTrue(node.should_use_fastpath_installer())
521
522+ def test_should_use_fastpath_installer_returns_False_for_debian(self):
523+ node = factory.make_node(boot_type=NODE_BOOT.DEBIAN)
524+ self.assertFalse(node.should_use_fastpath_installer())
525+
526 def test_use_xxx_installer(self):
527 # use_fastpath_installer() and use_traditional_installer() can be used
528 # to affect what the should_use_xxx_installer() methods return.
529@@ -1058,56 +1056,6 @@
530 self.assertTrue(node.should_use_fastpath_installer())
531 self.assertFalse(node.should_use_traditional_installer())
532
533- def test_use_traditional_installer_dissociates_tag_from_node(self):
534- # use_traditional_installer removes any association with the
535- # use-fastpath-installer tag. The tag is created even if it did not
536- # previously exist. If it does already exist, it is not deleted.
537- find_tag = lambda: list(
538- Tag.objects.filter(name="use-fastpath-installer"))
539- node = factory.make_node()
540- node.use_traditional_installer()
541- self.assertNotEqual([], find_tag())
542- node.use_fastpath_installer()
543- node.use_traditional_installer()
544- self.assertNotEqual([], find_tag())
545-
546- def test_use_fastpath_installer_associates_tag_with_node(self):
547- # use_traditional_installer() creates the use-traditional-installer
548- # tag when it is first needed, and associates it with the node.
549- find_tag = lambda: list(
550- Tag.objects.filter(name="use-fastpath-installer"))
551- self.assertEqual([], find_tag())
552- node = factory.make_node()
553- node.use_fastpath_installer()
554- self.assertNotEqual([], find_tag())
555-
556- def test_use_traditional_installer_complains_when_tag_has_expression(self):
557- # use_traditional_installer() complains when the use-fastpath-installer
558- # tag exists and is defined with an expression.
559- node = factory.make_node()
560- factory.make_tag(
561- name="use-fastpath-installer",
562- definition="//something")
563- error = self.assertRaises(
564- RuntimeError, node.use_traditional_installer)
565- self.assertIn(
566- "The use-fastpath-installer tag is defined with an expression",
567- unicode(error))
568-
569- def test_use_fastpath_installer_complains_when_tag_has_expression(self):
570- # use_fastpath_installer() complains when the
571- # use-fastpath-installer tag exists and is defined with an
572- # expression.
573- node = factory.make_node()
574- factory.make_tag(
575- name="use-fastpath-installer",
576- definition="//something")
577- error = self.assertRaises(
578- RuntimeError, node.use_fastpath_installer)
579- self.assertIn(
580- "The use-fastpath-installer tag is defined with an expression",
581- unicode(error))
582-
583 def test_split_arch_returns_arch_as_tuple(self):
584 main_arch = factory.make_name('arch')
585 sub_arch = factory.make_name('subarch')
586
587=== modified file 'src/maasserver/node_action.py'
588--- src/maasserver/node_action.py 2014-07-11 13:28:41 +0000
589+++ src/maasserver/node_action.py 2014-07-18 14:52:34 +0000
590@@ -35,6 +35,7 @@
591
592 from django.core.urlresolvers import reverse
593 from maasserver.enum import (
594+ NODE_BOOT,
595 NODE_PERMISSION,
596 NODE_STATUS,
597 NODE_STATUS_CHOICES_DICT,
598@@ -48,7 +49,6 @@
599 Node,
600 SSHKey,
601 )
602-from maasserver.models.tag import Tag
603 from maasserver.utils import map_enum
604
605 # All node statuses.
606@@ -212,25 +212,14 @@
607
608 def is_permitted(self):
609 permitted = super(UseCurtin, self).is_permitted()
610- return permitted and self.node.should_use_traditional_installer()
611+ return permitted and self.node.boot_type == NODE_BOOT.DEBIAN
612
613 def execute(self, allow_redirect=True):
614 """See `NodeAction.execute`."""
615- self.node.use_fastpath_installer()
616+ self.node.boot_type = NODE_BOOT.FASTPATH
617+ self.node.save()
618 return "Node marked as using curtin for install."
619
620- def inhibit(self):
621- """Inhibit if ``use-fastpath-installer`` uses an expression."""
622- tag, _ = Tag.objects.get_or_create(name="use-fastpath-installer")
623- if tag.is_defined:
624- return dedent("""\
625- The use-fastpath-installer tag is defined with an
626- expression. This expression must instead be updated to set
627- this node to install with the fast installer.
628- """)
629- else:
630- return None
631-
632
633 class UseDI(NodeAction):
634 """Set this node to use d-i for installation."""
635@@ -242,25 +231,14 @@
636
637 def is_permitted(self):
638 permitted = super(UseDI, self).is_permitted()
639- return permitted and self.node.should_use_fastpath_installer()
640+ return permitted and self.node.boot_type == NODE_BOOT.FASTPATH
641
642 def execute(self, allow_redirect=True):
643 """See `NodeAction.execute`."""
644- self.node.use_traditional_installer()
645+ self.node.boot_type = NODE_BOOT.DEBIAN
646+ self.node.save()
647 return "Node marked as using the Debian installer."
648
649- def inhibit(self):
650- """Inhibit if ``use-fastpath-installer`` uses an expression."""
651- tag, _ = Tag.objects.get_or_create(name="use-fastpath-installer")
652- if tag.is_defined:
653- return dedent("""\
654- The use-fastpath-installer tag is defined with an
655- expression. This expression must instead be updated to set
656- this node to install with the Debian installer.
657- """)
658- else:
659- return None
660-
661
662 class StartNode(NodeAction):
663 """Acquire and start a node."""
664
665=== modified file 'src/maasserver/tests/test_node_action.py'
666--- src/maasserver/tests/test_node_action.py 2014-07-16 14:12:13 +0000
667+++ src/maasserver/tests/test_node_action.py 2014-07-18 14:52:34 +0000
668@@ -18,6 +18,7 @@
669
670 from django.core.urlresolvers import reverse
671 from maasserver.enum import (
672+ NODE_BOOT,
673 NODE_PERMISSION,
674 NODE_STATUS,
675 NODE_STATUS_CHOICES,
676@@ -316,75 +317,45 @@
677
678 class TestUseCurtinNodeAction(MAASServerTestCase):
679
680- def test_sets_tag(self):
681+ def test_sets_boot_type(self):
682 user = factory.make_user()
683- node = factory.make_node(owner=user)
684- node.use_traditional_installer()
685+ node = factory.make_node(owner=user, boot_type=NODE_BOOT.DEBIAN)
686 action = UseCurtin(node, user)
687 self.assertTrue(action.is_permitted())
688 action.execute()
689- self.assertTrue(node.should_use_fastpath_installer())
690+ self.assertEqual(NODE_BOOT.FASTPATH, node.boot_type)
691
692 def test_requires_edit_permission(self):
693 user = factory.make_user()
694- node = factory.make_node()
695- node.use_traditional_installer()
696+ node = factory.make_node(boot_type=NODE_BOOT.DEBIAN)
697 self.assertFalse(UseCurtin(node, user).is_permitted())
698
699 def test_not_permitted_if_already_uses_curtin(self):
700- node = factory.make_node()
701- node.use_fastpath_installer()
702+ node = factory.make_node(boot_type=NODE_BOOT.FASTPATH)
703 user = factory.make_admin()
704 self.assertFalse(UseCurtin(node, user).is_permitted())
705
706- def test_inhibited_if_use_fastpath_installer_tag_uses_expr(self):
707- make_use_fastpath_installer_tag_with_expression()
708- node = factory.make_node()
709- user = factory.make_admin()
710- self.assertDocTestMatches(
711- """\
712- The use-fastpath-installer tag is defined with an
713- expression. This expression must instead be updated to set
714- this node to install with the fast installer.
715- """,
716- UseCurtin(node, user).inhibit())
717-
718
719 class TestUseDINodeAction(MAASServerTestCase):
720
721- def test_sets_tag(self):
722+ def test_sets_boot_type(self):
723 user = factory.make_user()
724- node = factory.make_node(owner=user)
725- node.use_fastpath_installer()
726+ node = factory.make_node(owner=user, boot_type=NODE_BOOT.FASTPATH)
727 action = UseDI(node, user)
728 self.assertTrue(action.is_permitted())
729 action.execute()
730- self.assertTrue(node.should_use_traditional_installer())
731+ self.assertEqual(NODE_BOOT.DEBIAN, node.boot_type)
732
733 def test_requires_edit_permission(self):
734 user = factory.make_user()
735- node = factory.make_node()
736- node.use_fastpath_installer()
737+ node = factory.make_node(boot_type=NODE_BOOT.FASTPATH)
738 self.assertFalse(UseDI(node, user).is_permitted())
739
740 def test_not_permitted_if_already_uses_di(self):
741- node = factory.make_node()
742- node.use_traditional_installer()
743+ node = factory.make_node(boot_type=NODE_BOOT.DEBIAN)
744 user = factory.make_admin()
745 self.assertFalse(UseDI(node, user).is_permitted())
746
747- def test_inhibited_if_use_fastpath_installer_tag_uses_expr(self):
748- make_use_fastpath_installer_tag_with_expression()
749- node = factory.make_node()
750- user = factory.make_admin()
751- self.assertDocTestMatches(
752- """\
753- The use-fastpath-installer tag is defined with an
754- expression. This expression must instead be updated to set
755- this node to install with the Debian installer.
756- """,
757- UseDI(node, user).inhibit())
758-
759
760 class TestMarkBrokenAction(MAASServerTestCase):
761