Merge lp:~allenap/maas/power-poll-fewer--bug-1389007 into lp:~maas-committers/maas/trunk
- power-poll-fewer--bug-1389007
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Gavin Panella |
Approved revision: | no longer in the source branch. |
Merged at revision: | 4076 |
Proposed branch: | lp:~allenap/maas/power-poll-fewer--bug-1389007 |
Merge into: | lp:~maas-committers/maas/trunk |
Diff against target: |
1586 lines (+998/-184) 16 files modified
HACKING.txt (+9/-3) src/maasserver/migrations/0150_power_parameters_and_state_updated_field.py (+515/-0) src/maasserver/models/node.py (+13/-3) src/maasserver/models/tests/test_node.py (+8/-0) src/maasserver/models/timestampedmodel.py (+1/-0) src/maasserver/rpc/nodes.py (+88/-22) src/maasserver/rpc/tests/test_nodes.py (+133/-22) src/maasserver/rpc/tests/test_regionservice.py (+9/-2) src/maasserver/testing/factory.py (+23/-9) src/maasserver/websockets/handlers/device.py (+1/-0) src/maasserver/websockets/handlers/node.py (+3/-0) src/provisioningserver/pserv_services/node_power_monitor_service.py (+31/-39) src/provisioningserver/pserv_services/tests/test_node_power_monitor_service.py (+35/-47) src/provisioningserver/rpc/power.py (+45/-32) src/provisioningserver/rpc/region.py (+10/-5) src/provisioningserver/rpc/tests/test_power.py (+74/-0) |
To merge this branch: | bzr merge lp:~allenap/maas/power-poll-fewer--bug-1389007 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gavin Panella (community) | Approve | ||
Review via email: mp+263550@code.launchpad.net |
This proposal supersedes a proposal from 2014-11-24.
Commit message
Query node power states in smaller batches.
Description of the change
Big diff because migrations!
This branch was previously reviewed by Graham Binns, and approved. It
has changed only a little since then, but I'd appreciate a sanity check
before I land it.
This branch adds a power_state_updated field to Node, which records when
the power state was last checked, regardless of success or failure. It's
a simple way to rate-limit checks.
The reason it's needed is because ListNodePowerPa
returns a subset of all queryable nodes (at least for a large
installation). Each time it's called it returns a list of those nodes
that need checking *in priority order*. Nodes that have never had their
power state checked are done first, then the ones that have been queried
least recently.
We /could/ later use this new field to trigger an immediate check via
the API or UI; set it to NULL and it'll be in the next set of checks.
That's a bit of an abuse though.
In any case, this branch is meant to be a most-bang-
kind of thing. It doesn't back us into a corner, but does solve the
immediate problem in what I hope is an elegant and effective way.
Thanks!
Graham Binns (gmb) wrote : Posted in a previous version of this proposal | # |
Gavin Panella (allenap) wrote : Posted in a previous version of this proposal | # |
Ta muchly!
MAAS Lander (maas-lander) wrote : Posted in a previous version of this proposal | # |
The attempt to merge lp:~allenap/maas/power-poll-fewer--bug-1389007 into lp:maas failed. Below is the output from the failed tests.
Ign http://
Get:1 http://
Ign http://
Get:2 http://
Ign http://
Hit http://
Get:3 http://
Hit http://
Get:4 http://
Get:5 http://
Hit http://
Get:6 http://
Get:7 http://
Get:8 http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Get:9 http://
Get:10 http://
Get:11 http://
Get:12 http://
Hit http://
Hit http://
Ign http://
Ign http://
Fetched 1,250 kB in 2s (437 kB/s)
Reading package lists...
sudo DEBIAN_
--
MAAS Lander (maas-lander) wrote : Posted in a previous version of this proposal | # |
There are additional revisions which have not been approved in review. Please seek review and approval of these new revisions.
MAAS Lander (maas-lander) wrote : Posted in a previous version of this proposal | # |
The attempt to merge lp:~allenap/maas/power-poll-fewer--bug-1389007 into lp:maas failed. Below is the output from the failed tests.
Ign http://
Get:1 http://
Ign http://
Get:2 http://
Ign http://
Hit http://
Get:3 http://
Hit http://
Get:4 http://
Get:5 http://
Get:6 http://
Get:7 http://
Hit http://
Get:8 http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Get:9 http://
Get:10 http://
Get:11 http://
Get:12 http://
Hit http://
Hit http://
Ign http://
Ign http://
Fetched 1,250 kB in 3s (411 kB/s)
Reading package lists...
sudo DEBIAN_
--
MAAS Lander (maas-lander) wrote : Posted in a previous version of this proposal | # |
The attempt to merge lp:~allenap/maas/power-poll-fewer--bug-1389007 into lp:maas failed. Below is the output from the failed tests.
Ign http://
Get:1 http://
Get:2 http://
Ign http://
Ign http://
Hit http://
Get:3 http://
Hit http://
Get:4 http://
Get:5 http://
Hit http://
Get:6 http://
Get:7 http://
Get:8 http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Ign http://
Ign http://
Get:9 http://
Get:10 http://
Get:11 http://
Get:12 http://
Hit http://
Hit http://
Fetched 1,251 kB in 2s (424 kB/s)
Reading package lists...
sudo DEBIAN_
--
Gavin Panella (allenap) wrote : Posted in a previous version of this proposal | # |
This is ready to land, but I want to make sure it's okay to go into 1.7.1 first.
Gavin Panella (allenap) wrote : | # |
So, everyone's good with this, right?
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~allenap/maas/power-poll-fewer--bug-1389007 into lp:maas failed. Below is the output from the failed tests.
Ign http://
Get:1 http://
Get:2 http://
Ign http://
Ign http://
Hit http://
Get:3 http://
Hit http://
Get:4 http://
Get:5 http://
Get:6 http://
Hit http://
Get:7 http://
Hit http://
Hit http://
Hit http://
Hit http://
Get:8 http://
Hit http://
Get:9 http://
Hit http://
Hit http://
Get:10 http://
Get:11 http://
Get:12 http://
Hit http://
Hit http://
Ign http://
Ign http://
Fetched 1,860 kB in 2s (629 kB/s)
Reading package lists...
sudo DEBIAN_
--
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~allenap/maas/power-poll-fewer--bug-1389007 into lp:maas failed. Below is the output from the failed tests.
Ign http://
Get:1 http://
Get:2 http://
Ign http://
Ign http://
Hit http://
Get:3 http://
Hit http://
Get:4 http://
Get:5 http://
Hit http://
Get:6 http://
Get:7 http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Get:8 http://
Get:9 http://
Get:10 http://
Hit http://
Hit http://
Get:11 http://
Get:12 http://
Hit http://
Hit http://
Ign http://
Ign http://
Fetched 1,860 kB in 3s (547 kB/s)
Reading package lists...
sudo DEBIAN_
--
Preview Diff
1 | === modified file 'HACKING.txt' |
2 | --- HACKING.txt 2015-06-26 10:40:52 +0000 |
3 | +++ HACKING.txt 2015-07-07 16:59:53 +0000 |
4 | @@ -449,13 +449,19 @@ |
5 | |
6 | .. _schemamigration: http://south.aeracode.org/docs/commands.html#schemamigration |
7 | |
8 | -Once you've changed the code, run:: |
9 | +Once you've changed the code, ensure the database is running and |
10 | +contains the starting schema:: |
11 | + |
12 | + $ make services/database/@start |
13 | + $ make syncdb |
14 | + |
15 | +then generate the migration script with:: |
16 | |
17 | $ ./bin/maas-region-admin schemamigration maasserver --auto description_of_the_change |
18 | |
19 | This will generate a migration module named |
20 | -``src/maasserver/migrations/<auto_number>_description_of_the_change.py``. Don't |
21 | -forget to add that file to the project with:: |
22 | +``src/maasserver/migrations/<auto_number>_description_of_the_change.py``. |
23 | +Don't forget to add that file to the project with:: |
24 | |
25 | $ bzr add src/maasserver/migrations/<auto_number>_description_of_the_change.py |
26 | |
27 | |
28 | === added file 'src/maasserver/migrations/0150_power_parameters_and_state_updated_field.py' |
29 | --- src/maasserver/migrations/0150_power_parameters_and_state_updated_field.py 1970-01-01 00:00:00 +0000 |
30 | +++ src/maasserver/migrations/0150_power_parameters_and_state_updated_field.py 2015-07-07 16:59:53 +0000 |
31 | @@ -0,0 +1,515 @@ |
32 | +# -*- coding: utf-8 -*- |
33 | +from django.db import models |
34 | +from south.db import db |
35 | +from south.utils import datetime_utils as datetime |
36 | +from south.v2 import SchemaMigration |
37 | + |
38 | + |
39 | +class Migration(SchemaMigration): |
40 | + |
41 | + def forwards(self, orm): |
42 | + # Adding field 'Node.power_state_updated' |
43 | + db.add_column(u'maasserver_node', 'power_state_updated', |
44 | + self.gf('django.db.models.fields.DateTimeField')(default=None, null=True), |
45 | + keep_default=False) |
46 | + |
47 | + |
48 | + # Changing field 'Node.power_parameters' |
49 | + db.alter_column(u'maasserver_node', 'power_parameters', self.gf('maasserver.fields.JSONObjectField')(max_length=32768)) |
50 | + |
51 | + def backwards(self, orm): |
52 | + # Deleting field 'Node.power_state_updated' |
53 | + db.delete_column(u'maasserver_node', 'power_state_updated') |
54 | + |
55 | + |
56 | + # Changing field 'Node.power_parameters' |
57 | + db.alter_column(u'maasserver_node', 'power_parameters', self.gf('maasserver.fields.JSONObjectField')()) |
58 | + |
59 | + models = { |
60 | + u'auth.group': { |
61 | + 'Meta': {'object_name': 'Group'}, |
62 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
63 | + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), |
64 | + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) |
65 | + }, |
66 | + u'auth.permission': { |
67 | + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, |
68 | + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), |
69 | + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), |
70 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
71 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) |
72 | + }, |
73 | + u'auth.user': { |
74 | + 'Meta': {'object_name': 'User'}, |
75 | + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), |
76 | + 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}), |
77 | + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), |
78 | + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), |
79 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
80 | + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), |
81 | + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
82 | + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
83 | + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), |
84 | + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), |
85 | + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), |
86 | + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), |
87 | + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) |
88 | + }, |
89 | + u'contenttypes.contenttype': { |
90 | + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, |
91 | + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), |
92 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
93 | + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), |
94 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) |
95 | + }, |
96 | + u'maasserver.blockdevice': { |
97 | + 'Meta': {'ordering': "[u'id']", 'unique_together': "((u'node', u'name'),)", 'object_name': 'BlockDevice'}, |
98 | + 'block_size': ('django.db.models.fields.IntegerField', [], {}), |
99 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
100 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
101 | + 'id_path': ('django.db.models.fields.FilePathField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), |
102 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), |
103 | + 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}), |
104 | + 'size': ('django.db.models.fields.BigIntegerField', [], {}), |
105 | + 'tags': ('djorm_pgarray.fields.ArrayField', [], {'default': '[]', 'dbtype': "u'text'", 'blank': 'True'}), |
106 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}) |
107 | + }, |
108 | + u'maasserver.bootresource': { |
109 | + 'Meta': {'unique_together': "((u'name', u'architecture'),)", 'object_name': 'BootResource'}, |
110 | + 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}), |
111 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
112 | + 'extra': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}), |
113 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
114 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), |
115 | + 'rtype': ('django.db.models.fields.IntegerField', [], {'max_length': '10'}), |
116 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}) |
117 | + }, |
118 | + u'maasserver.bootresourcefile': { |
119 | + 'Meta': {'unique_together': "((u'resource_set', u'filetype'),)", 'object_name': 'BootResourceFile'}, |
120 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
121 | + 'extra': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}), |
122 | + 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}), |
123 | + 'filetype': ('django.db.models.fields.CharField', [], {'default': "u'root-tgz'", 'max_length': '20'}), |
124 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
125 | + 'largefile': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.LargeFile']"}), |
126 | + 'resource_set': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'files'", 'to': u"orm['maasserver.BootResourceSet']"}), |
127 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}) |
128 | + }, |
129 | + u'maasserver.bootresourceset': { |
130 | + 'Meta': {'unique_together': "((u'resource', u'version'),)", 'object_name': 'BootResourceSet'}, |
131 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
132 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
133 | + 'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}), |
134 | + 'resource': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'sets'", 'to': u"orm['maasserver.BootResource']"}), |
135 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}), |
136 | + 'version': ('django.db.models.fields.CharField', [], {'max_length': '255'}) |
137 | + }, |
138 | + u'maasserver.bootsource': { |
139 | + 'Meta': {'object_name': 'BootSource'}, |
140 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
141 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
142 | + 'keyring_data': ('maasserver.fields.EditableBinaryField', [], {'blank': 'True'}), |
143 | + 'keyring_filename': ('django.db.models.fields.FilePathField', [], {'max_length': '100', 'blank': 'True'}), |
144 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}), |
145 | + 'url': ('django.db.models.fields.URLField', [], {'unique': 'True', 'max_length': '200'}) |
146 | + }, |
147 | + u'maasserver.bootsourcecache': { |
148 | + 'Meta': {'object_name': 'BootSourceCache'}, |
149 | + 'arch': ('django.db.models.fields.CharField', [], {'max_length': '20'}), |
150 | + 'boot_source': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.BootSource']"}), |
151 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
152 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
153 | + 'label': ('django.db.models.fields.CharField', [], {'max_length': '20'}), |
154 | + 'os': ('django.db.models.fields.CharField', [], {'max_length': '20'}), |
155 | + 'release': ('django.db.models.fields.CharField', [], {'max_length': '20'}), |
156 | + 'subarch': ('django.db.models.fields.CharField', [], {'max_length': '20'}), |
157 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}) |
158 | + }, |
159 | + u'maasserver.bootsourceselection': { |
160 | + 'Meta': {'unique_together': "((u'boot_source', u'os', u'release'),)", 'object_name': 'BootSourceSelection'}, |
161 | + 'arches': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'text'", 'null': 'True', 'blank': 'True'}), |
162 | + 'boot_source': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.BootSource']"}), |
163 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
164 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
165 | + 'labels': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'text'", 'null': 'True', 'blank': 'True'}), |
166 | + 'os': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'blank': 'True'}), |
167 | + 'release': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'blank': 'True'}), |
168 | + 'subarches': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'text'", 'null': 'True', 'blank': 'True'}), |
169 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}) |
170 | + }, |
171 | + u'maasserver.candidatename': { |
172 | + 'Meta': {'unique_together': "((u'name', u'position'),)", 'object_name': 'CandidateName'}, |
173 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
174 | + 'name': ('django.db.models.fields.SlugField', [], {'max_length': '50'}), |
175 | + 'position': ('django.db.models.fields.IntegerField', [], {}) |
176 | + }, |
177 | + u'maasserver.componenterror': { |
178 | + 'Meta': {'object_name': 'ComponentError'}, |
179 | + 'component': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}), |
180 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
181 | + 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000'}), |
182 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
183 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}) |
184 | + }, |
185 | + u'maasserver.config': { |
186 | + 'Meta': {'object_name': 'Config'}, |
187 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
188 | + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), |
189 | + 'value': ('maasserver.fields.JSONObjectField', [], {'null': 'True'}) |
190 | + }, |
191 | + u'maasserver.dhcplease': { |
192 | + 'Meta': {'object_name': 'DHCPLease'}, |
193 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
194 | + 'ip': ('maasserver.fields.MAASIPAddressField', [], {'unique': 'True', 'max_length': '39'}), |
195 | + 'mac': ('maasserver.fields.MACAddressField', [], {}), |
196 | + 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}) |
197 | + }, |
198 | + u'maasserver.downloadprogress': { |
199 | + 'Meta': {'object_name': 'DownloadProgress'}, |
200 | + 'bytes_downloaded': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), |
201 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
202 | + 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'blank': 'True'}), |
203 | + 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}), |
204 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
205 | + 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}), |
206 | + 'size': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), |
207 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}) |
208 | + }, |
209 | + u'maasserver.event': { |
210 | + 'Meta': {'object_name': 'Event'}, |
211 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
212 | + 'description': ('django.db.models.fields.TextField', [], {'default': "u''", 'blank': 'True'}), |
213 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
214 | + 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}), |
215 | + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.EventType']"}), |
216 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}) |
217 | + }, |
218 | + u'maasserver.eventtype': { |
219 | + 'Meta': {'object_name': 'EventType'}, |
220 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
221 | + 'description': ('django.db.models.fields.CharField', [], {'max_length': '255'}), |
222 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
223 | + 'level': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), |
224 | + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), |
225 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}) |
226 | + }, |
227 | + u'maasserver.fabric': { |
228 | + 'Meta': {'object_name': 'Fabric'}, |
229 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
230 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
231 | + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}), |
232 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}) |
233 | + }, |
234 | + u'maasserver.filestorage': { |
235 | + 'Meta': {'unique_together': "((u'filename', u'owner'),)", 'object_name': 'FileStorage'}, |
236 | + 'content': ('metadataserver.fields.BinaryField', [], {'blank': 'True'}), |
237 | + 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}), |
238 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
239 | + 'key': ('django.db.models.fields.CharField', [], {'default': "u'083eacb2-24a9-11e5-b81e-00163edfc3e6'", 'unique': 'True', 'max_length': '36'}), |
240 | + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}) |
241 | + }, |
242 | + u'maasserver.filesystem': { |
243 | + 'Meta': {'object_name': 'Filesystem'}, |
244 | + 'block_device': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.BlockDevice']", 'unique': 'True', 'null': 'True', 'blank': 'True'}), |
245 | + 'create_params': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), |
246 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
247 | + 'filesystem_group': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'filesystems'", 'null': 'True', 'to': u"orm['maasserver.FilesystemGroup']"}), |
248 | + 'fstype': ('django.db.models.fields.CharField', [], {'default': "u'ext4'", 'max_length': '20'}), |
249 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
250 | + 'label': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), |
251 | + 'mount_params': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), |
252 | + 'mount_point': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), |
253 | + 'partition': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Partition']", 'unique': 'True', 'null': 'True', 'blank': 'True'}), |
254 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}), |
255 | + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'}) |
256 | + }, |
257 | + u'maasserver.filesystemgroup': { |
258 | + 'Meta': {'object_name': 'FilesystemGroup'}, |
259 | + 'create_params': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), |
260 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
261 | + 'group_type': ('django.db.models.fields.CharField', [], {'max_length': '20'}), |
262 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
263 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), |
264 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}), |
265 | + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'}) |
266 | + }, |
267 | + u'maasserver.interface': { |
268 | + 'Meta': {'ordering': "(u'created',)", 'object_name': 'Interface'}, |
269 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
270 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
271 | + 'ip_addresses': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['maasserver.StaticIPAddress']", 'null': 'True', 'blank': 'True'}), |
272 | + 'ipv4_params': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}), |
273 | + 'ipv6_params': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}), |
274 | + 'mac': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.MACAddress']", 'null': 'True', 'blank': 'True'}), |
275 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), |
276 | + 'params': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}), |
277 | + 'parents': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['maasserver.Interface']", 'null': 'True', 'through': u"orm['maasserver.InterfaceRelationship']", 'blank': 'True'}), |
278 | + 'tags': ('djorm_pgarray.fields.ArrayField', [], {'default': '[]', 'dbtype': "u'text'", 'blank': 'True'}), |
279 | + 'type': ('django.db.models.fields.CharField', [], {'max_length': '20'}), |
280 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}), |
281 | + 'vlan': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.VLAN']", 'on_delete': 'models.PROTECT'}) |
282 | + }, |
283 | + u'maasserver.interfacerelationship': { |
284 | + 'Meta': {'object_name': 'InterfaceRelationship'}, |
285 | + 'child': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'parent_relationships'", 'to': u"orm['maasserver.Interface']"}), |
286 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
287 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
288 | + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'children_relationships'", 'to': u"orm['maasserver.Interface']"}), |
289 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}) |
290 | + }, |
291 | + u'maasserver.largefile': { |
292 | + 'Meta': {'object_name': 'LargeFile'}, |
293 | + 'content': ('maasserver.fields.LargeObjectField', [], {}), |
294 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
295 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
296 | + 'sha256': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'}), |
297 | + 'total_size': ('django.db.models.fields.BigIntegerField', [], {}), |
298 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}) |
299 | + }, |
300 | + u'maasserver.licensekey': { |
301 | + 'Meta': {'unique_together': "((u'osystem', u'distro_series'),)", 'object_name': 'LicenseKey'}, |
302 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
303 | + 'distro_series': ('django.db.models.fields.CharField', [], {'max_length': '255'}), |
304 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
305 | + 'license_key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), |
306 | + 'osystem': ('django.db.models.fields.CharField', [], {'max_length': '255'}), |
307 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}) |
308 | + }, |
309 | + u'maasserver.macaddress': { |
310 | + 'Meta': {'ordering': "(u'created',)", 'object_name': 'MACAddress'}, |
311 | + 'cluster_interface': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['maasserver.NodeGroupInterface']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), |
312 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
313 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
314 | + 'ip_addresses': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.StaticIPAddress']", 'symmetrical': 'False', 'through': u"orm['maasserver.MACStaticIPAddressLink']", 'blank': 'True'}), |
315 | + 'mac_address': ('maasserver.fields.MACAddressField', [], {'unique': 'True'}), |
316 | + 'networks': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Network']", 'symmetrical': 'False', 'blank': 'True'}), |
317 | + 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']", 'null': 'True', 'blank': 'True'}), |
318 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}) |
319 | + }, |
320 | + u'maasserver.macstaticipaddresslink': { |
321 | + 'Meta': {'unique_together': "((u'ip_address', u'mac_address'),)", 'object_name': 'MACStaticIPAddressLink'}, |
322 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
323 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
324 | + 'ip_address': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.StaticIPAddress']", 'unique': 'True'}), |
325 | + 'mac_address': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.MACAddress']"}), |
326 | + 'nic_alias': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), |
327 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}) |
328 | + }, |
329 | + u'maasserver.network': { |
330 | + 'Meta': {'object_name': 'Network'}, |
331 | + 'default_gateway': ('maasserver.fields.MAASIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}), |
332 | + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), |
333 | + 'dns_servers': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), |
334 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
335 | + 'ip': ('maasserver.fields.MAASIPAddressField', [], {'unique': 'True', 'max_length': '39'}), |
336 | + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), |
337 | + 'netmask': ('maasserver.fields.MAASIPAddressField', [], {'max_length': '39'}), |
338 | + 'vlan_tag': ('django.db.models.fields.PositiveSmallIntegerField', [], {'unique': 'True', 'null': 'True', 'blank': 'True'}) |
339 | + }, |
340 | + u'maasserver.node': { |
341 | + 'Meta': {'object_name': 'Node'}, |
342 | + 'agent_name': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'null': 'True', 'blank': 'True'}), |
343 | + 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '31', 'null': 'True', 'blank': 'True'}), |
344 | + 'boot_type': ('django.db.models.fields.CharField', [], {'default': "u'fastpath'", 'max_length': '20'}), |
345 | + 'cpu_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), |
346 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
347 | + 'disable_ipv4': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
348 | + 'distro_series': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'blank': 'True'}), |
349 | + 'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}), |
350 | + 'error_description': ('django.db.models.fields.TextField', [], {'default': "u''", 'blank': 'True'}), |
351 | + 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'unique': 'True', 'max_length': '255', 'blank': 'True'}), |
352 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
353 | + 'installable': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), |
354 | + 'license_key': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}), |
355 | + 'memory': ('django.db.models.fields.IntegerField', [], {'default': '0'}), |
356 | + 'netboot': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), |
357 | + 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}), |
358 | + 'osystem': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'blank': 'True'}), |
359 | + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}), |
360 | + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "u'children'", 'null': 'True', 'blank': 'True', 'to': u"orm['maasserver.Node']"}), |
361 | + 'power_parameters': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'max_length': '32768', 'blank': 'True'}), |
362 | + 'power_state': ('django.db.models.fields.CharField', [], {'default': "u'unknown'", 'max_length': '10'}), |
363 | + 'power_state_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), |
364 | + 'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}), |
365 | + 'pxe_mac': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': u"orm['maasserver.MACAddress']", 'blank': 'True', 'null': 'True'}), |
366 | + 'routers': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'macaddr'", 'null': 'True', 'blank': 'True'}), |
367 | + 'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '10'}), |
368 | + 'swap_size': ('django.db.models.fields.BigIntegerField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), |
369 | + 'system_id': ('django.db.models.fields.CharField', [], {'default': "u'node-083c99ea-24a9-11e5-b81e-00163edfc3e6'", 'unique': 'True', 'max_length': '41'}), |
370 | + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Tag']", 'symmetrical': 'False'}), |
371 | + 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Token']", 'null': 'True'}), |
372 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}), |
373 | + 'zone': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Zone']", 'on_delete': 'models.SET_DEFAULT'}) |
374 | + }, |
375 | + u'maasserver.nodegroup': { |
376 | + 'Meta': {'object_name': 'NodeGroup'}, |
377 | + 'api_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '18'}), |
378 | + 'api_token': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Token']", 'unique': 'True'}), |
379 | + 'cluster_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100', 'blank': 'True'}), |
380 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
381 | + 'default_disable_ipv4': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
382 | + 'dhcp_key': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}), |
383 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
384 | + 'maas_url': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}), |
385 | + 'name': ('maasserver.models.nodegroup.DomainNameField', [], {'max_length': '80', 'blank': 'True'}), |
386 | + 'status': ('django.db.models.fields.IntegerField', [], {'default': '1'}), |
387 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}), |
388 | + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'}) |
389 | + }, |
390 | + u'maasserver.nodegroupinterface': { |
391 | + 'Meta': {'unique_together': "((u'nodegroup', u'name'),)", 'object_name': 'NodeGroupInterface'}, |
392 | + 'broadcast_ip': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}), |
393 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
394 | + 'foreign_dhcp_ip': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}), |
395 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
396 | + 'interface': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}), |
397 | + 'ip': ('maasserver.fields.MAASIPAddressField', [], {'max_length': '39'}), |
398 | + 'ip_range_high': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}), |
399 | + 'ip_range_low': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}), |
400 | + 'management': ('django.db.models.fields.IntegerField', [], {'default': '0'}), |
401 | + 'name': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}), |
402 | + 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}), |
403 | + 'router_ip': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}), |
404 | + 'static_ip_range_high': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}), |
405 | + 'static_ip_range_low': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}), |
406 | + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Subnet']", 'null': 'True', 'on_delete': 'models.PROTECT', 'blank': 'True'}), |
407 | + 'subnet_mask': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}), |
408 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}), |
409 | + 'vlan': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.VLAN']", 'on_delete': 'models.PROTECT'}) |
410 | + }, |
411 | + u'maasserver.partition': { |
412 | + 'Meta': {'object_name': 'Partition'}, |
413 | + 'bootable': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
414 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
415 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
416 | + 'partition_table': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'partitions'", 'to': u"orm['maasserver.PartitionTable']"}), |
417 | + 'size': ('django.db.models.fields.BigIntegerField', [], {}), |
418 | + 'start_offset': ('django.db.models.fields.BigIntegerField', [], {}), |
419 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}), |
420 | + 'uuid': ('django.db.models.fields.CharField', [], {'max_length': '36', 'unique': 'True', 'null': 'True', 'blank': 'True'}) |
421 | + }, |
422 | + u'maasserver.partitiontable': { |
423 | + 'Meta': {'object_name': 'PartitionTable'}, |
424 | + 'block_device': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.BlockDevice']"}), |
425 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
426 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
427 | + 'table_type': ('django.db.models.fields.CharField', [], {'default': "u'GPT'", 'max_length': '20'}), |
428 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}) |
429 | + }, |
430 | + u'maasserver.physicalblockdevice': { |
431 | + 'Meta': {'ordering': "[u'id']", 'object_name': 'PhysicalBlockDevice', '_ormbases': [u'maasserver.BlockDevice']}, |
432 | + u'blockdevice_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['maasserver.BlockDevice']", 'unique': 'True', 'primary_key': 'True'}), |
433 | + 'model': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), |
434 | + 'serial': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}) |
435 | + }, |
436 | + u'maasserver.space': { |
437 | + 'Meta': {'object_name': 'Space'}, |
438 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
439 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
440 | + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}), |
441 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}) |
442 | + }, |
443 | + u'maasserver.sshkey': { |
444 | + 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSHKey'}, |
445 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
446 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
447 | + 'key': ('django.db.models.fields.TextField', [], {}), |
448 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}), |
449 | + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}) |
450 | + }, |
451 | + u'maasserver.sslkey': { |
452 | + 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSLKey'}, |
453 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
454 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
455 | + 'key': ('django.db.models.fields.TextField', [], {}), |
456 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}), |
457 | + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}) |
458 | + }, |
459 | + u'maasserver.staticipaddress': { |
460 | + 'Meta': {'object_name': 'StaticIPAddress'}, |
461 | + 'alloc_type': ('django.db.models.fields.IntegerField', [], {'default': '0'}), |
462 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
463 | + 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'null': 'True', 'blank': 'True'}), |
464 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
465 | + 'ip': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'unique': 'True', 'null': 'True', 'blank': 'True'}), |
466 | + 'subnet': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Subnet']", 'null': 'True', 'blank': 'True'}), |
467 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}), |
468 | + 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}) |
469 | + }, |
470 | + u'maasserver.subnet': { |
471 | + 'Meta': {'unique_together': "((u'name', u'space'),)", 'object_name': 'Subnet'}, |
472 | + 'cidr': ('maasserver.fields.CIDRField', [], {'unique': 'True'}), |
473 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
474 | + 'dns_servers': ('djorm_pgarray.fields.ArrayField', [], {'default': '[]', 'dbtype': "u'text'", 'null': 'True', 'blank': 'True'}), |
475 | + 'gateway_ip': ('maasserver.fields.MAASIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}), |
476 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
477 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), |
478 | + 'space': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Space']", 'on_delete': 'models.PROTECT'}), |
479 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}), |
480 | + 'vlan': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.VLAN']", 'on_delete': 'models.PROTECT'}) |
481 | + }, |
482 | + u'maasserver.tag': { |
483 | + 'Meta': {'object_name': 'Tag'}, |
484 | + 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}), |
485 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
486 | + 'definition': ('django.db.models.fields.TextField', [], {'blank': 'True'}), |
487 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
488 | + 'kernel_opts': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), |
489 | + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}), |
490 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}) |
491 | + }, |
492 | + u'maasserver.userprofile': { |
493 | + 'Meta': {'object_name': 'UserProfile'}, |
494 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
495 | + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'}) |
496 | + }, |
497 | + u'maasserver.virtualblockdevice': { |
498 | + 'Meta': {'ordering': "[u'id']", 'object_name': 'VirtualBlockDevice', '_ormbases': [u'maasserver.BlockDevice']}, |
499 | + u'blockdevice_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['maasserver.BlockDevice']", 'unique': 'True', 'primary_key': 'True'}), |
500 | + 'filesystem_group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'virtual_devices'", 'to': u"orm['maasserver.FilesystemGroup']"}), |
501 | + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'}) |
502 | + }, |
503 | + u'maasserver.vlan': { |
504 | + 'Meta': {'unique_together': "((u'vid', u'fabric'), (u'name', u'fabric'))", 'object_name': 'VLAN'}, |
505 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
506 | + 'fabric': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Fabric']"}), |
507 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
508 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '256'}), |
509 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}), |
510 | + 'vid': ('django.db.models.fields.IntegerField', [], {}) |
511 | + }, |
512 | + u'maasserver.zone': { |
513 | + 'Meta': {'ordering': "[u'name']", 'object_name': 'Zone'}, |
514 | + 'created': ('django.db.models.fields.DateTimeField', [], {}), |
515 | + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), |
516 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
517 | + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}), |
518 | + 'updated': ('django.db.models.fields.DateTimeField', [], {}) |
519 | + }, |
520 | + u'piston.consumer': { |
521 | + 'Meta': {'object_name': 'Consumer'}, |
522 | + 'description': ('django.db.models.fields.TextField', [], {}), |
523 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
524 | + 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}), |
525 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), |
526 | + 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}), |
527 | + 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '16'}), |
528 | + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'consumers'", 'null': 'True', 'to': u"orm['auth.User']"}) |
529 | + }, |
530 | + u'piston.token': { |
531 | + 'Meta': {'object_name': 'Token'}, |
532 | + 'callback': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), |
533 | + 'callback_confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
534 | + 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Consumer']"}), |
535 | + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
536 | + 'is_approved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
537 | + 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}), |
538 | + 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}), |
539 | + 'timestamp': ('django.db.models.fields.IntegerField', [], {'default': '1436274410L'}), |
540 | + 'token_type': ('django.db.models.fields.IntegerField', [], {}), |
541 | + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tokens'", 'null': 'True', 'to': u"orm['auth.User']"}), |
542 | + 'verifier': ('django.db.models.fields.CharField', [], {'max_length': '10'}) |
543 | + } |
544 | + } |
545 | + |
546 | + complete_apps = ['maasserver'] |
547 | |
548 | === modified file 'src/maasserver/models/node.py' |
549 | --- src/maasserver/models/node.py 2015-07-06 14:00:55 +0000 |
550 | +++ src/maasserver/models/node.py 2015-07-07 16:59:53 +0000 |
551 | @@ -39,6 +39,7 @@ |
552 | BooleanField, |
553 | CASCADE, |
554 | CharField, |
555 | + DateTimeField, |
556 | ForeignKey, |
557 | IntegerField, |
558 | Manager, |
559 | @@ -96,7 +97,10 @@ |
560 | from maasserver.models.physicalblockdevice import PhysicalBlockDevice |
561 | from maasserver.models.staticipaddress import StaticIPAddress |
562 | from maasserver.models.tag import Tag |
563 | -from maasserver.models.timestampedmodel import TimestampedModel |
564 | +from maasserver.models.timestampedmodel import ( |
565 | + now, |
566 | + TimestampedModel, |
567 | +) |
568 | from maasserver.models.zone import Zone |
569 | from maasserver.node_status import ( |
570 | COMMISSIONING_LIKE_STATUSES, |
571 | @@ -462,14 +466,19 @@ |
572 | power_type = CharField( |
573 | max_length=10, null=False, blank=True, default='') |
574 | |
575 | - # JSON-encoded set of parameters for power control. |
576 | - power_parameters = JSONObjectField(blank=True, default="") |
577 | + # JSON-encoded set of parameters for power control, limited to 32kiB when |
578 | + # encoded as JSON. |
579 | + power_parameters = JSONObjectField( |
580 | + max_length=(2 ** 15), blank=True, default="") |
581 | |
582 | power_state = CharField( |
583 | max_length=10, null=False, blank=False, |
584 | choices=POWER_STATE_CHOICES, default=POWER_STATE.UNKNOWN, |
585 | editable=False) |
586 | |
587 | + power_state_updated = DateTimeField( |
588 | + null=True, blank=False, default=None, editable=False) |
589 | + |
590 | token = ForeignKey( |
591 | Token, db_index=True, null=True, editable=False, unique=False) |
592 | |
593 | @@ -1772,6 +1781,7 @@ |
594 | def update_power_state(self, power_state): |
595 | """Update a node's power state """ |
596 | self.power_state = power_state |
597 | + self.power_state_updated = now() |
598 | mark_ready = ( |
599 | self.status == NODE_STATUS.RELEASING and |
600 | power_state == POWER_STATE.OFF) |
601 | |
602 | === modified file 'src/maasserver/models/tests/test_node.py' |
603 | --- src/maasserver/models/tests/test_node.py 2015-07-01 13:16:09 +0000 |
604 | +++ src/maasserver/models/tests/test_node.py 2015-07-07 16:59:53 +0000 |
605 | @@ -64,6 +64,7 @@ |
606 | from maasserver.models.node import PowerInfo |
607 | from maasserver.models.signals import power as node_query |
608 | from maasserver.models.staticipaddress import StaticIPAddress |
609 | +from maasserver.models.timestampedmodel import now |
610 | from maasserver.models.user import create_auth_token |
611 | from maasserver.node_status import ( |
612 | get_failed_status, |
613 | @@ -1878,6 +1879,13 @@ |
614 | node.update_power_state(state) |
615 | self.assertEqual(state, reload_object(node).power_state) |
616 | |
617 | + def test_update_power_state_sets_last_updated_field(self): |
618 | + node = factory.make_Node(power_state_updated=None) |
619 | + self.assertIsNone(node.power_state_updated) |
620 | + state = factory.pick_enum(POWER_STATE) |
621 | + node.update_power_state(state) |
622 | + self.assertEqual(now(), reload_object(node).power_state_updated) |
623 | + |
624 | def test_update_power_state_readies_node_if_releasing(self): |
625 | node = factory.make_Node( |
626 | power_state=POWER_STATE.ON, status=NODE_STATUS.RELEASING, |
627 | |
628 | === modified file 'src/maasserver/models/timestampedmodel.py' |
629 | --- src/maasserver/models/timestampedmodel.py 2015-05-07 18:14:38 +0000 |
630 | +++ src/maasserver/models/timestampedmodel.py 2015-07-07 16:59:53 +0000 |
631 | @@ -13,6 +13,7 @@ |
632 | |
633 | __metaclass__ = type |
634 | __all__ = [ |
635 | + 'now', |
636 | 'TimestampedModel', |
637 | ] |
638 | |
639 | |
640 | === modified file 'src/maasserver/rpc/nodes.py' |
641 | --- src/maasserver/rpc/nodes.py 2015-05-07 18:14:38 +0000 |
642 | +++ src/maasserver/rpc/nodes.py 2015-07-07 16:59:53 +0000 |
643 | @@ -19,6 +19,9 @@ |
644 | "create_node", |
645 | ] |
646 | |
647 | +from datetime import timedelta |
648 | +from itertools import chain |
649 | + |
650 | from django.contrib.auth.models import User |
651 | from django.core.exceptions import ValidationError |
652 | from maasserver import exceptions |
653 | @@ -30,6 +33,7 @@ |
654 | Node, |
655 | NodeGroup, |
656 | ) |
657 | +from maasserver.models.timestampedmodel import now |
658 | from maasserver.utils.orm import transactional |
659 | from provisioningserver.rpc.exceptions import ( |
660 | CommissionNodeFailed, |
661 | @@ -59,35 +63,97 @@ |
662 | raise NodeStateViolation(e) |
663 | |
664 | |
665 | -@synchronous |
666 | -@transactional |
667 | -def list_cluster_nodes_power_parameters(uuid): |
668 | - """Query a cluster controller and return all of its nodes |
669 | - power parameters |
670 | - |
671 | - for :py:class:`~provisioningserver.rpc.region.ListNodePowerParameters`. |
672 | +def _gen_cluster_nodes_power_parameters(nodes): |
673 | + """Generate power parameters for `nodes`. |
674 | + |
675 | + These fulfil a subset of the return schema for the RPC call for |
676 | + :py:class:`~provisioningserver.rpc.region.ListNodePowerParameters`. |
677 | + |
678 | + :return: A generator yielding `dict`s. |
679 | """ |
680 | - try: |
681 | - nodegroup = NodeGroup.objects.get_by_natural_key(uuid) |
682 | - except NodeGroup.DoesNotExist: |
683 | - raise NoSuchCluster.from_uuid(uuid) |
684 | - else: |
685 | - power_info_by_node = ( |
686 | - (node, node.get_effective_power_info()) |
687 | - for node in nodegroup.node_set.exclude( |
688 | - status=NODE_STATUS.BROKEN).exclude(installable=False) |
689 | - ) |
690 | - return [ |
691 | - { |
692 | + five_minutes_ago = now() - timedelta(minutes=5) |
693 | + |
694 | + # This is meant to be temporary until all the power types support querying |
695 | + # the power state of a node. See the definition of QUERY_POWER_TYPES for |
696 | + # more information. |
697 | + from provisioningserver.rpc.power import QUERY_POWER_TYPES |
698 | + |
699 | + nodes_unchecked = ( |
700 | + nodes |
701 | + .filter(power_state_updated=None) |
702 | + .filter(power_type__in=QUERY_POWER_TYPES) |
703 | + .exclude(status=NODE_STATUS.BROKEN) |
704 | + .exclude(installable=False) |
705 | + ) |
706 | + nodes_checked = ( |
707 | + nodes |
708 | + .exclude(power_state_updated=None) |
709 | + .exclude(power_state_updated__gt=five_minutes_ago) |
710 | + .filter(power_type__in=QUERY_POWER_TYPES) |
711 | + .exclude(status=NODE_STATUS.BROKEN) |
712 | + .exclude(installable=False) |
713 | + .order_by("power_state_updated", "system_id") |
714 | + ) |
715 | + |
716 | + for node in chain(nodes_unchecked, nodes_checked): |
717 | + power_info = node.get_effective_power_info() |
718 | + if power_info.power_type is not None: |
719 | + yield { |
720 | 'system_id': node.system_id, |
721 | 'hostname': node.hostname, |
722 | 'power_state': node.power_state, |
723 | 'power_type': power_info.power_type, |
724 | 'context': power_info.power_parameters, |
725 | } |
726 | - for node, power_info in power_info_by_node |
727 | - if power_info.power_type is not None |
728 | - ] |
729 | + |
730 | + |
731 | +def _gen_up_to_json_limit(things, limit): |
732 | + """Yield until the combined JSON dump of those things would exceed `limit`. |
733 | + |
734 | + :param things: Any iterable whose elements can dumped as JSON. |
735 | + :return: A generator that yields items from `things` unmodified, and in |
736 | + order, though maybe not all of them. |
737 | + """ |
738 | + # Deduct the space required for brackets. json.dumps(), by default, does |
739 | + # not add padding, so it's just the opening and closing brackets. |
740 | + limit -= 2 |
741 | + |
742 | + for index, thing in enumerate(things): |
743 | + # Adjust the limit according the the size of thing. |
744 | + if index == 0: |
745 | + # A sole element does not need a delimiter. |
746 | + limit -= len(json.dumps(thing)) |
747 | + else: |
748 | + # There is a delimiter between this and the preceeding element. |
749 | + # json.dumps(), by default, uses ", ", i.e. 2 characters. |
750 | + limit -= len(json.dumps(thing)) + 2 |
751 | + |
752 | + # Check if we've reached the limit. |
753 | + if limit == 0: |
754 | + yield thing |
755 | + break |
756 | + elif limit > 0: |
757 | + yield thing |
758 | + else: |
759 | + break |
760 | + |
761 | + |
762 | +@synchronous |
763 | +@transactional |
764 | +def list_cluster_nodes_power_parameters(uuid): |
765 | + """Return power parameters for a cluster's nodes, in priority order. |
766 | + |
767 | + For :py:class:`~provisioningserver.rpc.region.ListNodePowerParameters`. |
768 | + """ |
769 | + try: |
770 | + nodegroup = NodeGroup.objects.get_by_natural_key(uuid) |
771 | + except NodeGroup.DoesNotExist: |
772 | + raise NoSuchCluster.from_uuid(uuid) |
773 | + else: |
774 | + nodes = nodegroup.node_set.all() |
775 | + details = _gen_cluster_nodes_power_parameters(nodes) |
776 | + details = _gen_up_to_json_limit(details, 60 * (2 ** 10)) # 60kiB |
777 | + return list(details) |
778 | |
779 | |
780 | @synchronous |
781 | |
782 | === modified file 'src/maasserver/rpc/tests/test_nodes.py' |
783 | --- src/maasserver/rpc/tests/test_nodes.py 2015-06-10 11:24:44 +0000 |
784 | +++ src/maasserver/rpc/tests/test_nodes.py 2015-07-07 16:59:53 +0000 |
785 | @@ -14,10 +14,16 @@ |
786 | __metaclass__ = type |
787 | __all__ = [] |
788 | |
789 | +from datetime import timedelta |
790 | +from itertools import imap |
791 | +import json |
792 | +from operator import attrgetter |
793 | import random |
794 | +from random import randint |
795 | |
796 | from django.core.exceptions import ValidationError |
797 | from maasserver.enum import NODE_STATUS |
798 | +from maasserver.models.timestampedmodel import now |
799 | from maasserver.rpc.nodes import ( |
800 | commission_node, |
801 | create_node, |
802 | @@ -47,12 +53,15 @@ |
803 | NodeStateViolation, |
804 | NoSuchNode, |
805 | ) |
806 | +from provisioningserver.rpc.power import QUERY_POWER_TYPES |
807 | from simplejson import dumps |
808 | from testtools import ExpectedException |
809 | from testtools.matchers import ( |
810 | Contains, |
811 | Equals, |
812 | + GreaterThan, |
813 | Is, |
814 | + LessThan, |
815 | Not, |
816 | ) |
817 | |
818 | @@ -321,25 +330,127 @@ |
819 | # Those tests have been left there for now because they also check |
820 | # that the return values are being formatted correctly for RPC. |
821 | |
822 | - def test_does_not_return_power_info_for_broken_nodes(self): |
823 | - cluster = factory.make_NodeGroup() |
824 | - broken_node = factory.make_Node( |
825 | - nodegroup=cluster, status=NODE_STATUS.BROKEN) |
826 | - |
827 | - power_parameters = list_cluster_nodes_power_parameters(cluster.uuid) |
828 | - returned_system_ids = [ |
829 | - power_params['system_id'] for power_params in power_parameters] |
830 | - |
831 | - self.assertThat( |
832 | - returned_system_ids, Not(Contains(broken_node.system_id))) |
833 | - |
834 | - def test_does_not_return_power_info_for_devices(self): |
835 | - cluster = factory.make_NodeGroup() |
836 | - device = factory.make_Device() |
837 | - |
838 | - power_parameters = list_cluster_nodes_power_parameters(cluster.uuid) |
839 | - returned_system_ids = [ |
840 | - power_params['system_id'] for power_params in power_parameters] |
841 | - |
842 | - self.assertThat( |
843 | - returned_system_ids, Not(Contains(device.system_id))) |
844 | + def make_Node( |
845 | + self, cluster, power_type=None, power_state_updated=None, |
846 | + **kwargs): |
847 | + if power_type is None: |
848 | + # Ensure that this node's power status can be queried. |
849 | + power_type = random.choice(QUERY_POWER_TYPES) |
850 | + if power_state_updated is None: |
851 | + # Ensure that this node was last queried at least 5 minutes ago. |
852 | + power_state_updated = now() - timedelta(minutes=randint(6, 16)) |
853 | + return factory.make_Node( |
854 | + nodegroup=cluster, power_type=power_type, |
855 | + power_state_updated=power_state_updated, **kwargs) |
856 | + |
857 | + def test__returns_unchecked_nodes_first(self): |
858 | + cluster = factory.make_NodeGroup() |
859 | + nodes = [self.make_Node(cluster) for _ in xrange(5)] |
860 | + node_unchecked = random.choice(nodes) |
861 | + node_unchecked.power_state_updated = None |
862 | + node_unchecked.save() |
863 | + |
864 | + power_parameters = list_cluster_nodes_power_parameters(cluster.uuid) |
865 | + system_ids = [params["system_id"] for params in power_parameters] |
866 | + |
867 | + # The unchecked node is always the first out. |
868 | + self.assertEqual(node_unchecked.system_id, system_ids[0]) |
869 | + |
870 | + def test__excludes_recently_checked_nodes(self): |
871 | + cluster = factory.make_NodeGroup() |
872 | + |
873 | + node_unchecked = self.make_Node(cluster) |
874 | + node_unchecked.power_state_updated = None |
875 | + node_unchecked.save() |
876 | + |
877 | + datetime_now = now() |
878 | + node_checked_recently = self.make_Node(cluster) |
879 | + node_checked_recently.power_state_updated = datetime_now |
880 | + node_checked_recently.save() |
881 | + |
882 | + datetime_10_minutes_ago = datetime_now - timedelta(minutes=10) |
883 | + node_checked_long_ago = self.make_Node(cluster) |
884 | + node_checked_long_ago.power_state_updated = datetime_10_minutes_ago |
885 | + node_checked_long_ago.save() |
886 | + |
887 | + power_parameters = list_cluster_nodes_power_parameters(cluster.uuid) |
888 | + system_ids = [params["system_id"] for params in power_parameters] |
889 | + |
890 | + self.assertItemsEqual( |
891 | + {node_unchecked.system_id, node_checked_long_ago.system_id}, |
892 | + system_ids) |
893 | + |
894 | + def test__excludes_unqueryable_power_types(self): |
895 | + cluster = factory.make_NodeGroup() |
896 | + node_queryable = self.make_Node(cluster) |
897 | + self.make_Node(cluster, "foobar") # Unqueryable power type. |
898 | + |
899 | + power_parameters = list_cluster_nodes_power_parameters(cluster.uuid) |
900 | + system_ids = [params["system_id"] for params in power_parameters] |
901 | + |
902 | + self.assertItemsEqual([node_queryable.system_id], system_ids) |
903 | + |
904 | + def test__excludes_broken_nodes(self): |
905 | + cluster = factory.make_NodeGroup() |
906 | + node_queryable = self.make_Node(cluster) |
907 | + |
908 | + self.make_Node(cluster, status=NODE_STATUS.BROKEN) |
909 | + self.make_Node( |
910 | + cluster, status=NODE_STATUS.BROKEN, power_state_updated=( |
911 | + now() - timedelta(minutes=10))) |
912 | + |
913 | + power_parameters = list_cluster_nodes_power_parameters(cluster.uuid) |
914 | + system_ids = [params["system_id"] for params in power_parameters] |
915 | + |
916 | + self.assertItemsEqual([node_queryable.system_id], system_ids) |
917 | + |
918 | + def test__excludes_devices(self): |
919 | + cluster = factory.make_NodeGroup() |
920 | + node_queryable = self.make_Node(cluster) |
921 | + |
922 | + factory.make_Device(nodegroup=cluster) |
923 | + factory.make_Device(nodegroup=cluster, power_type="ipmi") |
924 | + factory.make_Device( |
925 | + nodegroup=cluster, power_type="ipmi", power_state_updated=( |
926 | + now() - timedelta(minutes=10))) |
927 | + |
928 | + power_parameters = list_cluster_nodes_power_parameters(cluster.uuid) |
929 | + system_ids = [params["system_id"] for params in power_parameters] |
930 | + |
931 | + self.assertItemsEqual([node_queryable.system_id], system_ids) |
932 | + |
933 | + def test__returns_checked_nodes_in_last_checked_order(self): |
934 | + cluster = factory.make_NodeGroup() |
935 | + nodes = [self.make_Node(cluster) for _ in xrange(5)] |
936 | + |
937 | + power_parameters = list_cluster_nodes_power_parameters(cluster.uuid) |
938 | + system_ids = [params["system_id"] for params in power_parameters] |
939 | + |
940 | + # Checked nodes are always sorted from least recently checked to most. |
941 | + node_sort_key = attrgetter("power_state_updated", "system_id") |
942 | + nodes_in_order = sorted(nodes, key=node_sort_key) |
943 | + self.assertEqual( |
944 | + [node.system_id for node in nodes_in_order], |
945 | + system_ids) |
946 | + |
947 | + def test__returns_at_most_60kiB_of_JSON(self): |
948 | + cluster = factory.make_NodeGroup() |
949 | + |
950 | + # Ensure that there are at least 64kiB of power parameters (when |
951 | + # converted to JSON) in the database. |
952 | + example_parameters = {"key%d" % i: "value%d" % i for i in xrange(100)} |
953 | + remaining = 2 ** 16 |
954 | + while remaining > 0: |
955 | + node = self.make_Node(cluster, power_parameters=example_parameters) |
956 | + remaining -= len(json.dumps(node.get_effective_power_parameters())) |
957 | + |
958 | + nodes = list_cluster_nodes_power_parameters(cluster.uuid) |
959 | + |
960 | + # The total size of the JSON is less than 60kiB, but only a bit. |
961 | + nodes_json = imap(json.dumps, nodes) |
962 | + nodes_json_lengths = imap(len, nodes_json) |
963 | + nodes_json_length = sum(nodes_json_lengths) |
964 | + expected_maximum = 60 * (2 ** 10) # 60kiB |
965 | + self.expectThat(nodes_json_length, LessThan(expected_maximum + 1)) |
966 | + expected_minimum = 50 * (2 ** 10) # 50kiB |
967 | + self.expectThat(nodes_json_length, GreaterThan(expected_minimum - 1)) |
968 | |
969 | === modified file 'src/maasserver/rpc/tests/test_regionservice.py' |
970 | --- src/maasserver/rpc/tests/test_regionservice.py 2015-06-26 08:53:12 +0000 |
971 | +++ src/maasserver/rpc/tests/test_regionservice.py 2015-07-07 16:59:53 +0000 |
972 | @@ -103,6 +103,7 @@ |
973 | NoSuchNode, |
974 | ) |
975 | from provisioningserver.rpc.interfaces import IConnection |
976 | +from provisioningserver.rpc.power import QUERY_POWER_TYPES |
977 | from provisioningserver.rpc.region import ( |
978 | Authenticate, |
979 | CommissionNode, |
980 | @@ -655,7 +656,10 @@ |
981 | nodegroup = yield deferToThread(self.create_nodegroup) |
982 | nodes = [] |
983 | for _ in range(3): |
984 | - node = yield deferToThread(self.create_node, nodegroup) |
985 | + node = yield deferToThread( |
986 | + self.create_node, nodegroup, |
987 | + power_type=random.choice(QUERY_POWER_TYPES), |
988 | + power_state_updated=None) |
989 | power_params = yield deferToThread( |
990 | self.get_node_power_parameters, node) |
991 | nodes.append({ |
992 | @@ -668,12 +672,15 @@ |
993 | |
994 | # Create a node with an invalid power type (i.e. the empty string). |
995 | # This will not be reported by the call to ListNodePowerParameters. |
996 | - yield deferToThread(self.create_node, nodegroup, power_type="") |
997 | + yield deferToThread( |
998 | + self.create_node, nodegroup, power_type="", |
999 | + power_state_updated=None) |
1000 | |
1001 | response = yield call_responder( |
1002 | Region(), ListNodePowerParameters, |
1003 | {b'uuid': nodegroup.uuid}) |
1004 | |
1005 | + self.maxDiff = None |
1006 | self.assertItemsEqual(nodes, response['nodes']) |
1007 | |
1008 | @wait_for_reactor |
1009 | |
1010 | === modified file 'src/maasserver/testing/factory.py' |
1011 | --- src/maasserver/testing/factory.py 2015-07-01 21:32:53 +0000 |
1012 | +++ src/maasserver/testing/factory.py 2015-07-07 16:59:53 +0000 |
1013 | @@ -17,6 +17,7 @@ |
1014 | "Messages", |
1015 | ] |
1016 | |
1017 | +from datetime import timedelta |
1018 | import hashlib |
1019 | from io import BytesIO |
1020 | import logging |
1021 | @@ -25,6 +26,7 @@ |
1022 | |
1023 | from django.contrib.auth.models import User |
1024 | from django.test.client import RequestFactory |
1025 | +from django.utils import timezone |
1026 | from maasserver.clusterrpc.power_parameters import get_power_types |
1027 | from maasserver.enum import ( |
1028 | BOOT_RESOURCE_FILE_TYPE, |
1029 | @@ -118,6 +120,11 @@ |
1030 | ALL_NODE_STATES = map_enum(NODE_STATUS).values() |
1031 | |
1032 | |
1033 | +# Use `undefined` instead of `None` for default factory arguments when `None` |
1034 | +# is a reasonable value for the argument. |
1035 | +undefined = object() |
1036 | + |
1037 | + |
1038 | class Messages: |
1039 | """A class to record messages published by Django messaging |
1040 | framework. |
1041 | @@ -242,12 +249,13 @@ |
1042 | device.save() |
1043 | return device |
1044 | |
1045 | - def make_Node(self, mac=False, hostname=None, status=None, |
1046 | - architecture="i386/generic", installable=True, updated=None, |
1047 | - created=None, nodegroup=None, routers=None, zone=None, |
1048 | - power_type=None, networks=None, boot_type=None, |
1049 | - sortable_name=False, parent=None, power_state=None, |
1050 | - disable_ipv4=None, **kwargs): |
1051 | + def make_Node( |
1052 | + self, mac=False, hostname=None, status=None, |
1053 | + architecture="i386/generic", installable=True, updated=None, |
1054 | + created=None, nodegroup=None, routers=None, zone=None, |
1055 | + networks=None, boot_type=None, sortable_name=False, |
1056 | + power_type=None, power_parameters=None, power_state=None, |
1057 | + power_state_updated=undefined, disable_ipv4=None, **kwargs): |
1058 | """Make a :class:`Node`. |
1059 | |
1060 | :param sortable_name: If `True`, use a that will sort consistently |
1061 | @@ -271,8 +279,13 @@ |
1062 | zone = self.make_Zone() |
1063 | if power_type is None: |
1064 | power_type = 'ether_wake' |
1065 | + if power_parameters is None: |
1066 | + power_parameters = "" |
1067 | if power_state is None: |
1068 | power_state = self.pick_enum(POWER_STATE) |
1069 | + if power_state_updated is undefined: |
1070 | + power_state_updated = ( |
1071 | + timezone.now() - timedelta(minutes=random.randint(0, 15))) |
1072 | if disable_ipv4 is None: |
1073 | disable_ipv4 = self.pick_bool() |
1074 | if boot_type is None: |
1075 | @@ -280,9 +293,10 @@ |
1076 | node = Node( |
1077 | hostname=hostname, status=status, architecture=architecture, |
1078 | installable=installable, nodegroup=nodegroup, routers=routers, |
1079 | - zone=zone, power_type=power_type, disable_ipv4=disable_ipv4, |
1080 | - parent=parent, boot_type=boot_type, power_state=power_state, |
1081 | - **kwargs) |
1082 | + zone=zone, boot_type=boot_type, power_type=power_type, |
1083 | + power_parameters=power_parameters, power_state=power_state, |
1084 | + power_state_updated=power_state_updated, |
1085 | + disable_ipv4=disable_ipv4, **kwargs) |
1086 | self._save_node_unchecked(node) |
1087 | # We do not generate random networks by default because the limited |
1088 | # number of VLAN identifiers (4,094) makes it very likely to |
1089 | |
1090 | === modified file 'src/maasserver/websockets/handlers/device.py' |
1091 | --- src/maasserver/websockets/handlers/device.py 2015-06-16 21:10:26 +0000 |
1092 | +++ src/maasserver/websockets/handlers/device.py 2015-07-07 16:59:53 +0000 |
1093 | @@ -138,6 +138,7 @@ |
1094 | "boot_type", |
1095 | "status", |
1096 | "power_parameters", |
1097 | + "power_state_updated", |
1098 | "disable_ipv4", |
1099 | "osystem", |
1100 | "power_type", |
1101 | |
1102 | === modified file 'src/maasserver/websockets/handlers/node.py' |
1103 | --- src/maasserver/websockets/handlers/node.py 2015-07-07 10:12:26 +0000 |
1104 | +++ src/maasserver/websockets/handlers/node.py 2015-07-07 16:59:53 +0000 |
1105 | @@ -95,6 +95,9 @@ |
1106 | "token", |
1107 | "netboot", |
1108 | "agent_name", |
1109 | + # power_state_updated isn't needed in the client yet, plus it's |
1110 | + # not native to JSON. Omit for now. |
1111 | + "power_state_updated", |
1112 | ] |
1113 | list_fields = [ |
1114 | "system_id", |
1115 | |
1116 | === modified file 'src/provisioningserver/pserv_services/node_power_monitor_service.py' |
1117 | --- src/provisioningserver/pserv_services/node_power_monitor_service.py 2015-05-07 18:14:38 +0000 |
1118 | +++ src/provisioningserver/pserv_services/node_power_monitor_service.py 2015-07-07 16:59:53 +0000 |
1119 | @@ -27,10 +27,6 @@ |
1120 | ) |
1121 | from provisioningserver.rpc.power import query_all_nodes |
1122 | from provisioningserver.rpc.region import ListNodePowerParameters |
1123 | -from provisioningserver.utils.twisted import ( |
1124 | - pause, |
1125 | - retries, |
1126 | -) |
1127 | from twisted.application.internet import TimerService |
1128 | from twisted.internet.defer import inlineCallbacks |
1129 | from twisted.python import log |
1130 | @@ -42,7 +38,7 @@ |
1131 | class NodePowerMonitorService(TimerService, object): |
1132 | """Service to monitor the power status of all nodes in this cluster.""" |
1133 | |
1134 | - check_interval = timedelta(minutes=5).total_seconds() |
1135 | + check_interval = timedelta(seconds=15).total_seconds() |
1136 | max_nodes_at_once = 5 |
1137 | |
1138 | def __init__(self, cluster_uuid, clock=None): |
1139 | @@ -57,42 +53,38 @@ |
1140 | Log errors on failure, but do not propagate them up; that will |
1141 | stop the timed loop from running. |
1142 | """ |
1143 | - def query_nodes_failed(failure): |
1144 | + try: |
1145 | + client = getRegionClient() |
1146 | + except NoConnectionsAvailable: |
1147 | + maaslog.debug( |
1148 | + "Cannot monitor nodes' power status; " |
1149 | + "region not available.") |
1150 | + else: |
1151 | + d = self.query_nodes(client, uuid) |
1152 | + d.addErrback(self.query_nodes_failed, uuid) |
1153 | + return d |
1154 | + |
1155 | + @inlineCallbacks |
1156 | + def query_nodes(self, client, uuid): |
1157 | + # Get the nodes' power parameters from the region. Keep getting more |
1158 | + # power parameters until the region returns an empty list. |
1159 | + while True: |
1160 | + response = yield client(ListNodePowerParameters, uuid=uuid) |
1161 | + power_parameters = response['nodes'] |
1162 | + if len(power_parameters) > 0: |
1163 | + yield query_all_nodes( |
1164 | + power_parameters, max_concurrency=self.max_nodes_at_once, |
1165 | + clock=self.clock) |
1166 | + else: |
1167 | + break |
1168 | + |
1169 | + def query_nodes_failed(self, failure, uuid): |
1170 | + if failure.check(NoSuchCluster): |
1171 | + maaslog.error("Cluster %s is not recognised.", uuid) |
1172 | + else: |
1173 | # Log the error in full to the Twisted log. |
1174 | - log.err(failure) |
1175 | + log.err(failure, "Querying node power states.") |
1176 | # Log something concise to the MAAS log. |
1177 | maaslog.error( |
1178 | "Failed to query nodes' power status: %s", |
1179 | failure.getErrorMessage()) |
1180 | - |
1181 | - return self.query_nodes(uuid).addErrback(query_nodes_failed) |
1182 | - |
1183 | - @inlineCallbacks |
1184 | - def query_nodes(self, uuid): |
1185 | - # Retry a few times, since this service usually comes up before |
1186 | - # the RPC service. |
1187 | - for elapsed, remaining, wait in retries(15, 5, self.clock): |
1188 | - try: |
1189 | - client = getRegionClient() |
1190 | - except NoConnectionsAvailable: |
1191 | - yield pause(wait, self.clock) |
1192 | - else: |
1193 | - break |
1194 | - else: |
1195 | - maaslog.error( |
1196 | - "Cannot monitor nodes' power status; " |
1197 | - "region not available.") |
1198 | - return |
1199 | - |
1200 | - # Get the nodes' power parameters from the region. |
1201 | - try: |
1202 | - response = yield client(ListNodePowerParameters, uuid=uuid) |
1203 | - except NoSuchCluster: |
1204 | - maaslog.error( |
1205 | - "This cluster (%s) is not recognised by the region.", |
1206 | - uuid) |
1207 | - else: |
1208 | - node_power_parameters = response['nodes'] |
1209 | - yield query_all_nodes( |
1210 | - node_power_parameters, |
1211 | - max_concurrency=self.max_nodes_at_once, clock=self.clock) |
1212 | |
1213 | === modified file 'src/provisioningserver/pserv_services/tests/test_node_power_monitor_service.py' |
1214 | --- src/provisioningserver/pserv_services/tests/test_node_power_monitor_service.py 2015-05-22 15:52:13 +0000 |
1215 | +++ src/provisioningserver/pserv_services/tests/test_node_power_monitor_service.py 2015-07-07 16:59:53 +0000 |
1216 | @@ -17,10 +17,7 @@ |
1217 | |
1218 | from fixtures import FakeLogger |
1219 | from maastesting.factory import factory |
1220 | -from maastesting.matchers import ( |
1221 | - MockCalledOnceWith, |
1222 | - MockCallsMatch, |
1223 | -) |
1224 | +from maastesting.matchers import MockCalledOnceWith |
1225 | from maastesting.testcase import ( |
1226 | MAASTestCase, |
1227 | MAASTwistedRunTest, |
1228 | @@ -28,13 +25,14 @@ |
1229 | from maastesting.twisted import TwistedLoggerFixture |
1230 | from mock import ( |
1231 | ANY, |
1232 | - call, |
1233 | + sentinel, |
1234 | ) |
1235 | from provisioningserver.pserv_services import ( |
1236 | node_power_monitor_service as npms, |
1237 | ) |
1238 | from provisioningserver.rpc import ( |
1239 | exceptions, |
1240 | + getRegionClient, |
1241 | region, |
1242 | ) |
1243 | from provisioningserver.rpc.testing import MockClusterToRegionRPCFixture |
1244 | @@ -56,94 +54,84 @@ |
1245 | service = npms.NodePowerMonitorService(cluster_uuid) |
1246 | self.assertThat(service, MatchesStructure.byEquality( |
1247 | call=(service.try_query_nodes, (cluster_uuid,), {}), |
1248 | - step=(5 * 60), clock=None)) |
1249 | + step=15, clock=None)) |
1250 | |
1251 | def make_monitor_service(self): |
1252 | cluster_uuid = factory.make_UUID() |
1253 | service = npms.NodePowerMonitorService(cluster_uuid, Clock()) |
1254 | return cluster_uuid, service |
1255 | |
1256 | - def test_query_nodes_retries_getting_client(self): |
1257 | - cluster_uuid, service = self.make_monitor_service() |
1258 | - |
1259 | - getRegionClient = self.patch(npms, "getRegionClient") |
1260 | - getRegionClient.side_effect = exceptions.NoConnectionsAvailable |
1261 | - |
1262 | - def has_been_called_n_times(n): |
1263 | - calls = [call()] * n |
1264 | - return MockCallsMatch(*calls) |
1265 | - |
1266 | - maaslog = self.useFixture(FakeLogger("maas")) |
1267 | - |
1268 | - d = service.query_nodes(cluster_uuid) |
1269 | - # Immediately the first attempt to get a client happens. |
1270 | - self.assertThat(getRegionClient, has_been_called_n_times(1)) |
1271 | - self.assertFalse(d.called) |
1272 | - # Followed by 3 more attempts as time passes. |
1273 | - service.clock.pump((5, 5, 5)) |
1274 | - self.assertThat(getRegionClient, has_been_called_n_times(4)) |
1275 | - # query_nodes returns after 15 seconds. |
1276 | - self.assertTrue(d.called) |
1277 | - self.assertIsNone(extract_result(d)) |
1278 | - |
1279 | - # A simple message is logged, but even this may be too noisy. |
1280 | - self.assertIn( |
1281 | - "Cannot monitor nodes' power status; region not available.", |
1282 | - maaslog.output) |
1283 | - |
1284 | def test_query_nodes_calls_the_region(self): |
1285 | cluster_uuid, service = self.make_monitor_service() |
1286 | |
1287 | rpc_fixture = self.useFixture(MockClusterToRegionRPCFixture()) |
1288 | - client, io = rpc_fixture.makeEventLoop(region.ListNodePowerParameters) |
1289 | - client.ListNodePowerParameters.return_value = succeed({"nodes": []}) |
1290 | + proto_region, io = rpc_fixture.makeEventLoop( |
1291 | + region.ListNodePowerParameters) |
1292 | + proto_region.ListNodePowerParameters.return_value = succeed( |
1293 | + {"nodes": []}) |
1294 | |
1295 | - d = service.query_nodes(cluster_uuid) |
1296 | + d = service.query_nodes(getRegionClient(), cluster_uuid) |
1297 | io.flush() |
1298 | |
1299 | self.assertEqual(None, extract_result(d)) |
1300 | self.assertThat( |
1301 | - client.ListNodePowerParameters, |
1302 | + proto_region.ListNodePowerParameters, |
1303 | MockCalledOnceWith(ANY, uuid=cluster_uuid)) |
1304 | |
1305 | def test_query_nodes_calls_query_all_nodes(self): |
1306 | cluster_uuid, service = self.make_monitor_service() |
1307 | + service.max_nodes_at_once = sentinel.max_nodes_at_once |
1308 | + |
1309 | + example_power_parameters = { |
1310 | + "system_id": factory.make_UUID(), |
1311 | + "hostname": factory.make_hostname(), |
1312 | + "power_state": factory.make_name("power_state"), |
1313 | + "power_type": factory.make_name("power_type"), |
1314 | + "context": {}, |
1315 | + } |
1316 | |
1317 | rpc_fixture = self.useFixture(MockClusterToRegionRPCFixture()) |
1318 | - client, io = rpc_fixture.makeEventLoop(region.ListNodePowerParameters) |
1319 | - client.ListNodePowerParameters.return_value = succeed({"nodes": []}) |
1320 | + proto_region, io = rpc_fixture.makeEventLoop( |
1321 | + region.ListNodePowerParameters) |
1322 | + proto_region.ListNodePowerParameters.side_effect = [ |
1323 | + succeed({"nodes": [example_power_parameters]}), |
1324 | + succeed({"nodes": []}), |
1325 | + ] |
1326 | |
1327 | query_all_nodes = self.patch(npms, "query_all_nodes") |
1328 | |
1329 | - d = service.query_nodes(cluster_uuid) |
1330 | + d = service.query_nodes(getRegionClient(), cluster_uuid) |
1331 | io.flush() |
1332 | |
1333 | self.assertEqual(None, extract_result(d)) |
1334 | self.assertThat( |
1335 | query_all_nodes, |
1336 | MockCalledOnceWith( |
1337 | - [], max_concurrency=service.max_nodes_at_once, |
1338 | + [example_power_parameters], |
1339 | + max_concurrency=sentinel.max_nodes_at_once, |
1340 | clock=service.clock)) |
1341 | |
1342 | def test_query_nodes_copes_with_NoSuchCluster(self): |
1343 | cluster_uuid, service = self.make_monitor_service() |
1344 | |
1345 | rpc_fixture = self.useFixture(MockClusterToRegionRPCFixture()) |
1346 | - client, io = rpc_fixture.makeEventLoop(region.ListNodePowerParameters) |
1347 | - client.ListNodePowerParameters.return_value = fail( |
1348 | + proto_region, io = rpc_fixture.makeEventLoop( |
1349 | + region.ListNodePowerParameters) |
1350 | + proto_region.ListNodePowerParameters.return_value = fail( |
1351 | exceptions.NoSuchCluster.from_uuid(cluster_uuid)) |
1352 | |
1353 | - d = service.query_nodes(cluster_uuid) |
1354 | + d = service.query_nodes(getRegionClient(), cluster_uuid) |
1355 | + d.addErrback(service.query_nodes_failed, cluster_uuid) |
1356 | with FakeLogger("maas") as maaslog: |
1357 | io.flush() |
1358 | |
1359 | self.assertEqual(None, extract_result(d)) |
1360 | self.assertDocTestMatches( |
1361 | - "This cluster (...) is not recognised by the region.", |
1362 | - maaslog.output) |
1363 | + "Cluster ... is not recognised.", maaslog.output) |
1364 | |
1365 | def test_try_query_nodes_logs_other_errors(self): |
1366 | cluster_uuid, service = self.make_monitor_service() |
1367 | + self.patch(npms, "getRegionClient").return_value = sentinel.client |
1368 | |
1369 | query_nodes = self.patch(service, "query_nodes") |
1370 | query_nodes.return_value = fail( |
1371 | |
1372 | === modified file 'src/provisioningserver/rpc/power.py' |
1373 | --- src/provisioningserver/rpc/power.py 2015-07-07 10:16:40 +0000 |
1374 | +++ src/provisioningserver/rpc/power.py 2015-07-07 16:59:53 +0000 |
1375 | @@ -19,6 +19,7 @@ |
1376 | |
1377 | from datetime import timedelta |
1378 | from functools import partial |
1379 | +import sys |
1380 | |
1381 | from provisioningserver.drivers.power import PowerDriverRegistry |
1382 | from provisioningserver.events import ( |
1383 | @@ -383,41 +384,53 @@ |
1384 | # type, however this is left here to prevent PEBKAC. |
1385 | raise PowerActionFail("Unknown power_type '%s'" % power_type) |
1386 | |
1387 | - # Use increasing waiting times to work around race conditions that could |
1388 | - # arise when power querying the node. |
1389 | - for waiting_time in default_waiting_policy: |
1390 | - error = None |
1391 | - # Perform power query. |
1392 | + def check_power_state(state): |
1393 | + if state not in ("on", "off", "unknown"): |
1394 | + # This is considered an error. |
1395 | + raise PowerActionFail(state) |
1396 | + |
1397 | + # Capture errors as we go along. |
1398 | + exc_info = None, None, None |
1399 | + |
1400 | + if is_power_driver_available(power_type): |
1401 | + # New-style power drivers handle retries for themselves, so we only |
1402 | + # ever call them once. |
1403 | try: |
1404 | - # Check if power_type has PowerDriver support. |
1405 | - if is_power_driver_available(power_type): |
1406 | - power_state = yield perform_power_driver_query( |
1407 | - system_id, hostname, power_type, context) |
1408 | - else: |
1409 | + power_state = yield perform_power_driver_query( |
1410 | + system_id, hostname, power_type, context) |
1411 | + check_power_state(power_state) |
1412 | + except: |
1413 | + # Hold the error; it will be reported later. |
1414 | + exc_info = sys.exc_info() |
1415 | + else: |
1416 | + yield power_state_update(system_id, power_state) |
1417 | + returnValue(power_state) |
1418 | + else: |
1419 | + # Old-style power drivers need to be retried. Use increasing waiting |
1420 | + # times to work around race conditions that could arise when power |
1421 | + # querying the node. |
1422 | + for waiting_time in default_waiting_policy: |
1423 | + # Perform power query. |
1424 | + try: |
1425 | power_state = yield deferToThread( |
1426 | - perform_power_query, system_id, |
1427 | - hostname, power_type, context) |
1428 | - if power_state not in ("on", "off", "unknown"): |
1429 | - # This is considered an error. |
1430 | - raise PowerActionFail(power_state) |
1431 | - except PowerActionFail as e: |
1432 | - # Hold the error so if failure after retries, we can |
1433 | - # log the reason. |
1434 | - error = e |
1435 | - |
1436 | - # Wait before trying again. |
1437 | - yield pause(waiting_time, clock) |
1438 | - if is_power_driver_available(power_type): |
1439 | - break |
1440 | - continue |
1441 | - yield power_state_update(system_id, power_state) |
1442 | - returnValue(power_state) |
1443 | - |
1444 | - # Send node is broken, since query failed after the multiple retries. |
1445 | - message = "Node could not be queried %s (%s) %s" % ( |
1446 | - system_id, hostname, error) |
1447 | + perform_power_query, system_id, hostname, |
1448 | + power_type, context) |
1449 | + check_power_state(power_state) |
1450 | + except: |
1451 | + # Hold the error; it may be reported later. |
1452 | + exc_info = sys.exc_info() |
1453 | + # Wait before trying again. |
1454 | + yield pause(waiting_time, clock) |
1455 | + else: |
1456 | + yield power_state_update(system_id, power_state) |
1457 | + returnValue(power_state) |
1458 | + |
1459 | + # Reaching here means that things have gone wrong. |
1460 | + assert exc_info != (None, None, None) |
1461 | + exc_type, exc_value, exc_trace = exc_info |
1462 | + message = "Power state could not be queried: %s" % (exc_value,) |
1463 | yield power_query_failure(system_id, hostname, message) |
1464 | - raise PowerActionFail(error) |
1465 | + raise exc_type, exc_value, exc_trace |
1466 | |
1467 | |
1468 | def maaslog_report_success(node, power_state): |
1469 | |
1470 | === modified file 'src/provisioningserver/rpc/region.py' |
1471 | --- src/provisioningserver/rpc/region.py 2015-06-11 19:27:51 +0000 |
1472 | +++ src/provisioningserver/rpc/region.py 2015-07-07 16:59:53 +0000 |
1473 | @@ -221,11 +221,16 @@ |
1474 | |
1475 | |
1476 | class ListNodePowerParameters(amp.Command): |
1477 | - """Return the list of power parameters for nodes |
1478 | - that this cluster controls. |
1479 | - |
1480 | - Used to query all of the nodes that the cluster |
1481 | - composes. |
1482 | + """Return power parameters for the nodes in the specified cluster. |
1483 | + |
1484 | + This will only return power parameters for nodes that have power types for |
1485 | + which MAAS has a query capability. |
1486 | + |
1487 | + It will return nodes in priority order. Those nodes at the beginning of |
1488 | + the list should be queried first. |
1489 | + |
1490 | + It may return an empty list. This means that all nodes have been recently |
1491 | + queried. Take a break before asking again. |
1492 | |
1493 | :since: 1.7 |
1494 | """ |
1495 | |
1496 | === modified file 'src/provisioningserver/rpc/tests/test_power.py' |
1497 | --- src/provisioningserver/rpc/tests/test_power.py 2015-06-10 17:01:15 +0000 |
1498 | +++ src/provisioningserver/rpc/tests/test_power.py 2015-07-07 16:59:53 +0000 |
1499 | @@ -30,6 +30,7 @@ |
1500 | MAASTwistedRunTest, |
1501 | ) |
1502 | from maastesting.twisted import ( |
1503 | + always_fail_with, |
1504 | always_succeed_with, |
1505 | TwistedLoggerFixture, |
1506 | ) |
1507 | @@ -796,6 +797,79 @@ |
1508 | self.assertEqual("off", extract_result(d)) |
1509 | |
1510 | |
1511 | +class TestPowerQueryExceptions(MAASTestCase): |
1512 | + |
1513 | + scenarios = tuple( |
1514 | + (power_type, { |
1515 | + "power_type": power_type, |
1516 | + "func": ( # Function to invoke driver. |
1517 | + "perform_power_driver_query" |
1518 | + if power_type in PowerDriverRegistry |
1519 | + else "perform_power_query"), |
1520 | + "waits": ( # Pauses between retries. |
1521 | + [] if power_type in PowerDriverRegistry |
1522 | + else power.default_waiting_policy), |
1523 | + "calls": ( # No. of calls to the driver. |
1524 | + 1 if power_type in PowerDriverRegistry |
1525 | + else len(power.default_waiting_policy)), |
1526 | + }) |
1527 | + for power_type in power.QUERY_POWER_TYPES |
1528 | + ) |
1529 | + |
1530 | + def test_get_power_state_captures_all_exceptions(self): |
1531 | + logger_twisted = self.useFixture(TwistedLoggerFixture()) |
1532 | + logger_maaslog = self.useFixture(FakeLogger("maas")) |
1533 | + |
1534 | + # Avoid threads here. |
1535 | + self.patch(power, "deferToThread", maybeDeferred) |
1536 | + |
1537 | + exception_type = factory.make_exception_type() |
1538 | + exception_message = factory.make_string() |
1539 | + exception = exception_type(exception_message) |
1540 | + |
1541 | + # Pretend the query always fails with `exception`. |
1542 | + query = self.patch_autospec(power, self.func) |
1543 | + query.side_effect = always_fail_with(exception) |
1544 | + |
1545 | + # Intercept calls to power_query_failure(). |
1546 | + self.patch_autospec(power, "power_query_failure") |
1547 | + |
1548 | + system_id = factory.make_name('system_id') |
1549 | + hostname = factory.make_name('hostname') |
1550 | + context = sentinel.context |
1551 | + clock = Clock() |
1552 | + |
1553 | + d = power.get_power_state( |
1554 | + system_id, hostname, self.power_type, context, clock) |
1555 | + |
1556 | + # Crank through some number of retries. |
1557 | + for wait in self.waits: |
1558 | + self.assertFalse(d.called) |
1559 | + clock.advance(wait) |
1560 | + self.assertTrue(d.called) |
1561 | + |
1562 | + # Finally the exception from the query is raised. |
1563 | + self.assertRaises(exception_type, extract_result, d) |
1564 | + |
1565 | + # The broken power query function patched earlier was called the same |
1566 | + # number of times as there are steps in the default waiting policy. |
1567 | + expected_call = call(system_id, hostname, self.power_type, context) |
1568 | + expected_calls = [expected_call] * self.calls |
1569 | + self.assertThat(query, MockCallsMatch(*expected_calls)) |
1570 | + |
1571 | + # power_query_failure() was called once at the end with a message |
1572 | + # constructed using the error message we fabricated at the beginning. |
1573 | + expected_message = ( |
1574 | + "Power state could not be queried: %s" % exception_message) |
1575 | + self.assertThat(power.power_query_failure, MockCalledOnceWith( |
1576 | + system_id, hostname, expected_message)) |
1577 | + |
1578 | + # Nothing was logged to the Twisted log or to maaslog; that happens |
1579 | + # elsewhere, in maaslog_query_failure() and maaslog_query(). |
1580 | + self.assertEqual("", logger_twisted.output) |
1581 | + self.assertEqual("", logger_maaslog.output) |
1582 | + |
1583 | + |
1584 | class TestPowerQueryAsync(MAASTestCase): |
1585 | |
1586 | run_tests_with = MAASTwistedRunTest.make_factory(timeout=5) |
LGTM. I have some questions and concerns, but nothing to hold the branch back.