blob: 8754550404ef3be548c58c4b0ae1b175fec4dcb3 [file] [log] [blame]
Simon Huntcda9c032016-04-11 10:32:54 -07001/*
2 * Copyright 2016 Open Networking Laboratory
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package org.onosproject.ui.impl.topo.model;
18
Simon Hunt23fb1352016-04-11 12:15:19 -070019import org.onosproject.cluster.ControllerNode;
Simon Hunt642bc452016-05-04 19:34:45 -070020import org.onosproject.cluster.NodeId;
Simon Hunt23fb1352016-04-11 12:15:19 -070021import org.onosproject.cluster.RoleInfo;
Simon Huntcda9c032016-04-11 10:32:54 -070022import org.onosproject.event.EventDispatcher;
23import org.onosproject.net.Device;
Simon Hunt23fb1352016-04-11 12:15:19 -070024import org.onosproject.net.DeviceId;
Simon Huntc0f20c12016-05-09 09:30:20 -070025import org.onosproject.net.EdgeLink;
Simon Hunt23fb1352016-04-11 12:15:19 -070026import org.onosproject.net.Host;
Simon Huntc0f20c12016-05-09 09:30:20 -070027import org.onosproject.net.HostId;
28import org.onosproject.net.HostLocation;
Simon Hunt23fb1352016-04-11 12:15:19 -070029import org.onosproject.net.Link;
30import org.onosproject.net.region.Region;
Simon Huntc0f20c12016-05-09 09:30:20 -070031import org.onosproject.net.region.RegionId;
Simon Hunt642bc452016-05-04 19:34:45 -070032import org.onosproject.ui.model.ServiceBundle;
Simon Hunt338a3b42016-04-14 09:43:52 -070033import org.onosproject.ui.model.topo.UiClusterMember;
Simon Huntcda9c032016-04-11 10:32:54 -070034import org.onosproject.ui.model.topo.UiDevice;
Simon Huntc0f20c12016-05-09 09:30:20 -070035import org.onosproject.ui.model.topo.UiElement;
36import org.onosproject.ui.model.topo.UiHost;
37import org.onosproject.ui.model.topo.UiLink;
38import org.onosproject.ui.model.topo.UiLinkId;
39import org.onosproject.ui.model.topo.UiRegion;
Simon Hunt23fb1352016-04-11 12:15:19 -070040import org.onosproject.ui.model.topo.UiTopology;
Simon Hunt642bc452016-05-04 19:34:45 -070041import org.slf4j.Logger;
42import org.slf4j.LoggerFactory;
Simon Hunt23fb1352016-04-11 12:15:19 -070043
Simon Huntd5b96732016-07-08 13:22:27 -070044import java.util.List;
Simon Huntc0f20c12016-05-09 09:30:20 -070045import java.util.Set;
46
47import static org.onosproject.net.DefaultEdgeLink.createEdgeLink;
Simon Hunt642bc452016-05-04 19:34:45 -070048import static org.onosproject.ui.impl.topo.model.UiModelEvent.Type.CLUSTER_MEMBER_ADDED_OR_UPDATED;
49import static org.onosproject.ui.impl.topo.model.UiModelEvent.Type.CLUSTER_MEMBER_REMOVED;
50import static org.onosproject.ui.impl.topo.model.UiModelEvent.Type.DEVICE_ADDED_OR_UPDATED;
Simon Hunt23fb1352016-04-11 12:15:19 -070051import static org.onosproject.ui.impl.topo.model.UiModelEvent.Type.DEVICE_REMOVED;
Simon Huntc0f20c12016-05-09 09:30:20 -070052import static org.onosproject.ui.impl.topo.model.UiModelEvent.Type.HOST_ADDED_OR_UPDATED;
53import static org.onosproject.ui.impl.topo.model.UiModelEvent.Type.HOST_MOVED;
54import static org.onosproject.ui.impl.topo.model.UiModelEvent.Type.HOST_REMOVED;
55import static org.onosproject.ui.impl.topo.model.UiModelEvent.Type.LINK_ADDED_OR_UPDATED;
56import static org.onosproject.ui.impl.topo.model.UiModelEvent.Type.LINK_REMOVED;
57import static org.onosproject.ui.impl.topo.model.UiModelEvent.Type.REGION_ADDED_OR_UPDATED;
58import static org.onosproject.ui.impl.topo.model.UiModelEvent.Type.REGION_REMOVED;
59import static org.onosproject.ui.model.topo.UiLinkId.uiLinkId;
Simon Huntcda9c032016-04-11 10:32:54 -070060
61/**
62 * UI Topology Model cache.
63 */
64class ModelCache {
65
Simon Huntc0f20c12016-05-09 09:30:20 -070066 private static final String E_NO_ELEMENT = "Tried to remove non-member {}: {}";
67
Simon Hunt642bc452016-05-04 19:34:45 -070068 private static final Logger log = LoggerFactory.getLogger(ModelCache.class);
69
70 private final ServiceBundle services;
Simon Huntcda9c032016-04-11 10:32:54 -070071 private final EventDispatcher dispatcher;
Simon Hunt23fb1352016-04-11 12:15:19 -070072 private final UiTopology uiTopology = new UiTopology();
Simon Huntcda9c032016-04-11 10:32:54 -070073
Simon Hunt642bc452016-05-04 19:34:45 -070074 ModelCache(ServiceBundle services, EventDispatcher eventDispatcher) {
75 this.services = services;
Simon Huntcda9c032016-04-11 10:32:54 -070076 this.dispatcher = eventDispatcher;
77 }
78
Simon Hunt338a3b42016-04-14 09:43:52 -070079 @Override
80 public String toString() {
81 return "ModelCache{" + uiTopology + "}";
82 }
83
Simon Huntc0f20c12016-05-09 09:30:20 -070084 private void postEvent(UiModelEvent.Type type, UiElement subject) {
85 dispatcher.post(new UiModelEvent(type, subject));
86 }
87
Simon Huntcda9c032016-04-11 10:32:54 -070088 void clear() {
Simon Hunt23fb1352016-04-11 12:15:19 -070089 uiTopology.clear();
Simon Huntcda9c032016-04-11 10:32:54 -070090 }
91
92 /**
Simon Huntc0f20c12016-05-09 09:30:20 -070093 * Create our internal model of the global topology. An assumption we are
94 * making is that the topology is empty to start.
Simon Huntcda9c032016-04-11 10:32:54 -070095 */
96 void load() {
Simon Huntc0f20c12016-05-09 09:30:20 -070097 loadClusterMembers();
98 loadRegions();
99 loadDevices();
100 loadLinks();
101 loadHosts();
Simon Huntcda9c032016-04-11 10:32:54 -0700102 }
103
104
Simon Huntc0f20c12016-05-09 09:30:20 -0700105 // === CLUSTER MEMBERS
106
107 private UiClusterMember addNewClusterMember(ControllerNode n) {
108 UiClusterMember member = new UiClusterMember(uiTopology, n);
109 uiTopology.add(member);
110 return member;
111 }
112
113 private void updateClusterMember(UiClusterMember member) {
114 ControllerNode.State state = services.cluster().getState(member.id());
115 member.setState(state);
116 member.setMastership(services.mastership().getDevicesOf(member.id()));
117 // NOTE: 'UI-attached' is session-based data, not global, so will
118 // be set elsewhere
119 }
120
121 private void loadClusterMembers() {
122 for (ControllerNode n : services.cluster().getNodes()) {
123 UiClusterMember member = addNewClusterMember(n);
124 updateClusterMember(member);
125 }
126 }
127
128 // invoked from UiSharedTopologyModel cluster event listener
Simon Hunt338a3b42016-04-14 09:43:52 -0700129 void addOrUpdateClusterMember(ControllerNode cnode) {
Simon Hunt642bc452016-05-04 19:34:45 -0700130 NodeId id = cnode.id();
131 UiClusterMember member = uiTopology.findClusterMember(id);
132 if (member == null) {
Simon Huntc0f20c12016-05-09 09:30:20 -0700133 member = addNewClusterMember(cnode);
Simon Hunt338a3b42016-04-14 09:43:52 -0700134 }
Simon Huntc0f20c12016-05-09 09:30:20 -0700135 updateClusterMember(member);
Simon Hunt338a3b42016-04-14 09:43:52 -0700136
Simon Huntc0f20c12016-05-09 09:30:20 -0700137 postEvent(CLUSTER_MEMBER_ADDED_OR_UPDATED, member);
Simon Hunt338a3b42016-04-14 09:43:52 -0700138 }
139
Simon Huntc0f20c12016-05-09 09:30:20 -0700140 // package private for unit test access
141 UiClusterMember accessClusterMember(NodeId id) {
142 return uiTopology.findClusterMember(id);
143 }
144
145 // invoked from UiSharedTopologyModel cluster event listener
Simon Hunt338a3b42016-04-14 09:43:52 -0700146 void removeClusterMember(ControllerNode cnode) {
Simon Hunt642bc452016-05-04 19:34:45 -0700147 NodeId id = cnode.id();
148 UiClusterMember member = uiTopology.findClusterMember(id);
149 if (member != null) {
150 uiTopology.remove(member);
Simon Huntc0f20c12016-05-09 09:30:20 -0700151 postEvent(CLUSTER_MEMBER_REMOVED, member);
Simon Hunt642bc452016-05-04 19:34:45 -0700152 } else {
Simon Huntc0f20c12016-05-09 09:30:20 -0700153 log.warn(E_NO_ELEMENT, "cluster node", id);
Simon Hunt642bc452016-05-04 19:34:45 -0700154 }
Simon Hunt338a3b42016-04-14 09:43:52 -0700155 }
156
Simon Huntd5b96732016-07-08 13:22:27 -0700157 List<UiClusterMember> getAllClusterMembers() {
158 return uiTopology.allClusterMembers();
159 }
160
Simon Huntc0f20c12016-05-09 09:30:20 -0700161
162 // === MASTERSHIP CHANGES
163
164 // invoked from UiSharedTopologyModel mastership listener
Simon Hunt338a3b42016-04-14 09:43:52 -0700165 void updateMasterships(DeviceId deviceId, RoleInfo roleInfo) {
Simon Huntc0f20c12016-05-09 09:30:20 -0700166 // To think about:: do we need to store mastership info?
167 // or can we rely on looking it up live?
Simon Hunt338a3b42016-04-14 09:43:52 -0700168 // TODO: store the updated mastership information
169 // TODO: post event
170 }
171
Simon Huntc0f20c12016-05-09 09:30:20 -0700172
173 // === REGIONS
174
175 private UiRegion addNewRegion(Region r) {
176 UiRegion region = new UiRegion(uiTopology, r);
177 uiTopology.add(region);
178 return region;
179 }
180
181 private void updateRegion(UiRegion region) {
182 Set<DeviceId> devs = services.region().getRegionDevices(region.id());
183 region.reconcileDevices(devs);
184 }
185
186 private void loadRegions() {
187 for (Region r : services.region().getRegions()) {
188 UiRegion region = addNewRegion(r);
189 updateRegion(region);
190 }
191 }
192
193 // invoked from UiSharedTopologyModel region listener
Simon Hunt338a3b42016-04-14 09:43:52 -0700194 void addOrUpdateRegion(Region region) {
Simon Huntc0f20c12016-05-09 09:30:20 -0700195 RegionId id = region.id();
196 UiRegion uiRegion = uiTopology.findRegion(id);
197 if (uiRegion == null) {
198 uiRegion = addNewRegion(region);
199 }
200 updateRegion(uiRegion);
201
202 postEvent(REGION_ADDED_OR_UPDATED, uiRegion);
Simon Hunt338a3b42016-04-14 09:43:52 -0700203 }
204
Simon Hunt58a0dd02016-05-17 11:54:23 -0700205 // package private for unit test access
206 UiRegion accessRegion(RegionId id) {
Simon Huntd5b96732016-07-08 13:22:27 -0700207 return id == null ? null : uiTopology.findRegion(id);
Simon Hunt58a0dd02016-05-17 11:54:23 -0700208 }
209
Simon Huntc0f20c12016-05-09 09:30:20 -0700210 // invoked from UiSharedTopologyModel region listener
Simon Hunt338a3b42016-04-14 09:43:52 -0700211 void removeRegion(Region region) {
Simon Huntc0f20c12016-05-09 09:30:20 -0700212 RegionId id = region.id();
213 UiRegion uiRegion = uiTopology.findRegion(id);
214 if (uiRegion != null) {
215 uiTopology.remove(uiRegion);
216 postEvent(REGION_REMOVED, uiRegion);
217 } else {
218 log.warn(E_NO_ELEMENT, "region", id);
219 }
Simon Hunt338a3b42016-04-14 09:43:52 -0700220 }
221
Simon Huntc0f20c12016-05-09 09:30:20 -0700222
223 // === DEVICES
224
225 private UiDevice addNewDevice(Device d) {
226 UiDevice device = new UiDevice(uiTopology, d);
227 uiTopology.add(device);
228 return device;
229 }
230
231 private void updateDevice(UiDevice device) {
Thomas Vachuska92b016b2016-05-20 11:37:57 -0700232 Region regionForDevice = services.region().getRegionForDevice(device.id());
233 if (regionForDevice != null) {
234 device.setRegionId(regionForDevice.id());
235 }
Simon Huntc0f20c12016-05-09 09:30:20 -0700236 }
237
238 private void loadDevices() {
239 for (Device d : services.device().getDevices()) {
240 UiDevice device = addNewDevice(d);
241 updateDevice(device);
242 }
243 }
244
245 // invoked from UiSharedTopologyModel device listener
Simon Huntcda9c032016-04-11 10:32:54 -0700246 void addOrUpdateDevice(Device device) {
Simon Huntc0f20c12016-05-09 09:30:20 -0700247 DeviceId id = device.id();
248 UiDevice uiDevice = uiTopology.findDevice(id);
249 if (uiDevice == null) {
250 uiDevice = addNewDevice(device);
251 }
252 updateDevice(uiDevice);
Simon Huntcda9c032016-04-11 10:32:54 -0700253
Simon Huntc0f20c12016-05-09 09:30:20 -0700254 postEvent(DEVICE_ADDED_OR_UPDATED, uiDevice);
Simon Huntcda9c032016-04-11 10:32:54 -0700255 }
256
Simon Hunt58a0dd02016-05-17 11:54:23 -0700257 // package private for unit test access
258 UiDevice accessDevice(DeviceId id) {
259 return uiTopology.findDevice(id);
260 }
261
Simon Huntc0f20c12016-05-09 09:30:20 -0700262 // invoked from UiSharedTopologyModel device listener
Simon Huntcda9c032016-04-11 10:32:54 -0700263 void removeDevice(Device device) {
Simon Huntc0f20c12016-05-09 09:30:20 -0700264 DeviceId id = device.id();
265 UiDevice uiDevice = uiTopology.findDevice(id);
266 if (uiDevice != null) {
267 uiTopology.remove(uiDevice);
268 postEvent(DEVICE_REMOVED, uiDevice);
269 } else {
270 log.warn(E_NO_ELEMENT, "device", id);
271 }
Simon Huntcda9c032016-04-11 10:32:54 -0700272 }
273
Simon Huntc0f20c12016-05-09 09:30:20 -0700274
275 // === LINKS
276
277 private UiLink addNewLink(UiLinkId id) {
278 UiLink uiLink = new UiLink(uiTopology, id);
279 uiTopology.add(uiLink);
280 return uiLink;
281 }
282
283 private void updateLink(UiLink uiLink, Link link) {
284 uiLink.attachBackingLink(link);
285 }
286
287 private void loadLinks() {
288 for (Link link : services.link().getLinks()) {
289 UiLinkId id = uiLinkId(link);
290
291 UiLink uiLink = uiTopology.findLink(id);
292 if (uiLink == null) {
293 uiLink = addNewLink(id);
294 }
295 updateLink(uiLink, link);
296 }
297 }
298
299 // invoked from UiSharedTopologyModel link listener
Simon Hunt23fb1352016-04-11 12:15:19 -0700300 void addOrUpdateLink(Link link) {
Simon Huntc0f20c12016-05-09 09:30:20 -0700301 UiLinkId id = uiLinkId(link);
302 UiLink uiLink = uiTopology.findLink(id);
303 if (uiLink == null) {
304 uiLink = addNewLink(id);
305 }
306 updateLink(uiLink, link);
307
308 postEvent(LINK_ADDED_OR_UPDATED, uiLink);
Simon Hunt23fb1352016-04-11 12:15:19 -0700309 }
310
Simon Hunt58a0dd02016-05-17 11:54:23 -0700311 // package private for unit test access
312 UiLink accessLink(UiLinkId id) {
313 return uiTopology.findLink(id);
314 }
315
Simon Huntc0f20c12016-05-09 09:30:20 -0700316 // invoked from UiSharedTopologyModel link listener
Simon Hunt23fb1352016-04-11 12:15:19 -0700317 void removeLink(Link link) {
Simon Huntc0f20c12016-05-09 09:30:20 -0700318 UiLinkId id = uiLinkId(link);
319 UiLink uiLink = uiTopology.findLink(id);
320 if (uiLink != null) {
321 boolean remaining = uiLink.detachBackingLink(link);
322 if (remaining) {
323 postEvent(LINK_ADDED_OR_UPDATED, uiLink);
324 } else {
325 uiTopology.remove(uiLink);
326 postEvent(LINK_REMOVED, uiLink);
327 }
328 } else {
329 log.warn(E_NO_ELEMENT, "link", id);
330 }
Simon Hunt23fb1352016-04-11 12:15:19 -0700331 }
332
Simon Huntc0f20c12016-05-09 09:30:20 -0700333
334 // === HOSTS
335
Simon Hunt58a0dd02016-05-17 11:54:23 -0700336 private EdgeLink synthesizeLink(Host h) {
337 return createEdgeLink(h, true);
338 }
339
Simon Huntc0f20c12016-05-09 09:30:20 -0700340 private UiHost addNewHost(Host h) {
341 UiHost host = new UiHost(uiTopology, h);
342 uiTopology.add(host);
343
Simon Hunt58a0dd02016-05-17 11:54:23 -0700344 EdgeLink elink = synthesizeLink(h);
345 UiLinkId elinkId = uiLinkId(elink);
346 host.setEdgeLinkId(elinkId);
347
348 // add synthesized edge link to the topology
349 UiLink edgeLink = addNewLink(elinkId);
350 edgeLink.attachEdgeLink(elink);
Simon Huntc0f20c12016-05-09 09:30:20 -0700351
352 return host;
353 }
354
Simon Hunt58a0dd02016-05-17 11:54:23 -0700355 private void insertNewUiLink(UiLinkId id, EdgeLink e) {
356 UiLink newEdgeLink = addNewLink(id);
357 newEdgeLink.attachEdgeLink(e);
Simon Huntc0f20c12016-05-09 09:30:20 -0700358
Simon Huntc0f20c12016-05-09 09:30:20 -0700359 }
360
361 private void updateHost(UiHost uiHost, Host h) {
Simon Hunt58a0dd02016-05-17 11:54:23 -0700362 UiLink existing = uiTopology.findLink(uiHost.edgeLinkId());
363
364 EdgeLink currentElink = synthesizeLink(h);
365 UiLinkId currentElinkId = uiLinkId(currentElink);
366
367 if (existing != null) {
368 if (!currentElinkId.equals(existing.id())) {
369 // edge link has changed
370 insertNewUiLink(currentElinkId, currentElink);
371 uiHost.setEdgeLinkId(currentElinkId);
372
373 uiTopology.remove(existing);
374 }
375
376 } else {
377 // no previously existing edge link
378 insertNewUiLink(currentElinkId, currentElink);
379 uiHost.setEdgeLinkId(currentElinkId);
380
381 }
382
Simon Huntc0f20c12016-05-09 09:30:20 -0700383 HostLocation hloc = h.location();
384 uiHost.setLocation(hloc.deviceId(), hloc.port());
Simon Huntc0f20c12016-05-09 09:30:20 -0700385 }
386
387 private void loadHosts() {
388 for (Host h : services.host().getHosts()) {
389 UiHost host = addNewHost(h);
390 updateHost(host, h);
391 }
392 }
393
394 // invoked from UiSharedTopologyModel host listener
Simon Hunt23fb1352016-04-11 12:15:19 -0700395 void addOrUpdateHost(Host host) {
Simon Huntc0f20c12016-05-09 09:30:20 -0700396 HostId id = host.id();
397 UiHost uiHost = uiTopology.findHost(id);
398 if (uiHost == null) {
399 uiHost = addNewHost(host);
400 }
401 updateHost(uiHost, host);
402
403 postEvent(HOST_ADDED_OR_UPDATED, uiHost);
Simon Hunt23fb1352016-04-11 12:15:19 -0700404 }
405
Simon Huntc0f20c12016-05-09 09:30:20 -0700406 // invoked from UiSharedTopologyModel host listener
Simon Hunt23fb1352016-04-11 12:15:19 -0700407 void moveHost(Host host, Host prevHost) {
Simon Huntc0f20c12016-05-09 09:30:20 -0700408 UiHost uiHost = uiTopology.findHost(prevHost.id());
Simon Hunt58a0dd02016-05-17 11:54:23 -0700409 if (uiHost != null) {
410 updateHost(uiHost, host);
411 postEvent(HOST_MOVED, uiHost);
412 } else {
413 log.warn(E_NO_ELEMENT, "host", prevHost.id());
414 }
415 }
Simon Huntc0f20c12016-05-09 09:30:20 -0700416
Simon Hunt58a0dd02016-05-17 11:54:23 -0700417 // package private for unit test access
418 UiHost accessHost(HostId id) {
419 return uiTopology.findHost(id);
Simon Hunt23fb1352016-04-11 12:15:19 -0700420 }
421
Simon Huntc0f20c12016-05-09 09:30:20 -0700422 // invoked from UiSharedTopologyModel host listener
Simon Hunt23fb1352016-04-11 12:15:19 -0700423 void removeHost(Host host) {
Simon Huntc0f20c12016-05-09 09:30:20 -0700424 HostId id = host.id();
425 UiHost uiHost = uiTopology.findHost(id);
426 if (uiHost != null) {
Simon Hunt58a0dd02016-05-17 11:54:23 -0700427 UiLink edgeLink = uiTopology.findLink(uiHost.edgeLinkId());
428 uiTopology.remove(edgeLink);
Simon Huntc0f20c12016-05-09 09:30:20 -0700429 uiTopology.remove(uiHost);
Simon Huntc0f20c12016-05-09 09:30:20 -0700430 postEvent(HOST_REMOVED, uiHost);
431 } else {
432 log.warn(E_NO_ELEMENT, "host", id);
433 }
Simon Hunt23fb1352016-04-11 12:15:19 -0700434 }
Simon Hunt338a3b42016-04-14 09:43:52 -0700435
Simon Huntc0f20c12016-05-09 09:30:20 -0700436
437 // === CACHE STATISTICS
438
Simon Hunt338a3b42016-04-14 09:43:52 -0700439 /**
Simon Hunt58a0dd02016-05-17 11:54:23 -0700440 * Returns a detailed (multi-line) string showing the contents of the cache.
441 *
442 * @return detailed string
443 */
444 public String dumpString() {
445 return uiTopology.dumpString();
446 }
447
448 /**
Simon Hunt338a3b42016-04-14 09:43:52 -0700449 * Returns the number of members in the cluster.
450 *
451 * @return number of cluster members
452 */
453 public int clusterMemberCount() {
454 return uiTopology.clusterMemberCount();
455 }
456
457 /**
Simon Huntc0f20c12016-05-09 09:30:20 -0700458 * Returns the number of regions in the topology.
Simon Hunt338a3b42016-04-14 09:43:52 -0700459 *
460 * @return number of regions
461 */
462 public int regionCount() {
463 return uiTopology.regionCount();
464 }
Simon Hunt58a0dd02016-05-17 11:54:23 -0700465
466 /**
467 * Returns the number of devices in the topology.
468 *
469 * @return number of devices
470 */
471 public int deviceCount() {
472 return uiTopology.deviceCount();
473 }
474
475 /**
476 * Returns the number of links in the topology.
477 *
478 * @return number of links
479 */
480 public int linkCount() {
481 return uiTopology.linkCount();
482 }
483
484 /**
485 * Returns the number of hosts in the topology.
486 *
487 * @return number of hosts
488 */
489 public int hostCount() {
490 return uiTopology.hostCount();
491 }
Simon Huntd5b96732016-07-08 13:22:27 -0700492
Simon Huntcda9c032016-04-11 10:32:54 -0700493}