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