Merge lp:~allenap/maas/power-poll-fewer--bug-1389007--1.8 into lp:maas/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
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.

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

Selfie (backport).

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'HACKING.txt'
--- HACKING.txt 2015-05-20 23:53:24 +0000
+++ HACKING.txt 2015-07-15 12:15:32 +0000
@@ -449,13 +449,19 @@
449449
450.. _schemamigration: http://south.aeracode.org/docs/commands.html#schemamigration450.. _schemamigration: http://south.aeracode.org/docs/commands.html#schemamigration
451451
452Once you've changed the code, run::452Once you've changed the code, ensure the database is running and
453contains the starting schema::
454
455 $ make services/database/@start
456 $ make syncdb
457
458then generate the migration script with::
453459
454 $ ./bin/maas-region-admin schemamigration maasserver --auto description_of_the_change460 $ ./bin/maas-region-admin schemamigration maasserver --auto description_of_the_change
455461
456This will generate a migration module named462This will generate a migration module named
457``src/maasserver/migrations/<auto_number>_description_of_the_change.py``. Don't463``src/maasserver/migrations/<auto_number>_description_of_the_change.py``.
458forget to add that file to the project with::464Don't forget to add that file to the project with::
459465
460 $ bzr add src/maasserver/migrations/<auto_number>_description_of_the_change.py466 $ bzr add src/maasserver/migrations/<auto_number>_description_of_the_change.py
461467
462468
=== added file 'src/maasserver/migrations/0139_power_parameters_and_state_updated_field.py'
--- src/maasserver/migrations/0139_power_parameters_and_state_updated_field.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/migrations/0139_power_parameters_and_state_updated_field.py 2015-07-15 12:15:32 +0000
@@ -0,0 +1,453 @@
1# -*- coding: utf-8 -*-
2from django.db import models
3from south.db import db
4from south.utils import datetime_utils as datetime
5from south.v2 import SchemaMigration
6
7
8class Migration(SchemaMigration):
9
10 def forwards(self, orm):
11 # Adding field 'Node.power_state_updated'
12 db.add_column(u'maasserver_node', 'power_state_updated',
13 self.gf('django.db.models.fields.DateTimeField')(default=None, null=True),
14 keep_default=False)
15
16
17 # Changing field 'Node.power_parameters'
18 db.alter_column(u'maasserver_node', 'power_parameters', self.gf('maasserver.fields.JSONObjectField')(max_length=32768))
19
20 def backwards(self, orm):
21 # Deleting field 'Node.power_state_updated'
22 db.delete_column(u'maasserver_node', 'power_state_updated')
23
24
25 # Changing field 'Node.power_parameters'
26 db.alter_column(u'maasserver_node', 'power_parameters', self.gf('maasserver.fields.JSONObjectField')())
27
28 models = {
29 u'auth.group': {
30 'Meta': {'object_name': 'Group'},
31 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
32 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
33 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
34 },
35 u'auth.permission': {
36 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
37 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
38 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
39 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
40 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
41 },
42 u'auth.user': {
43 'Meta': {'object_name': 'User'},
44 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
45 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}),
46 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
47 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
48 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
49 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
50 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
51 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
52 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
53 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
54 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
55 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
56 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
57 },
58 u'contenttypes.contenttype': {
59 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
60 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
61 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
62 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
63 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
64 },
65 u'maasserver.blockdevice': {
66 'Meta': {'ordering': "[u'id']", 'unique_together': "((u'node', u'path'),)", 'object_name': 'BlockDevice'},
67 'block_size': ('django.db.models.fields.IntegerField', [], {}),
68 'created': ('django.db.models.fields.DateTimeField', [], {}),
69 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
70 'id_path': ('django.db.models.fields.FilePathField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
71 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
72 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}),
73 'path': ('django.db.models.fields.FilePathField', [], {'max_length': '100'}),
74 'size': ('django.db.models.fields.BigIntegerField', [], {}),
75 'tags': ('djorm_pgarray.fields.ArrayField', [], {'default': '[]', 'dbtype': "u'text'", 'blank': 'True'}),
76 'updated': ('django.db.models.fields.DateTimeField', [], {})
77 },
78 u'maasserver.bootresource': {
79 'Meta': {'unique_together': "((u'name', u'architecture'),)", 'object_name': 'BootResource'},
80 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
81 'created': ('django.db.models.fields.DateTimeField', [], {}),
82 'extra': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
83 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
84 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
85 'rtype': ('django.db.models.fields.IntegerField', [], {'max_length': '10'}),
86 'updated': ('django.db.models.fields.DateTimeField', [], {})
87 },
88 u'maasserver.bootresourcefile': {
89 'Meta': {'unique_together': "((u'resource_set', u'filetype'),)", 'object_name': 'BootResourceFile'},
90 'created': ('django.db.models.fields.DateTimeField', [], {}),
91 'extra': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
92 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
93 'filetype': ('django.db.models.fields.CharField', [], {'default': "u'root-tgz'", 'max_length': '20'}),
94 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
95 'largefile': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.LargeFile']"}),
96 'resource_set': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'files'", 'to': u"orm['maasserver.BootResourceSet']"}),
97 'updated': ('django.db.models.fields.DateTimeField', [], {})
98 },
99 u'maasserver.bootresourceset': {
100 'Meta': {'unique_together': "((u'resource', u'version'),)", 'object_name': 'BootResourceSet'},
101 'created': ('django.db.models.fields.DateTimeField', [], {}),
102 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
103 'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
104 'resource': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'sets'", 'to': u"orm['maasserver.BootResource']"}),
105 'updated': ('django.db.models.fields.DateTimeField', [], {}),
106 'version': ('django.db.models.fields.CharField', [], {'max_length': '255'})
107 },
108 u'maasserver.bootsource': {
109 'Meta': {'object_name': 'BootSource'},
110 'created': ('django.db.models.fields.DateTimeField', [], {}),
111 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
112 'keyring_data': ('maasserver.fields.EditableBinaryField', [], {'blank': 'True'}),
113 'keyring_filename': ('django.db.models.fields.FilePathField', [], {'max_length': '100', 'blank': 'True'}),
114 'updated': ('django.db.models.fields.DateTimeField', [], {}),
115 'url': ('django.db.models.fields.URLField', [], {'unique': 'True', 'max_length': '200'})
116 },
117 u'maasserver.bootsourcecache': {
118 'Meta': {'object_name': 'BootSourceCache'},
119 'arch': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
120 'boot_source': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.BootSource']"}),
121 'created': ('django.db.models.fields.DateTimeField', [], {}),
122 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
123 'label': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
124 'os': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
125 'release': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
126 'subarch': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
127 'updated': ('django.db.models.fields.DateTimeField', [], {})
128 },
129 u'maasserver.bootsourceselection': {
130 'Meta': {'unique_together': "((u'boot_source', u'os', u'release'),)", 'object_name': 'BootSourceSelection'},
131 'arches': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'text'", 'null': 'True', 'blank': 'True'}),
132 'boot_source': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.BootSource']"}),
133 'created': ('django.db.models.fields.DateTimeField', [], {}),
134 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
135 'labels': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'text'", 'null': 'True', 'blank': 'True'}),
136 'os': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'blank': 'True'}),
137 'release': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'blank': 'True'}),
138 'subarches': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'text'", 'null': 'True', 'blank': 'True'}),
139 'updated': ('django.db.models.fields.DateTimeField', [], {})
140 },
141 u'maasserver.candidatename': {
142 'Meta': {'unique_together': "((u'name', u'position'),)", 'object_name': 'CandidateName'},
143 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
144 'name': ('django.db.models.fields.SlugField', [], {'max_length': '50'}),
145 'position': ('django.db.models.fields.IntegerField', [], {})
146 },
147 u'maasserver.componenterror': {
148 'Meta': {'object_name': 'ComponentError'},
149 'component': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
150 'created': ('django.db.models.fields.DateTimeField', [], {}),
151 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000'}),
152 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
153 'updated': ('django.db.models.fields.DateTimeField', [], {})
154 },
155 u'maasserver.config': {
156 'Meta': {'object_name': 'Config'},
157 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
158 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
159 'value': ('maasserver.fields.JSONObjectField', [], {'null': 'True'})
160 },
161 u'maasserver.dhcplease': {
162 'Meta': {'object_name': 'DHCPLease'},
163 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
164 'ip': ('maasserver.fields.MAASIPAddressField', [], {'unique': 'True', 'max_length': '39'}),
165 'mac': ('maasserver.fields.MACAddressField', [], {}),
166 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"})
167 },
168 u'maasserver.downloadprogress': {
169 'Meta': {'object_name': 'DownloadProgress'},
170 'bytes_downloaded': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
171 'created': ('django.db.models.fields.DateTimeField', [], {}),
172 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'blank': 'True'}),
173 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
174 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
175 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
176 'size': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
177 'updated': ('django.db.models.fields.DateTimeField', [], {})
178 },
179 u'maasserver.event': {
180 'Meta': {'object_name': 'Event'},
181 'created': ('django.db.models.fields.DateTimeField', [], {}),
182 'description': ('django.db.models.fields.TextField', [], {'default': "u''", 'blank': 'True'}),
183 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
184 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}),
185 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.EventType']"}),
186 'updated': ('django.db.models.fields.DateTimeField', [], {})
187 },
188 u'maasserver.eventtype': {
189 'Meta': {'object_name': 'EventType'},
190 'created': ('django.db.models.fields.DateTimeField', [], {}),
191 'description': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
192 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
193 'level': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
194 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
195 'updated': ('django.db.models.fields.DateTimeField', [], {})
196 },
197 u'maasserver.filestorage': {
198 'Meta': {'unique_together': "((u'filename', u'owner'),)", 'object_name': 'FileStorage'},
199 'content': ('metadataserver.fields.BinaryField', [], {'blank': 'True'}),
200 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
201 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
202 'key': ('django.db.models.fields.CharField', [], {'default': "u'c4af88bc-2ae9-11e5-9139-00163edfc3e6'", 'unique': 'True', 'max_length': '36'}),
203 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
204 },
205 u'maasserver.filesystem': {
206 'Meta': {'object_name': 'Filesystem'},
207 'block_device': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.BlockDevice']", 'null': 'True', 'blank': 'True'}),
208 'create_params': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
209 'created': ('django.db.models.fields.DateTimeField', [], {}),
210 'filesystem_group': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'filesystems'", 'null': 'True', 'to': u"orm['maasserver.FilesystemGroup']"}),
211 'fstype': ('django.db.models.fields.CharField', [], {'default': "u'ext4'", 'max_length': '20'}),
212 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
213 'mount_params': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
214 'mount_point': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
215 'partition': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Partition']", 'null': 'True', 'blank': 'True'}),
216 'updated': ('django.db.models.fields.DateTimeField', [], {}),
217 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'})
218 },
219 u'maasserver.filesystemgroup': {
220 'Meta': {'object_name': 'FilesystemGroup'},
221 'create_params': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
222 'created': ('django.db.models.fields.DateTimeField', [], {}),
223 'group_type': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
224 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
225 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
226 'updated': ('django.db.models.fields.DateTimeField', [], {}),
227 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'})
228 },
229 u'maasserver.largefile': {
230 'Meta': {'object_name': 'LargeFile'},
231 'content': ('maasserver.fields.LargeObjectField', [], {}),
232 'created': ('django.db.models.fields.DateTimeField', [], {}),
233 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
234 'sha256': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'}),
235 'total_size': ('django.db.models.fields.BigIntegerField', [], {}),
236 'updated': ('django.db.models.fields.DateTimeField', [], {})
237 },
238 u'maasserver.licensekey': {
239 'Meta': {'unique_together': "((u'osystem', u'distro_series'),)", 'object_name': 'LicenseKey'},
240 'created': ('django.db.models.fields.DateTimeField', [], {}),
241 'distro_series': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
242 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
243 'license_key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
244 'osystem': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
245 'updated': ('django.db.models.fields.DateTimeField', [], {})
246 },
247 u'maasserver.macaddress': {
248 'Meta': {'ordering': "(u'created',)", 'object_name': 'MACAddress'},
249 'cluster_interface': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['maasserver.NodeGroupInterface']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
250 'created': ('django.db.models.fields.DateTimeField', [], {}),
251 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
252 'ip_addresses': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.StaticIPAddress']", 'symmetrical': 'False', 'through': u"orm['maasserver.MACStaticIPAddressLink']", 'blank': 'True'}),
253 'mac_address': ('maasserver.fields.MACAddressField', [], {'unique': 'True'}),
254 'networks': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Network']", 'symmetrical': 'False', 'blank': 'True'}),
255 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']", 'null': 'True', 'blank': 'True'}),
256 'updated': ('django.db.models.fields.DateTimeField', [], {})
257 },
258 u'maasserver.macstaticipaddresslink': {
259 'Meta': {'unique_together': "((u'ip_address', u'mac_address'),)", 'object_name': 'MACStaticIPAddressLink'},
260 'created': ('django.db.models.fields.DateTimeField', [], {}),
261 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
262 'ip_address': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.StaticIPAddress']", 'unique': 'True'}),
263 'mac_address': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.MACAddress']"}),
264 'nic_alias': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
265 'updated': ('django.db.models.fields.DateTimeField', [], {})
266 },
267 u'maasserver.network': {
268 'Meta': {'object_name': 'Network'},
269 'default_gateway': ('maasserver.fields.MAASIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
270 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
271 'dns_servers': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
272 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
273 'ip': ('maasserver.fields.MAASIPAddressField', [], {'unique': 'True', 'max_length': '39'}),
274 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
275 'netmask': ('maasserver.fields.MAASIPAddressField', [], {'max_length': '39'}),
276 'vlan_tag': ('django.db.models.fields.PositiveSmallIntegerField', [], {'unique': 'True', 'null': 'True', 'blank': 'True'})
277 },
278 u'maasserver.node': {
279 'Meta': {'object_name': 'Node'},
280 'agent_name': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'null': 'True', 'blank': 'True'}),
281 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '31', 'null': 'True', 'blank': 'True'}),
282 'boot_type': ('django.db.models.fields.CharField', [], {'default': "u'fastpath'", 'max_length': '20'}),
283 'cpu_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
284 'created': ('django.db.models.fields.DateTimeField', [], {}),
285 'disable_ipv4': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
286 'distro_series': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'blank': 'True'}),
287 'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
288 'error_description': ('django.db.models.fields.TextField', [], {'default': "u''", 'blank': 'True'}),
289 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'unique': 'True', 'max_length': '255', 'blank': 'True'}),
290 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
291 'installable': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
292 'license_key': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'blank': 'True'}),
293 'memory': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
294 'netboot': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
295 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}),
296 'osystem': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '20', 'blank': 'True'}),
297 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}),
298 'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "u'children'", 'null': 'True', 'blank': 'True', 'to': u"orm['maasserver.Node']"}),
299 'power_parameters': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'max_length': '32768', 'blank': 'True'}),
300 'power_state': ('django.db.models.fields.CharField', [], {'default': "u'unknown'", 'max_length': '10'}),
301 'power_state_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}),
302 'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}),
303 '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'}),
304 'routers': ('djorm_pgarray.fields.ArrayField', [], {'default': 'None', 'dbtype': "u'macaddr'", 'null': 'True', 'blank': 'True'}),
305 'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '10'}),
306 'swap_size': ('django.db.models.fields.BigIntegerField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
307 'system_id': ('django.db.models.fields.CharField', [], {'default': "u'node-c4b17d70-2ae9-11e5-9139-00163edfc3e6'", 'unique': 'True', 'max_length': '41'}),
308 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Tag']", 'symmetrical': 'False'}),
309 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Token']", 'null': 'True'}),
310 'updated': ('django.db.models.fields.DateTimeField', [], {}),
311 'zone': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Zone']", 'on_delete': 'models.SET_DEFAULT'})
312 },
313 u'maasserver.nodegroup': {
314 'Meta': {'object_name': 'NodeGroup'},
315 'api_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '18'}),
316 'api_token': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Token']", 'unique': 'True'}),
317 'cluster_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100', 'blank': 'True'}),
318 'created': ('django.db.models.fields.DateTimeField', [], {}),
319 'default_disable_ipv4': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
320 'dhcp_key': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
321 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
322 'maas_url': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
323 'name': ('maasserver.models.nodegroup.DomainNameField', [], {'max_length': '80', 'blank': 'True'}),
324 'status': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
325 'updated': ('django.db.models.fields.DateTimeField', [], {}),
326 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'})
327 },
328 u'maasserver.nodegroupinterface': {
329 'Meta': {'unique_together': "((u'nodegroup', u'name'),)", 'object_name': 'NodeGroupInterface'},
330 'broadcast_ip': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
331 'created': ('django.db.models.fields.DateTimeField', [], {}),
332 'foreign_dhcp_ip': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
333 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
334 'interface': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
335 'ip': ('maasserver.fields.MAASIPAddressField', [], {'max_length': '39'}),
336 'ip_range_high': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
337 'ip_range_low': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
338 'management': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
339 'name': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
340 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
341 'router_ip': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
342 'static_ip_range_high': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
343 'static_ip_range_low': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
344 'subnet_mask': ('maasserver.fields.MAASIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
345 'updated': ('django.db.models.fields.DateTimeField', [], {})
346 },
347 u'maasserver.partition': {
348 'Meta': {'object_name': 'Partition'},
349 'bootable': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
350 'created': ('django.db.models.fields.DateTimeField', [], {}),
351 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
352 'partition_table': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'partitions'", 'to': u"orm['maasserver.PartitionTable']"}),
353 'size': ('django.db.models.fields.BigIntegerField', [], {}),
354 'start_offset': ('django.db.models.fields.BigIntegerField', [], {}),
355 'updated': ('django.db.models.fields.DateTimeField', [], {}),
356 'uuid': ('django.db.models.fields.CharField', [], {'max_length': '36', 'unique': 'True', 'null': 'True', 'blank': 'True'})
357 },
358 u'maasserver.partitiontable': {
359 'Meta': {'object_name': 'PartitionTable'},
360 'block_device': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.BlockDevice']"}),
361 'created': ('django.db.models.fields.DateTimeField', [], {}),
362 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
363 'table_type': ('django.db.models.fields.CharField', [], {'default': "u'GPT'", 'max_length': '20'}),
364 'updated': ('django.db.models.fields.DateTimeField', [], {})
365 },
366 u'maasserver.physicalblockdevice': {
367 'Meta': {'ordering': "[u'id']", 'object_name': 'PhysicalBlockDevice', '_ormbases': [u'maasserver.BlockDevice']},
368 u'blockdevice_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['maasserver.BlockDevice']", 'unique': 'True', 'primary_key': 'True'}),
369 'model': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
370 'serial': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'})
371 },
372 u'maasserver.sshkey': {
373 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSHKey'},
374 'created': ('django.db.models.fields.DateTimeField', [], {}),
375 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
376 'key': ('django.db.models.fields.TextField', [], {}),
377 'updated': ('django.db.models.fields.DateTimeField', [], {}),
378 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
379 },
380 u'maasserver.sslkey': {
381 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSLKey'},
382 'created': ('django.db.models.fields.DateTimeField', [], {}),
383 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
384 'key': ('django.db.models.fields.TextField', [], {}),
385 'updated': ('django.db.models.fields.DateTimeField', [], {}),
386 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
387 },
388 u'maasserver.staticipaddress': {
389 'Meta': {'object_name': 'StaticIPAddress'},
390 'alloc_type': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
391 'created': ('django.db.models.fields.DateTimeField', [], {}),
392 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'null': 'True', 'blank': 'True'}),
393 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
394 'ip': ('maasserver.fields.MAASIPAddressField', [], {'unique': 'True', 'max_length': '39'}),
395 'updated': ('django.db.models.fields.DateTimeField', [], {}),
396 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
397 },
398 u'maasserver.tag': {
399 'Meta': {'object_name': 'Tag'},
400 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
401 'created': ('django.db.models.fields.DateTimeField', [], {}),
402 'definition': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
403 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
404 'kernel_opts': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
405 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
406 'updated': ('django.db.models.fields.DateTimeField', [], {})
407 },
408 u'maasserver.userprofile': {
409 'Meta': {'object_name': 'UserProfile'},
410 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
411 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'})
412 },
413 u'maasserver.virtualblockdevice': {
414 'Meta': {'ordering': "[u'id']", 'object_name': 'VirtualBlockDevice', '_ormbases': [u'maasserver.BlockDevice']},
415 u'blockdevice_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['maasserver.BlockDevice']", 'unique': 'True', 'primary_key': 'True'}),
416 'filesystem_group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'virtual_devices'", 'to': u"orm['maasserver.FilesystemGroup']"}),
417 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'})
418 },
419 u'maasserver.zone': {
420 'Meta': {'ordering': "[u'name']", 'object_name': 'Zone'},
421 'created': ('django.db.models.fields.DateTimeField', [], {}),
422 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
423 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
424 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
425 'updated': ('django.db.models.fields.DateTimeField', [], {})
426 },
427 u'piston.consumer': {
428 'Meta': {'object_name': 'Consumer'},
429 'description': ('django.db.models.fields.TextField', [], {}),
430 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
431 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
432 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
433 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
434 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '16'}),
435 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'consumers'", 'null': 'True', 'to': u"orm['auth.User']"})
436 },
437 u'piston.token': {
438 'Meta': {'object_name': 'Token'},
439 'callback': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
440 'callback_confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
441 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['piston.Consumer']"}),
442 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
443 'is_approved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
444 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
445 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
446 'timestamp': ('django.db.models.fields.IntegerField', [], {'default': '1436961921L'}),
447 'token_type': ('django.db.models.fields.IntegerField', [], {}),
448 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tokens'", 'null': 'True', 'to': u"orm['auth.User']"}),
449 'verifier': ('django.db.models.fields.CharField', [], {'max_length': '10'})
450 }
451 }
452
453 complete_apps = ['maasserver']
0454
=== modified file 'src/maasserver/models/node.py'
--- src/maasserver/models/node.py 2015-05-22 15:03:29 +0000
+++ src/maasserver/models/node.py 2015-07-15 12:15:32 +0000
@@ -38,6 +38,7 @@
38 BooleanField,38 BooleanField,
39 CASCADE,39 CASCADE,
40 CharField,40 CharField,
41 DateTimeField,
41 ForeignKey,42 ForeignKey,
42 IntegerField,43 IntegerField,
43 Manager,44 Manager,
@@ -91,7 +92,10 @@
91from maasserver.models.physicalblockdevice import PhysicalBlockDevice92from maasserver.models.physicalblockdevice import PhysicalBlockDevice
92from maasserver.models.staticipaddress import StaticIPAddress93from maasserver.models.staticipaddress import StaticIPAddress
93from maasserver.models.tag import Tag94from maasserver.models.tag import Tag
94from maasserver.models.timestampedmodel import TimestampedModel95from maasserver.models.timestampedmodel import (
96 now,
97 TimestampedModel,
98)
95from maasserver.models.zone import Zone99from maasserver.models.zone import Zone
96from maasserver.node_status import (100from maasserver.node_status import (
97 COMMISSIONING_LIKE_STATUSES,101 COMMISSIONING_LIKE_STATUSES,
@@ -457,14 +461,19 @@
457 power_type = CharField(461 power_type = CharField(
458 max_length=10, null=False, blank=True, default='')462 max_length=10, null=False, blank=True, default='')
459463
460 # JSON-encoded set of parameters for power control.464 # JSON-encoded set of parameters for power control, limited to 32kiB when
461 power_parameters = JSONObjectField(blank=True, default="")465 # encoded as JSON.
466 power_parameters = JSONObjectField(
467 max_length=(2 ** 15), blank=True, default="")
462468
463 power_state = CharField(469 power_state = CharField(
464 max_length=10, null=False, blank=False,470 max_length=10, null=False, blank=False,
465 choices=POWER_STATE_CHOICES, default=POWER_STATE.UNKNOWN,471 choices=POWER_STATE_CHOICES, default=POWER_STATE.UNKNOWN,
466 editable=False)472 editable=False)
467473
474 power_state_updated = DateTimeField(
475 null=True, blank=False, default=None, editable=False)
476
468 token = ForeignKey(477 token = ForeignKey(
469 Token, db_index=True, null=True, editable=False, unique=False)478 Token, db_index=True, null=True, editable=False, unique=False)
470479
@@ -1728,6 +1737,7 @@
1728 def update_power_state(self, power_state):1737 def update_power_state(self, power_state):
1729 """Update a node's power state """1738 """Update a node's power state """
1730 self.power_state = power_state1739 self.power_state = power_state
1740 self.power_state_updated = now()
1731 mark_ready = (1741 mark_ready = (
1732 self.status == NODE_STATUS.RELEASING and1742 self.status == NODE_STATUS.RELEASING and
1733 power_state == POWER_STATE.OFF)1743 power_state == POWER_STATE.OFF)
17341744
=== modified file 'src/maasserver/models/tests/test_node.py'
--- src/maasserver/models/tests/test_node.py 2015-05-26 07:38:51 +0000
+++ src/maasserver/models/tests/test_node.py 2015-07-15 12:15:32 +0000
@@ -63,6 +63,7 @@
63)63)
64from maasserver.models.node import PowerInfo64from maasserver.models.node import PowerInfo
65from maasserver.models.staticipaddress import StaticIPAddress65from maasserver.models.staticipaddress import StaticIPAddress
66from maasserver.models.timestampedmodel import now
66from maasserver.models.user import create_auth_token67from maasserver.models.user import create_auth_token
67from maasserver.node_status import (68from maasserver.node_status import (
68 get_failed_status,69 get_failed_status,
@@ -1837,6 +1838,13 @@
1837 node.update_power_state(state)1838 node.update_power_state(state)
1838 self.assertEqual(state, reload_object(node).power_state)1839 self.assertEqual(state, reload_object(node).power_state)
18391840
1841 def test_update_power_state_sets_last_updated_field(self):
1842 node = factory.make_Node(power_state_updated=None)
1843 self.assertIsNone(node.power_state_updated)
1844 state = factory.pick_enum(POWER_STATE)
1845 node.update_power_state(state)
1846 self.assertEqual(now(), reload_object(node).power_state_updated)
1847
1840 def test_update_power_state_readies_node_if_releasing(self):1848 def test_update_power_state_readies_node_if_releasing(self):
1841 node = factory.make_Node(1849 node = factory.make_Node(
1842 power_state=POWER_STATE.ON, status=NODE_STATUS.RELEASING,1850 power_state=POWER_STATE.ON, status=NODE_STATUS.RELEASING,
18431851
=== modified file 'src/maasserver/models/timestampedmodel.py'
--- src/maasserver/models/timestampedmodel.py 2015-05-07 18:14:38 +0000
+++ src/maasserver/models/timestampedmodel.py 2015-07-15 12:15:32 +0000
@@ -13,6 +13,7 @@
1313
14__metaclass__ = type14__metaclass__ = type
15__all__ = [15__all__ = [
16 'now',
16 'TimestampedModel',17 'TimestampedModel',
17 ]18 ]
1819
1920
=== modified file 'src/maasserver/rpc/nodes.py'
--- src/maasserver/rpc/nodes.py 2015-05-07 18:14:38 +0000
+++ src/maasserver/rpc/nodes.py 2015-07-15 12:15:32 +0000
@@ -19,6 +19,9 @@
19 "create_node",19 "create_node",
20]20]
2121
22from datetime import timedelta
23from itertools import chain
24
22from django.contrib.auth.models import User25from django.contrib.auth.models import User
23from django.core.exceptions import ValidationError26from django.core.exceptions import ValidationError
24from maasserver import exceptions27from maasserver import exceptions
@@ -30,6 +33,7 @@
30 Node,33 Node,
31 NodeGroup,34 NodeGroup,
32)35)
36from maasserver.models.timestampedmodel import now
33from maasserver.utils.orm import transactional37from maasserver.utils.orm import transactional
34from provisioningserver.rpc.exceptions import (38from provisioningserver.rpc.exceptions import (
35 CommissionNodeFailed,39 CommissionNodeFailed,
@@ -59,35 +63,97 @@
59 raise NodeStateViolation(e)63 raise NodeStateViolation(e)
6064
6165
62@synchronous66def _gen_cluster_nodes_power_parameters(nodes):
63@transactional67 """Generate power parameters for `nodes`.
64def list_cluster_nodes_power_parameters(uuid):68
65 """Query a cluster controller and return all of its nodes69 These fulfil a subset of the return schema for the RPC call for
66 power parameters70 :py:class:`~provisioningserver.rpc.region.ListNodePowerParameters`.
6771
68 for :py:class:`~provisioningserver.rpc.region.ListNodePowerParameters`.72 :return: A generator yielding `dict`s.
69 """73 """
70 try:74 five_minutes_ago = now() - timedelta(minutes=5)
71 nodegroup = NodeGroup.objects.get_by_natural_key(uuid)75
72 except NodeGroup.DoesNotExist:76 # This is meant to be temporary until all the power types support querying
73 raise NoSuchCluster.from_uuid(uuid)77 # the power state of a node. See the definition of QUERY_POWER_TYPES for
74 else:78 # more information.
75 power_info_by_node = (79 from provisioningserver.rpc.power import QUERY_POWER_TYPES
76 (node, node.get_effective_power_info())80
77 for node in nodegroup.node_set.exclude(81 nodes_unchecked = (
78 status=NODE_STATUS.BROKEN).exclude(installable=False)82 nodes
79 )83 .filter(power_state_updated=None)
80 return [84 .filter(power_type__in=QUERY_POWER_TYPES)
81 {85 .exclude(status=NODE_STATUS.BROKEN)
86 .exclude(installable=False)
87 )
88 nodes_checked = (
89 nodes
90 .exclude(power_state_updated=None)
91 .exclude(power_state_updated__gt=five_minutes_ago)
92 .filter(power_type__in=QUERY_POWER_TYPES)
93 .exclude(status=NODE_STATUS.BROKEN)
94 .exclude(installable=False)
95 .order_by("power_state_updated", "system_id")
96 )
97
98 for node in chain(nodes_unchecked, nodes_checked):
99 power_info = node.get_effective_power_info()
100 if power_info.power_type is not None:
101 yield {
82 'system_id': node.system_id,102 'system_id': node.system_id,
83 'hostname': node.hostname,103 'hostname': node.hostname,
84 'power_state': node.power_state,104 'power_state': node.power_state,
85 'power_type': power_info.power_type,105 'power_type': power_info.power_type,
86 'context': power_info.power_parameters,106 'context': power_info.power_parameters,
87 }107 }
88 for node, power_info in power_info_by_node108
89 if power_info.power_type is not None109
90 ]110def _gen_up_to_json_limit(things, limit):
111 """Yield until the combined JSON dump of those things would exceed `limit`.
112
113 :param things: Any iterable whose elements can dumped as JSON.
114 :return: A generator that yields items from `things` unmodified, and in
115 order, though maybe not all of them.
116 """
117 # Deduct the space required for brackets. json.dumps(), by default, does
118 # not add padding, so it's just the opening and closing brackets.
119 limit -= 2
120
121 for index, thing in enumerate(things):
122 # Adjust the limit according the the size of thing.
123 if index == 0:
124 # A sole element does not need a delimiter.
125 limit -= len(json.dumps(thing))
126 else:
127 # There is a delimiter between this and the preceeding element.
128 # json.dumps(), by default, uses ", ", i.e. 2 characters.
129 limit -= len(json.dumps(thing)) + 2
130
131 # Check if we've reached the limit.
132 if limit == 0:
133 yield thing
134 break
135 elif limit > 0:
136 yield thing
137 else:
138 break
139
140
141@synchronous
142@transactional
143def list_cluster_nodes_power_parameters(uuid):
144 """Return power parameters for a cluster's nodes, in priority order.
145
146 For :py:class:`~provisioningserver.rpc.region.ListNodePowerParameters`.
147 """
148 try:
149 nodegroup = NodeGroup.objects.get_by_natural_key(uuid)
150 except NodeGroup.DoesNotExist:
151 raise NoSuchCluster.from_uuid(uuid)
152 else:
153 nodes = nodegroup.node_set.all()
154 details = _gen_cluster_nodes_power_parameters(nodes)
155 details = _gen_up_to_json_limit(details, 60 * (2 ** 10)) # 60kiB
156 return list(details)
91157
92158
93@synchronous159@synchronous
94160
=== modified file 'src/maasserver/rpc/tests/test_nodes.py'
--- src/maasserver/rpc/tests/test_nodes.py 2015-05-22 15:52:13 +0000
+++ src/maasserver/rpc/tests/test_nodes.py 2015-07-15 12:15:32 +0000
@@ -14,10 +14,16 @@
14__metaclass__ = type14__metaclass__ = type
15__all__ = []15__all__ = []
1616
17from datetime import timedelta
18from itertools import imap
19import json
20from operator import attrgetter
17import random21import random
22from random import randint
1823
19from django.core.exceptions import ValidationError24from django.core.exceptions import ValidationError
20from maasserver.enum import NODE_STATUS25from maasserver.enum import NODE_STATUS
26from maasserver.models.timestampedmodel import now
21from maasserver.rpc.nodes import (27from maasserver.rpc.nodes import (
22 commission_node,28 commission_node,
23 create_node,29 create_node,
@@ -47,12 +53,15 @@
47 NodeStateViolation,53 NodeStateViolation,
48 NoSuchNode,54 NoSuchNode,
49)55)
56from provisioningserver.rpc.power import QUERY_POWER_TYPES
50from simplejson import dumps57from simplejson import dumps
51from testtools import ExpectedException58from testtools import ExpectedException
52from testtools.matchers import (59from testtools.matchers import (
53 Contains,60 Contains,
54 Equals,61 Equals,
62 GreaterThan,
55 Is,63 Is,
64 LessThan,
56 Not,65 Not,
57)66)
5867
@@ -321,25 +330,127 @@
321 # Those tests have been left there for now because they also check330 # Those tests have been left there for now because they also check
322 # that the return values are being formatted correctly for RPC.331 # that the return values are being formatted correctly for RPC.
323332
324 def test_does_not_return_power_info_for_broken_nodes(self):333 def make_Node(
325 cluster = factory.make_NodeGroup()334 self, cluster, power_type=None, power_state_updated=None,
326 broken_node = factory.make_Node(335 **kwargs):
327 nodegroup=cluster, status=NODE_STATUS.BROKEN)336 if power_type is None:
328337 # Ensure that this node's power status can be queried.
329 power_parameters = list_cluster_nodes_power_parameters(cluster.uuid)338 power_type = random.choice(QUERY_POWER_TYPES)
330 returned_system_ids = [339 if power_state_updated is None:
331 power_params['system_id'] for power_params in power_parameters]340 # Ensure that this node was last queried at least 5 minutes ago.
332341 power_state_updated = now() - timedelta(minutes=randint(6, 16))
333 self.assertThat(342 return factory.make_Node(
334 returned_system_ids, Not(Contains(broken_node.system_id)))343 nodegroup=cluster, power_type=power_type,
335344 power_state_updated=power_state_updated, **kwargs)
336 def test_does_not_return_power_info_for_devices(self):345
337 cluster = factory.make_NodeGroup()346 def test__returns_unchecked_nodes_first(self):
338 device = factory.make_Device()347 cluster = factory.make_NodeGroup()
339348 nodes = [self.make_Node(cluster) for _ in xrange(5)]
340 power_parameters = list_cluster_nodes_power_parameters(cluster.uuid)349 node_unchecked = random.choice(nodes)
341 returned_system_ids = [350 node_unchecked.power_state_updated = None
342 power_params['system_id'] for power_params in power_parameters]351 node_unchecked.save()
343352
344 self.assertThat(353 power_parameters = list_cluster_nodes_power_parameters(cluster.uuid)
345 returned_system_ids, Not(Contains(device.system_id)))354 system_ids = [params["system_id"] for params in power_parameters]
355
356 # The unchecked node is always the first out.
357 self.assertEqual(node_unchecked.system_id, system_ids[0])
358
359 def test__excludes_recently_checked_nodes(self):
360 cluster = factory.make_NodeGroup()
361
362 node_unchecked = self.make_Node(cluster)
363 node_unchecked.power_state_updated = None
364 node_unchecked.save()
365
366 datetime_now = now()
367 node_checked_recently = self.make_Node(cluster)
368 node_checked_recently.power_state_updated = datetime_now
369 node_checked_recently.save()
370
371 datetime_10_minutes_ago = datetime_now - timedelta(minutes=10)
372 node_checked_long_ago = self.make_Node(cluster)
373 node_checked_long_ago.power_state_updated = datetime_10_minutes_ago
374 node_checked_long_ago.save()
375
376 power_parameters = list_cluster_nodes_power_parameters(cluster.uuid)
377 system_ids = [params["system_id"] for params in power_parameters]
378
379 self.assertItemsEqual(
380 {node_unchecked.system_id, node_checked_long_ago.system_id},
381 system_ids)
382
383 def test__excludes_unqueryable_power_types(self):
384 cluster = factory.make_NodeGroup()
385 node_queryable = self.make_Node(cluster)
386 self.make_Node(cluster, "foobar") # Unqueryable power type.
387
388 power_parameters = list_cluster_nodes_power_parameters(cluster.uuid)
389 system_ids = [params["system_id"] for params in power_parameters]
390
391 self.assertItemsEqual([node_queryable.system_id], system_ids)
392
393 def test__excludes_broken_nodes(self):
394 cluster = factory.make_NodeGroup()
395 node_queryable = self.make_Node(cluster)
396
397 self.make_Node(cluster, status=NODE_STATUS.BROKEN)
398 self.make_Node(
399 cluster, status=NODE_STATUS.BROKEN, power_state_updated=(
400 now() - timedelta(minutes=10)))
401
402 power_parameters = list_cluster_nodes_power_parameters(cluster.uuid)
403 system_ids = [params["system_id"] for params in power_parameters]
404
405 self.assertItemsEqual([node_queryable.system_id], system_ids)
406
407 def test__excludes_devices(self):
408 cluster = factory.make_NodeGroup()
409 node_queryable = self.make_Node(cluster)
410
411 factory.make_Device(nodegroup=cluster)
412 factory.make_Device(nodegroup=cluster, power_type="ipmi")
413 factory.make_Device(
414 nodegroup=cluster, power_type="ipmi", power_state_updated=(
415 now() - timedelta(minutes=10)))
416
417 power_parameters = list_cluster_nodes_power_parameters(cluster.uuid)
418 system_ids = [params["system_id"] for params in power_parameters]
419
420 self.assertItemsEqual([node_queryable.system_id], system_ids)
421
422 def test__returns_checked_nodes_in_last_checked_order(self):
423 cluster = factory.make_NodeGroup()
424 nodes = [self.make_Node(cluster) for _ in xrange(5)]
425
426 power_parameters = list_cluster_nodes_power_parameters(cluster.uuid)
427 system_ids = [params["system_id"] for params in power_parameters]
428
429 # Checked nodes are always sorted from least recently checked to most.
430 node_sort_key = attrgetter("power_state_updated", "system_id")
431 nodes_in_order = sorted(nodes, key=node_sort_key)
432 self.assertEqual(
433 [node.system_id for node in nodes_in_order],
434 system_ids)
435
436 def test__returns_at_most_60kiB_of_JSON(self):
437 cluster = factory.make_NodeGroup()
438
439 # Ensure that there are at least 64kiB of power parameters (when
440 # converted to JSON) in the database.
441 example_parameters = {"key%d" % i: "value%d" % i for i in xrange(100)}
442 remaining = 2 ** 16
443 while remaining > 0:
444 node = self.make_Node(cluster, power_parameters=example_parameters)
445 remaining -= len(json.dumps(node.get_effective_power_parameters()))
446
447 nodes = list_cluster_nodes_power_parameters(cluster.uuid)
448
449 # The total size of the JSON is less than 60kiB, but only a bit.
450 nodes_json = imap(json.dumps, nodes)
451 nodes_json_lengths = imap(len, nodes_json)
452 nodes_json_length = sum(nodes_json_lengths)
453 expected_maximum = 60 * (2 ** 10) # 60kiB
454 self.expectThat(nodes_json_length, LessThan(expected_maximum + 1))
455 expected_minimum = 50 * (2 ** 10) # 50kiB
456 self.expectThat(nodes_json_length, GreaterThan(expected_minimum - 1))
346457
=== modified file 'src/maasserver/rpc/tests/test_regionservice.py'
--- src/maasserver/rpc/tests/test_regionservice.py 2015-06-05 13:21:13 +0000
+++ src/maasserver/rpc/tests/test_regionservice.py 2015-07-15 12:15:32 +0000
@@ -104,6 +104,7 @@
104 NoSuchNode,104 NoSuchNode,
105)105)
106from provisioningserver.rpc.interfaces import IConnection106from provisioningserver.rpc.interfaces import IConnection
107from provisioningserver.rpc.power import QUERY_POWER_TYPES
107from provisioningserver.rpc.region import (108from provisioningserver.rpc.region import (
108 Authenticate,109 Authenticate,
109 CommissionNode,110 CommissionNode,
@@ -688,7 +689,10 @@
688 nodegroup = yield deferToThread(self.create_nodegroup)689 nodegroup = yield deferToThread(self.create_nodegroup)
689 nodes = []690 nodes = []
690 for _ in range(3):691 for _ in range(3):
691 node = yield deferToThread(self.create_node, nodegroup)692 node = yield deferToThread(
693 self.create_node, nodegroup,
694 power_type=random.choice(QUERY_POWER_TYPES),
695 power_state_updated=None)
692 power_params = yield deferToThread(696 power_params = yield deferToThread(
693 self.get_node_power_parameters, node)697 self.get_node_power_parameters, node)
694 nodes.append({698 nodes.append({
@@ -701,12 +705,15 @@
701705
702 # Create a node with an invalid power type (i.e. the empty string).706 # Create a node with an invalid power type (i.e. the empty string).
703 # This will not be reported by the call to ListNodePowerParameters.707 # This will not be reported by the call to ListNodePowerParameters.
704 yield deferToThread(self.create_node, nodegroup, power_type="")708 yield deferToThread(
709 self.create_node, nodegroup, power_type="",
710 power_state_updated=None)
705711
706 response = yield call_responder(712 response = yield call_responder(
707 Region(), ListNodePowerParameters,713 Region(), ListNodePowerParameters,
708 {b'uuid': nodegroup.uuid})714 {b'uuid': nodegroup.uuid})
709715
716 self.maxDiff = None
710 self.assertItemsEqual(nodes, response['nodes'])717 self.assertItemsEqual(nodes, response['nodes'])
711718
712 @wait_for_reactor719 @wait_for_reactor
713720
=== modified file 'src/maasserver/testing/factory.py'
--- src/maasserver/testing/factory.py 2015-05-20 13:51:49 +0000
+++ src/maasserver/testing/factory.py 2015-07-15 12:15:32 +0000
@@ -17,6 +17,7 @@
17 "Messages",17 "Messages",
18 ]18 ]
1919
20from datetime import timedelta
20import hashlib21import hashlib
21from io import BytesIO22from io import BytesIO
22import logging23import logging
@@ -25,6 +26,7 @@
2526
26from django.contrib.auth.models import User27from django.contrib.auth.models import User
27from django.test.client import RequestFactory28from django.test.client import RequestFactory
29from django.utils import timezone
28from maasserver.clusterrpc.power_parameters import get_power_types30from maasserver.clusterrpc.power_parameters import get_power_types
29from maasserver.enum import (31from maasserver.enum import (
30 BOOT_RESOURCE_FILE_TYPE,32 BOOT_RESOURCE_FILE_TYPE,
@@ -107,6 +109,11 @@
107ALL_NODE_STATES = map_enum(NODE_STATUS).values()109ALL_NODE_STATES = map_enum(NODE_STATUS).values()
108110
109111
112# Use `undefined` instead of `None` for default factory arguments when `None`
113# is a reasonable value for the argument.
114undefined = object()
115
116
110class Messages:117class Messages:
111 """A class to record messages published by Django messaging118 """A class to record messages published by Django messaging
112 framework.119 framework.
@@ -231,12 +238,13 @@
231 device.save()238 device.save()
232 return device239 return device
233240
234 def make_Node(self, mac=False, hostname=None, status=None,241 def make_Node(
235 architecture="i386/generic", installable=True, updated=None,242 self, mac=False, hostname=None, status=None,
236 created=None, nodegroup=None, routers=None, zone=None,243 architecture="i386/generic", installable=True, updated=None,
237 power_type=None, networks=None, boot_type=None,244 created=None, nodegroup=None, routers=None, zone=None,
238 sortable_name=False, parent=None, power_state=None,245 networks=None, boot_type=None, sortable_name=False,
239 disable_ipv4=None, **kwargs):246 power_type=None, power_parameters=None, power_state=None,
247 power_state_updated=undefined, disable_ipv4=None, **kwargs):
240 """Make a :class:`Node`.248 """Make a :class:`Node`.
241249
242 :param sortable_name: If `True`, use a that will sort consistently250 :param sortable_name: If `True`, use a that will sort consistently
@@ -260,8 +268,13 @@
260 zone = self.make_Zone()268 zone = self.make_Zone()
261 if power_type is None:269 if power_type is None:
262 power_type = 'ether_wake'270 power_type = 'ether_wake'
271 if power_parameters is None:
272 power_parameters = ""
263 if power_state is None:273 if power_state is None:
264 power_state = self.pick_enum(POWER_STATE)274 power_state = self.pick_enum(POWER_STATE)
275 if power_state_updated is undefined:
276 power_state_updated = (
277 timezone.now() - timedelta(minutes=random.randint(0, 15)))
265 if disable_ipv4 is None:278 if disable_ipv4 is None:
266 disable_ipv4 = self.pick_bool()279 disable_ipv4 = self.pick_bool()
267 if boot_type is None:280 if boot_type is None:
@@ -269,9 +282,10 @@
269 node = Node(282 node = Node(
270 hostname=hostname, status=status, architecture=architecture,283 hostname=hostname, status=status, architecture=architecture,
271 installable=installable, nodegroup=nodegroup, routers=routers,284 installable=installable, nodegroup=nodegroup, routers=routers,
272 zone=zone, power_type=power_type, disable_ipv4=disable_ipv4,285 zone=zone, boot_type=boot_type, power_type=power_type,
273 parent=parent, boot_type=boot_type, power_state=power_state,286 power_parameters=power_parameters, power_state=power_state,
274 **kwargs)287 power_state_updated=power_state_updated,
288 disable_ipv4=disable_ipv4, **kwargs)
275 self._save_node_unchecked(node)289 self._save_node_unchecked(node)
276 # We do not generate random networks by default because the limited290 # We do not generate random networks by default because the limited
277 # number of VLAN identifiers (4,094) makes it very likely to291 # number of VLAN identifiers (4,094) makes it very likely to
278292
=== modified file 'src/maasserver/websockets/handlers/device.py'
--- src/maasserver/websockets/handlers/device.py 2015-05-27 16:17:55 +0000
+++ src/maasserver/websockets/handlers/device.py 2015-07-15 12:15:32 +0000
@@ -139,6 +139,7 @@
139 "boot_type",139 "boot_type",
140 "status",140 "status",
141 "power_parameters",141 "power_parameters",
142 "power_state_updated",
142 "disable_ipv4",143 "disable_ipv4",
143 "osystem",144 "osystem",
144 "power_type",145 "power_type",
145146
=== modified file 'src/maasserver/websockets/handlers/node.py'
--- src/maasserver/websockets/handlers/node.py 2015-07-10 09:08:58 +0000
+++ src/maasserver/websockets/handlers/node.py 2015-07-15 12:15:32 +0000
@@ -95,6 +95,9 @@
95 "token",95 "token",
96 "netboot",96 "netboot",
97 "agent_name",97 "agent_name",
98 # power_state_updated isn't needed in the client yet, plus it's
99 # not native to JSON. Omit for now.
100 "power_state_updated",
98 ]101 ]
99 list_fields = [102 list_fields = [
100 "system_id",103 "system_id",
101104
=== modified file 'src/provisioningserver/pserv_services/node_power_monitor_service.py'
--- src/provisioningserver/pserv_services/node_power_monitor_service.py 2015-05-07 18:14:38 +0000
+++ src/provisioningserver/pserv_services/node_power_monitor_service.py 2015-07-15 12:15:32 +0000
@@ -27,10 +27,6 @@
27)27)
28from provisioningserver.rpc.power import query_all_nodes28from provisioningserver.rpc.power import query_all_nodes
29from provisioningserver.rpc.region import ListNodePowerParameters29from provisioningserver.rpc.region import ListNodePowerParameters
30from provisioningserver.utils.twisted import (
31 pause,
32 retries,
33)
34from twisted.application.internet import TimerService30from twisted.application.internet import TimerService
35from twisted.internet.defer import inlineCallbacks31from twisted.internet.defer import inlineCallbacks
36from twisted.python import log32from twisted.python import log
@@ -42,7 +38,7 @@
42class NodePowerMonitorService(TimerService, object):38class NodePowerMonitorService(TimerService, object):
43 """Service to monitor the power status of all nodes in this cluster."""39 """Service to monitor the power status of all nodes in this cluster."""
4440
45 check_interval = timedelta(minutes=5).total_seconds()41 check_interval = timedelta(seconds=15).total_seconds()
46 max_nodes_at_once = 542 max_nodes_at_once = 5
4743
48 def __init__(self, cluster_uuid, clock=None):44 def __init__(self, cluster_uuid, clock=None):
@@ -57,42 +53,38 @@
57 Log errors on failure, but do not propagate them up; that will53 Log errors on failure, but do not propagate them up; that will
58 stop the timed loop from running.54 stop the timed loop from running.
59 """55 """
60 def query_nodes_failed(failure):56 try:
57 client = getRegionClient()
58 except NoConnectionsAvailable:
59 maaslog.debug(
60 "Cannot monitor nodes' power status; "
61 "region not available.")
62 else:
63 d = self.query_nodes(client, uuid)
64 d.addErrback(self.query_nodes_failed, uuid)
65 return d
66
67 @inlineCallbacks
68 def query_nodes(self, client, uuid):
69 # Get the nodes' power parameters from the region. Keep getting more
70 # power parameters until the region returns an empty list.
71 while True:
72 response = yield client(ListNodePowerParameters, uuid=uuid)
73 power_parameters = response['nodes']
74 if len(power_parameters) > 0:
75 yield query_all_nodes(
76 power_parameters, max_concurrency=self.max_nodes_at_once,
77 clock=self.clock)
78 else:
79 break
80
81 def query_nodes_failed(self, failure, uuid):
82 if failure.check(NoSuchCluster):
83 maaslog.error("Cluster %s is not recognised.", uuid)
84 else:
61 # Log the error in full to the Twisted log.85 # Log the error in full to the Twisted log.
62 log.err(failure)86 log.err(failure, "Querying node power states.")
63 # Log something concise to the MAAS log.87 # Log something concise to the MAAS log.
64 maaslog.error(88 maaslog.error(
65 "Failed to query nodes' power status: %s",89 "Failed to query nodes' power status: %s",
66 failure.getErrorMessage())90 failure.getErrorMessage())
67
68 return self.query_nodes(uuid).addErrback(query_nodes_failed)
69
70 @inlineCallbacks
71 def query_nodes(self, uuid):
72 # Retry a few times, since this service usually comes up before
73 # the RPC service.
74 for elapsed, remaining, wait in retries(15, 5, self.clock):
75 try:
76 client = getRegionClient()
77 except NoConnectionsAvailable:
78 yield pause(wait, self.clock)
79 else:
80 break
81 else:
82 maaslog.error(
83 "Cannot monitor nodes' power status; "
84 "region not available.")
85 return
86
87 # Get the nodes' power parameters from the region.
88 try:
89 response = yield client(ListNodePowerParameters, uuid=uuid)
90 except NoSuchCluster:
91 maaslog.error(
92 "This cluster (%s) is not recognised by the region.",
93 uuid)
94 else:
95 node_power_parameters = response['nodes']
96 yield query_all_nodes(
97 node_power_parameters,
98 max_concurrency=self.max_nodes_at_once, clock=self.clock)
9991
=== modified file 'src/provisioningserver/pserv_services/tests/test_node_power_monitor_service.py'
--- src/provisioningserver/pserv_services/tests/test_node_power_monitor_service.py 2015-05-22 15:52:13 +0000
+++ src/provisioningserver/pserv_services/tests/test_node_power_monitor_service.py 2015-07-15 12:15:32 +0000
@@ -17,10 +17,7 @@
1717
18from fixtures import FakeLogger18from fixtures import FakeLogger
19from maastesting.factory import factory19from maastesting.factory import factory
20from maastesting.matchers import (20from maastesting.matchers import MockCalledOnceWith
21 MockCalledOnceWith,
22 MockCallsMatch,
23)
24from maastesting.testcase import (21from maastesting.testcase import (
25 MAASTestCase,22 MAASTestCase,
26 MAASTwistedRunTest,23 MAASTwistedRunTest,
@@ -28,13 +25,14 @@
28from maastesting.twisted import TwistedLoggerFixture25from maastesting.twisted import TwistedLoggerFixture
29from mock import (26from mock import (
30 ANY,27 ANY,
31 call,28 sentinel,
32)29)
33from provisioningserver.pserv_services import (30from provisioningserver.pserv_services import (
34 node_power_monitor_service as npms,31 node_power_monitor_service as npms,
35)32)
36from provisioningserver.rpc import (33from provisioningserver.rpc import (
37 exceptions,34 exceptions,
35 getRegionClient,
38 region,36 region,
39)37)
40from provisioningserver.rpc.testing import MockClusterToRegionRPCFixture38from provisioningserver.rpc.testing import MockClusterToRegionRPCFixture
@@ -56,94 +54,84 @@
56 service = npms.NodePowerMonitorService(cluster_uuid)54 service = npms.NodePowerMonitorService(cluster_uuid)
57 self.assertThat(service, MatchesStructure.byEquality(55 self.assertThat(service, MatchesStructure.byEquality(
58 call=(service.try_query_nodes, (cluster_uuid,), {}),56 call=(service.try_query_nodes, (cluster_uuid,), {}),
59 step=(5 * 60), clock=None))57 step=15, clock=None))
6058
61 def make_monitor_service(self):59 def make_monitor_service(self):
62 cluster_uuid = factory.make_UUID()60 cluster_uuid = factory.make_UUID()
63 service = npms.NodePowerMonitorService(cluster_uuid, Clock())61 service = npms.NodePowerMonitorService(cluster_uuid, Clock())
64 return cluster_uuid, service62 return cluster_uuid, service
6563
66 def test_query_nodes_retries_getting_client(self):
67 cluster_uuid, service = self.make_monitor_service()
68
69 getRegionClient = self.patch(npms, "getRegionClient")
70 getRegionClient.side_effect = exceptions.NoConnectionsAvailable
71
72 def has_been_called_n_times(n):
73 calls = [call()] * n
74 return MockCallsMatch(*calls)
75
76 maaslog = self.useFixture(FakeLogger("maas"))
77
78 d = service.query_nodes(cluster_uuid)
79 # Immediately the first attempt to get a client happens.
80 self.assertThat(getRegionClient, has_been_called_n_times(1))
81 self.assertFalse(d.called)
82 # Followed by 3 more attempts as time passes.
83 service.clock.pump((5, 5, 5))
84 self.assertThat(getRegionClient, has_been_called_n_times(4))
85 # query_nodes returns after 15 seconds.
86 self.assertTrue(d.called)
87 self.assertIsNone(extract_result(d))
88
89 # A simple message is logged, but even this may be too noisy.
90 self.assertIn(
91 "Cannot monitor nodes' power status; region not available.",
92 maaslog.output)
93
94 def test_query_nodes_calls_the_region(self):64 def test_query_nodes_calls_the_region(self):
95 cluster_uuid, service = self.make_monitor_service()65 cluster_uuid, service = self.make_monitor_service()
9666
97 rpc_fixture = self.useFixture(MockClusterToRegionRPCFixture())67 rpc_fixture = self.useFixture(MockClusterToRegionRPCFixture())
98 client, io = rpc_fixture.makeEventLoop(region.ListNodePowerParameters)68 proto_region, io = rpc_fixture.makeEventLoop(
99 client.ListNodePowerParameters.return_value = succeed({"nodes": []})69 region.ListNodePowerParameters)
70 proto_region.ListNodePowerParameters.return_value = succeed(
71 {"nodes": []})
10072
101 d = service.query_nodes(cluster_uuid)73 d = service.query_nodes(getRegionClient(), cluster_uuid)
102 io.flush()74 io.flush()
10375
104 self.assertEqual(None, extract_result(d))76 self.assertEqual(None, extract_result(d))
105 self.assertThat(77 self.assertThat(
106 client.ListNodePowerParameters,78 proto_region.ListNodePowerParameters,
107 MockCalledOnceWith(ANY, uuid=cluster_uuid))79 MockCalledOnceWith(ANY, uuid=cluster_uuid))
10880
109 def test_query_nodes_calls_query_all_nodes(self):81 def test_query_nodes_calls_query_all_nodes(self):
110 cluster_uuid, service = self.make_monitor_service()82 cluster_uuid, service = self.make_monitor_service()
83 service.max_nodes_at_once = sentinel.max_nodes_at_once
84
85 example_power_parameters = {
86 "system_id": factory.make_UUID(),
87 "hostname": factory.make_hostname(),
88 "power_state": factory.make_name("power_state"),
89 "power_type": factory.make_name("power_type"),
90 "context": {},
91 }
11192
112 rpc_fixture = self.useFixture(MockClusterToRegionRPCFixture())93 rpc_fixture = self.useFixture(MockClusterToRegionRPCFixture())
113 client, io = rpc_fixture.makeEventLoop(region.ListNodePowerParameters)94 proto_region, io = rpc_fixture.makeEventLoop(
114 client.ListNodePowerParameters.return_value = succeed({"nodes": []})95 region.ListNodePowerParameters)
96 proto_region.ListNodePowerParameters.side_effect = [
97 succeed({"nodes": [example_power_parameters]}),
98 succeed({"nodes": []}),
99 ]
115100
116 query_all_nodes = self.patch(npms, "query_all_nodes")101 query_all_nodes = self.patch(npms, "query_all_nodes")
117102
118 d = service.query_nodes(cluster_uuid)103 d = service.query_nodes(getRegionClient(), cluster_uuid)
119 io.flush()104 io.flush()
120105
121 self.assertEqual(None, extract_result(d))106 self.assertEqual(None, extract_result(d))
122 self.assertThat(107 self.assertThat(
123 query_all_nodes,108 query_all_nodes,
124 MockCalledOnceWith(109 MockCalledOnceWith(
125 [], max_concurrency=service.max_nodes_at_once,110 [example_power_parameters],
111 max_concurrency=sentinel.max_nodes_at_once,
126 clock=service.clock))112 clock=service.clock))
127113
128 def test_query_nodes_copes_with_NoSuchCluster(self):114 def test_query_nodes_copes_with_NoSuchCluster(self):
129 cluster_uuid, service = self.make_monitor_service()115 cluster_uuid, service = self.make_monitor_service()
130116
131 rpc_fixture = self.useFixture(MockClusterToRegionRPCFixture())117 rpc_fixture = self.useFixture(MockClusterToRegionRPCFixture())
132 client, io = rpc_fixture.makeEventLoop(region.ListNodePowerParameters)118 proto_region, io = rpc_fixture.makeEventLoop(
133 client.ListNodePowerParameters.return_value = fail(119 region.ListNodePowerParameters)
120 proto_region.ListNodePowerParameters.return_value = fail(
134 exceptions.NoSuchCluster.from_uuid(cluster_uuid))121 exceptions.NoSuchCluster.from_uuid(cluster_uuid))
135122
136 d = service.query_nodes(cluster_uuid)123 d = service.query_nodes(getRegionClient(), cluster_uuid)
124 d.addErrback(service.query_nodes_failed, cluster_uuid)
137 with FakeLogger("maas") as maaslog:125 with FakeLogger("maas") as maaslog:
138 io.flush()126 io.flush()
139127
140 self.assertEqual(None, extract_result(d))128 self.assertEqual(None, extract_result(d))
141 self.assertDocTestMatches(129 self.assertDocTestMatches(
142 "This cluster (...) is not recognised by the region.",130 "Cluster ... is not recognised.", maaslog.output)
143 maaslog.output)
144131
145 def test_try_query_nodes_logs_other_errors(self):132 def test_try_query_nodes_logs_other_errors(self):
146 cluster_uuid, service = self.make_monitor_service()133 cluster_uuid, service = self.make_monitor_service()
134 self.patch(npms, "getRegionClient").return_value = sentinel.client
147135
148 query_nodes = self.patch(service, "query_nodes")136 query_nodes = self.patch(service, "query_nodes")
149 query_nodes.return_value = fail(137 query_nodes.return_value = fail(
150138
=== modified file 'src/provisioningserver/rpc/power.py'
--- src/provisioningserver/rpc/power.py 2015-07-10 09:08:58 +0000
+++ src/provisioningserver/rpc/power.py 2015-07-15 12:15:32 +0000
@@ -19,6 +19,7 @@
1919
20from datetime import timedelta20from datetime import timedelta
21from functools import partial21from functools import partial
22import sys
2223
23from provisioningserver.events import (24from provisioningserver.events import (
24 EVENT_TYPES,25 EVENT_TYPES,
@@ -331,33 +332,38 @@
331 # type, however this is left here to prevent PEBKAC.332 # type, however this is left here to prevent PEBKAC.
332 raise PowerActionFail("Unknown power_type '%s'" % power_type)333 raise PowerActionFail("Unknown power_type '%s'" % power_type)
333334
335 def check_power_state(state):
336 if state not in ("on", "off", "unknown"):
337 # This is considered an error.
338 raise PowerActionFail(state)
339
340 # Capture errors as we go along.
341 exc_info = None, None, None
342
334 # Use increasing waiting times to work around race conditions that could343 # Use increasing waiting times to work around race conditions that could
335 # arise when power querying the node.344 # arise when power querying the node.
336 for waiting_time in default_waiting_policy:345 for waiting_time in default_waiting_policy:
337 error = None
338 # Perform power query.346 # Perform power query.
339 try:347 try:
340 power_state = yield deferToThread(348 power_state = yield deferToThread(
341 perform_power_query, system_id, hostname, power_type, context)349 perform_power_query, system_id, hostname,
342 if power_state not in ("on", "off", "unknown"):350 power_type, context)
343 # This is considered an error.351 check_power_state(power_state)
344 raise PowerActionFail(power_state)352 except:
345 except PowerActionFail as e:353 # Hold the error; it may be reported later.
346 # Hold the error so if failure after retries, we can354 exc_info = sys.exc_info()
347 # log the reason.
348 error = e
349
350 # Wait before trying again.355 # Wait before trying again.
351 yield pause(waiting_time, clock)356 yield pause(waiting_time, clock)
352 continue357 else:
353 yield power_state_update(system_id, power_state)358 yield power_state_update(system_id, power_state)
354 returnValue(power_state)359 returnValue(power_state)
355360
356 # Send node is broken, since query failed after the multiple retries.361 # Reaching here means that things have gone wrong.
357 message = "Node could not be queried %s (%s) %s" % (362 assert exc_info != (None, None, None)
358 system_id, hostname, error)363 exc_type, exc_value, exc_trace = exc_info
364 message = "Power state could not be queried: %s" % (exc_value,)
359 yield power_query_failure(system_id, hostname, message)365 yield power_query_failure(system_id, hostname, message)
360 raise PowerActionFail(error)366 raise exc_type, exc_value, exc_trace
361367
362368
363def maaslog_report_success(node, power_state):369def maaslog_report_success(node, power_state):
364370
=== modified file 'src/provisioningserver/rpc/region.py'
--- src/provisioningserver/rpc/region.py 2015-05-07 18:14:38 +0000
+++ src/provisioningserver/rpc/region.py 2015-07-15 12:15:32 +0000
@@ -246,11 +246,16 @@
246246
247247
248class ListNodePowerParameters(amp.Command):248class ListNodePowerParameters(amp.Command):
249 """Return the list of power parameters for nodes249 """Return power parameters for the nodes in the specified cluster.
250 that this cluster controls.250
251251 This will only return power parameters for nodes that have power types for
252 Used to query all of the nodes that the cluster252 which MAAS has a query capability.
253 composes.253
254 It will return nodes in priority order. Those nodes at the beginning of
255 the list should be queried first.
256
257 It may return an empty list. This means that all nodes have been recently
258 queried. Take a break before asking again.
254259
255 :since: 1.7260 :since: 1.7
256 """261 """
257262
=== modified file 'src/provisioningserver/rpc/tests/test_power.py'
--- src/provisioningserver/rpc/tests/test_power.py 2015-05-22 15:52:13 +0000
+++ src/provisioningserver/rpc/tests/test_power.py 2015-07-15 12:15:32 +0000
@@ -30,6 +30,7 @@
30 MAASTwistedRunTest,30 MAASTwistedRunTest,
31)31)
32from maastesting.twisted import (32from maastesting.twisted import (
33 always_fail_with,
33 always_succeed_with,34 always_succeed_with,
34 TwistedLoggerFixture,35 TwistedLoggerFixture,
35)36)
@@ -41,6 +42,7 @@
41 sentinel,42 sentinel,
42)43)
43import provisioningserver44import provisioningserver
45from provisioningserver.drivers.power import PowerDriverRegistry
44from provisioningserver.events import EVENT_TYPES46from provisioningserver.events import EVENT_TYPES
45from provisioningserver.power.poweraction import PowerActionFail47from provisioningserver.power.poweraction import PowerActionFail
46from provisioningserver.rpc import (48from provisioningserver.rpc import (
@@ -643,6 +645,79 @@
643 self.assertEqual("off", extract_result(d))645 self.assertEqual("off", extract_result(d))
644646
645647
648class TestPowerQueryExceptions(MAASTestCase):
649
650 scenarios = tuple(
651 (power_type, {
652 "power_type": power_type,
653 "func": ( # Function to invoke driver.
654 "perform_power_driver_query"
655 if power_type in PowerDriverRegistry
656 else "perform_power_query"),
657 "waits": ( # Pauses between retries.
658 [] if power_type in PowerDriverRegistry
659 else power.default_waiting_policy),
660 "calls": ( # No. of calls to the driver.
661 1 if power_type in PowerDriverRegistry
662 else len(power.default_waiting_policy)),
663 })
664 for power_type in power.QUERY_POWER_TYPES
665 )
666
667 def test_get_power_state_captures_all_exceptions(self):
668 logger_twisted = self.useFixture(TwistedLoggerFixture())
669 logger_maaslog = self.useFixture(FakeLogger("maas"))
670
671 # Avoid threads here.
672 self.patch(power, "deferToThread", maybeDeferred)
673
674 exception_type = factory.make_exception_type()
675 exception_message = factory.make_string()
676 exception = exception_type(exception_message)
677
678 # Pretend the query always fails with `exception`.
679 query = self.patch_autospec(power, self.func)
680 query.side_effect = always_fail_with(exception)
681
682 # Intercept calls to power_query_failure().
683 self.patch_autospec(power, "power_query_failure")
684
685 system_id = factory.make_name('system_id')
686 hostname = factory.make_name('hostname')
687 context = sentinel.context
688 clock = Clock()
689
690 d = power.get_power_state(
691 system_id, hostname, self.power_type, context, clock)
692
693 # Crank through some number of retries.
694 for wait in self.waits:
695 self.assertFalse(d.called)
696 clock.advance(wait)
697 self.assertTrue(d.called)
698
699 # Finally the exception from the query is raised.
700 self.assertRaises(exception_type, extract_result, d)
701
702 # The broken power query function patched earlier was called the same
703 # number of times as there are steps in the default waiting policy.
704 expected_call = call(system_id, hostname, self.power_type, context)
705 expected_calls = [expected_call] * self.calls
706 self.assertThat(query, MockCallsMatch(*expected_calls))
707
708 # power_query_failure() was called once at the end with a message
709 # constructed using the error message we fabricated at the beginning.
710 expected_message = (
711 "Power state could not be queried: %s" % exception_message)
712 self.assertThat(power.power_query_failure, MockCalledOnceWith(
713 system_id, hostname, expected_message))
714
715 # Nothing was logged to the Twisted log or to maaslog; that happens
716 # elsewhere, in maaslog_query_failure() and maaslog_query().
717 self.assertEqual("", logger_twisted.output)
718 self.assertEqual("", logger_maaslog.output)
719
720
646class TestPowerQueryAsync(MAASTestCase):721class TestPowerQueryAsync(MAASTestCase):
647722
648 run_tests_with = MAASTwistedRunTest.make_factory(timeout=5)723 run_tests_with = MAASTwistedRunTest.make_factory(timeout=5)

Subscribers

People subscribed via source and target branches

to all changes: