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