blob: 71d73f9996b6fd54fd44c4fd270542e0e04bf27d [file] [log] [blame]
Thomas Vachuska33979fd2015-07-31 11:41:14 -07001/*
Brian O'Connor5ab426f2016-04-09 01:19:45 -07002 * Copyright 2015-present Open Networking Laboratory
Thomas Vachuska33979fd2015-07-31 11:41:14 -07003 *
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 */
16package org.onosproject.incubator.net.virtual.impl;
17
Brian Stanke86914282016-05-25 15:36:50 -040018import com.google.common.collect.Maps;
Thomas Vachuska33979fd2015-07-31 11:41:14 -070019import org.apache.felix.scr.annotations.Activate;
20import org.apache.felix.scr.annotations.Component;
21import org.apache.felix.scr.annotations.Deactivate;
22import org.apache.felix.scr.annotations.Reference;
23import org.apache.felix.scr.annotations.ReferenceCardinality;
24import org.apache.felix.scr.annotations.Service;
Brian Stanke11f6d532016-07-05 16:17:59 -040025import org.onlab.osgi.DefaultServiceDirectory;
Brian Stanke7a81b532016-06-14 15:43:51 -040026import org.onlab.packet.IpAddress;
27import org.onlab.packet.MacAddress;
28import org.onlab.packet.VlanId;
Thomas Vachuska33979fd2015-07-31 11:41:14 -070029import org.onosproject.incubator.net.tunnel.TunnelId;
30import org.onosproject.incubator.net.virtual.NetworkId;
31import org.onosproject.incubator.net.virtual.TenantId;
32import org.onosproject.incubator.net.virtual.VirtualDevice;
Brian Stanke7a81b532016-06-14 15:43:51 -040033import org.onosproject.incubator.net.virtual.VirtualHost;
Thomas Vachuska33979fd2015-07-31 11:41:14 -070034import org.onosproject.incubator.net.virtual.VirtualLink;
35import org.onosproject.incubator.net.virtual.VirtualNetwork;
36import org.onosproject.incubator.net.virtual.VirtualNetworkAdminService;
37import org.onosproject.incubator.net.virtual.VirtualNetworkEvent;
Brian Stanke11f6d532016-07-05 16:17:59 -040038import org.onosproject.incubator.net.virtual.VirtualNetworkIntent;
Thomas Vachuska33979fd2015-07-31 11:41:14 -070039import org.onosproject.incubator.net.virtual.VirtualNetworkListener;
Thomas Vachuska3d62fd72015-09-25 14:58:13 -070040import org.onosproject.incubator.net.virtual.VirtualNetworkProvider;
Thomas Vachuska858f65b2016-07-06 10:46:01 -070041import org.onosproject.incubator.net.virtual.VirtualNetworkProviderRegistry;
Thomas Vachuska3d62fd72015-09-25 14:58:13 -070042import org.onosproject.incubator.net.virtual.VirtualNetworkProviderService;
Thomas Vachuska33979fd2015-07-31 11:41:14 -070043import org.onosproject.incubator.net.virtual.VirtualNetworkService;
44import org.onosproject.incubator.net.virtual.VirtualNetworkStore;
45import org.onosproject.incubator.net.virtual.VirtualNetworkStoreDelegate;
46import org.onosproject.incubator.net.virtual.VirtualPort;
47import org.onosproject.net.ConnectPoint;
48import org.onosproject.net.DeviceId;
Brian Stanke7a81b532016-06-14 15:43:51 -040049import org.onosproject.net.HostId;
50import org.onosproject.net.HostLocation;
Brian Stanke612cebf2016-05-02 10:21:33 -040051import org.onosproject.net.Link;
Thomas Vachuska33979fd2015-07-31 11:41:14 -070052import org.onosproject.net.Port;
53import org.onosproject.net.PortNumber;
Brian Stanke86914282016-05-25 15:36:50 -040054import org.onosproject.net.device.DeviceService;
Claudine Chiu30a8a2a2016-07-14 17:09:04 -040055import org.onosproject.net.host.HostService;
Brian Stanke11f6d532016-07-05 16:17:59 -040056import org.onosproject.net.intent.IntentEvent;
57import org.onosproject.net.intent.IntentListener;
58import org.onosproject.net.intent.IntentService;
59import org.onosproject.net.intent.IntentState;
Brian Stanke86914282016-05-25 15:36:50 -040060import org.onosproject.net.link.LinkService;
Thomas Vachuska3d62fd72015-09-25 14:58:13 -070061import org.onosproject.net.provider.AbstractListenerProviderRegistry;
62import org.onosproject.net.provider.AbstractProviderService;
Brian Stanke8e9f8d12016-06-08 14:48:33 -040063import org.onosproject.net.topology.TopologyService;
Thomas Vachuska33979fd2015-07-31 11:41:14 -070064import org.slf4j.Logger;
65import org.slf4j.LoggerFactory;
66
Brian Stanke86914282016-05-25 15:36:50 -040067import java.util.Map;
Thomas Vachuska33979fd2015-07-31 11:41:14 -070068import java.util.Set;
69
70import static com.google.common.base.Preconditions.checkNotNull;
71
72/**
73 * Implementation of the virtual network service.
74 */
75@Component(immediate = true)
76@Service
77public class VirtualNetworkManager
Thomas Vachuska3d62fd72015-09-25 14:58:13 -070078 extends AbstractListenerProviderRegistry<VirtualNetworkEvent, VirtualNetworkListener,
Thomas Vachuska858f65b2016-07-06 10:46:01 -070079 VirtualNetworkProvider, VirtualNetworkProviderService>
80 implements VirtualNetworkService, VirtualNetworkAdminService, VirtualNetworkProviderRegistry {
Thomas Vachuska33979fd2015-07-31 11:41:14 -070081
82 private final Logger log = LoggerFactory.getLogger(getClass());
83
84 private static final String TENANT_NULL = "Tenant ID cannot be null";
85 private static final String NETWORK_NULL = "Network ID cannot be null";
86 private static final String DEVICE_NULL = "Device ID cannot be null";
87 private static final String LINK_POINT_NULL = "Link end-point cannot be null";
88
89 @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
90 protected VirtualNetworkStore store;
91
Brian Stanke11f6d532016-07-05 16:17:59 -040092 @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
93 protected IntentService intentService;
94
95 private final InternalVirtualIntentListener intentListener = new InternalVirtualIntentListener();
96
Brian Stanke0e5c94e2016-03-08 11:20:04 -050097 private VirtualNetworkStoreDelegate delegate = this::post;
Thomas Vachuska33979fd2015-07-31 11:41:14 -070098
Thomas Vachuska3d62fd72015-09-25 14:58:13 -070099 // TODO: figure out how to coordinate "implementation" of a virtual network in a cluster
Thomas Vachuska33979fd2015-07-31 11:41:14 -0700100
Brian Stanke11f6d532016-07-05 16:17:59 -0400101 /**
102 * Only used for Junit test methods outside of this package.
103 *
104 * @param store virtual network store
105 */
106 public void setStore(VirtualNetworkStore store) {
107 this.store = store;
108 }
109
110 /**
111 * Only used for Junit test methods outside of this package.
112 *
113 * @param intentService intent service
114 */
115
116 public void setIntentService(IntentService intentService) {
117 this.intentService = intentService;
118 }
119
Thomas Vachuska33979fd2015-07-31 11:41:14 -0700120 @Activate
Brian Stanke11f6d532016-07-05 16:17:59 -0400121 public void activate() {
Thomas Vachuska33979fd2015-07-31 11:41:14 -0700122 store.setDelegate(delegate);
Brian Stanke0e5c94e2016-03-08 11:20:04 -0500123 eventDispatcher.addSink(VirtualNetworkEvent.class, listenerRegistry);
Brian Stanke11f6d532016-07-05 16:17:59 -0400124 intentService.addListener(intentListener);
Thomas Vachuska33979fd2015-07-31 11:41:14 -0700125 log.info("Started");
126 }
127
128 @Deactivate
Brian Stanke11f6d532016-07-05 16:17:59 -0400129 public void deactivate() {
Thomas Vachuska33979fd2015-07-31 11:41:14 -0700130 store.unsetDelegate(delegate);
Brian Stanke0e5c94e2016-03-08 11:20:04 -0500131 eventDispatcher.removeSink(VirtualNetworkEvent.class);
Brian Stanke11f6d532016-07-05 16:17:59 -0400132 intentService.removeListener(intentListener);
Thomas Vachuska33979fd2015-07-31 11:41:14 -0700133 log.info("Stopped");
134 }
135
136 @Override
137 public void registerTenantId(TenantId tenantId) {
138 checkNotNull(tenantId, TENANT_NULL);
139 store.addTenantId(tenantId);
140 }
141
142 @Override
143 public void unregisterTenantId(TenantId tenantId) {
144 checkNotNull(tenantId, TENANT_NULL);
145 store.removeTenantId(tenantId);
146 }
147
148 @Override
149 public Set<TenantId> getTenantIds() {
150 return store.getTenantIds();
151 }
152
153 @Override
154 public VirtualNetwork createVirtualNetwork(TenantId tenantId) {
155 checkNotNull(tenantId, TENANT_NULL);
156 return store.addNetwork(tenantId);
157 }
158
159 @Override
160 public void removeVirtualNetwork(NetworkId networkId) {
161 checkNotNull(networkId, NETWORK_NULL);
162 store.removeNetwork(networkId);
163 }
164
165 @Override
166 public VirtualDevice createVirtualDevice(NetworkId networkId, DeviceId deviceId) {
167 checkNotNull(networkId, NETWORK_NULL);
168 checkNotNull(deviceId, DEVICE_NULL);
169 return store.addDevice(networkId, deviceId);
170 }
171
172 @Override
173 public void removeVirtualDevice(NetworkId networkId, DeviceId deviceId) {
174 checkNotNull(networkId, NETWORK_NULL);
175 checkNotNull(deviceId, DEVICE_NULL);
176 store.removeDevice(networkId, deviceId);
177 }
178
179 @Override
Brian Stanke7a81b532016-06-14 15:43:51 -0400180 public VirtualHost createVirtualHost(NetworkId networkId, HostId hostId, MacAddress mac,
181 VlanId vlan, HostLocation location, Set<IpAddress> ips) {
182 checkNotNull(networkId, NETWORK_NULL);
183 checkNotNull(hostId, DEVICE_NULL);
184 return store.addHost(networkId, hostId, mac, vlan, location, ips);
185 }
186
187 @Override
188 public void removeVirtualHost(NetworkId networkId, HostId hostId) {
189 checkNotNull(networkId, NETWORK_NULL);
190 checkNotNull(hostId, DEVICE_NULL);
191 store.removeHost(networkId, hostId);
192 }
193
194 @Override
Thomas Vachuska33979fd2015-07-31 11:41:14 -0700195 public VirtualLink createVirtualLink(NetworkId networkId,
Brian Stanke9a108972016-04-11 15:25:17 -0400196 ConnectPoint src, ConnectPoint dst) {
Thomas Vachuska33979fd2015-07-31 11:41:14 -0700197 checkNotNull(networkId, NETWORK_NULL);
198 checkNotNull(src, LINK_POINT_NULL);
199 checkNotNull(dst, LINK_POINT_NULL);
Brian Stanke11f6d532016-07-05 16:17:59 -0400200 return store.addLink(networkId, src, dst, Link.State.ACTIVE, null);
Thomas Vachuska33979fd2015-07-31 11:41:14 -0700201 }
202
Brian Stanke612cebf2016-05-02 10:21:33 -0400203 /**
204 * Maps the physical connect point to a virtual connect point.
205 *
Brian Stanke11f6d532016-07-05 16:17:59 -0400206 * @param networkId network identifier
Brian Stanke612cebf2016-05-02 10:21:33 -0400207 * @param physicalCp physical connect point
208 * @return virtual connect point
209 */
210 private ConnectPoint mapPhysicalToVirtualToPort(NetworkId networkId,
Brian Stanke11f6d532016-07-05 16:17:59 -0400211 ConnectPoint physicalCp) {
Brian Stanke612cebf2016-05-02 10:21:33 -0400212 Set<VirtualPort> ports = store.getPorts(networkId, null);
213 for (VirtualPort port : ports) {
214 if (port.realizedBy().element().id().equals(physicalCp.elementId()) &&
215 port.realizedBy().number().equals(physicalCp.port())) {
216 return new ConnectPoint(port.element().id(), port.number());
217 }
218 }
219 return null;
220 }
221
Thomas Vachuska33979fd2015-07-31 11:41:14 -0700222 @Override
223 public void removeVirtualLink(NetworkId networkId, ConnectPoint src, ConnectPoint dst) {
224 checkNotNull(networkId, NETWORK_NULL);
225 checkNotNull(src, LINK_POINT_NULL);
226 checkNotNull(dst, LINK_POINT_NULL);
Brian Stanke11f6d532016-07-05 16:17:59 -0400227 store.removeLink(networkId, src, dst);
Thomas Vachuska33979fd2015-07-31 11:41:14 -0700228 }
229
230 @Override
231 public VirtualPort createVirtualPort(NetworkId networkId, DeviceId deviceId,
232 PortNumber portNumber, Port realizedBy) {
233 checkNotNull(networkId, NETWORK_NULL);
234 checkNotNull(deviceId, DEVICE_NULL);
235 checkNotNull(portNumber, "Port description cannot be null");
236 return store.addPort(networkId, deviceId, portNumber, realizedBy);
237 }
238
239 @Override
240 public void removeVirtualPort(NetworkId networkId, DeviceId deviceId, PortNumber portNumber) {
241 checkNotNull(networkId, NETWORK_NULL);
242 checkNotNull(deviceId, DEVICE_NULL);
243 checkNotNull(portNumber, "Port number cannot be null");
244 store.removePort(networkId, deviceId, portNumber);
245 }
246
247 @Override
248 public Set<VirtualNetwork> getVirtualNetworks(TenantId tenantId) {
249 checkNotNull(tenantId, TENANT_NULL);
250 return store.getNetworks(tenantId);
251 }
252
Brian Stanke11f6d532016-07-05 16:17:59 -0400253 /**
254 * Returns the virtual network matching the network identifier.
255 *
256 * @param networkId network identifier
257 * @return virtual network
258 */
Brian Stanke86914282016-05-25 15:36:50 -0400259 private VirtualNetwork getVirtualNetwork(NetworkId networkId) {
260 checkNotNull(networkId, NETWORK_NULL);
261 return store.getNetwork(networkId);
262 }
263
Thomas Vachuska33979fd2015-07-31 11:41:14 -0700264 @Override
265 public Set<VirtualDevice> getVirtualDevices(NetworkId networkId) {
266 checkNotNull(networkId, NETWORK_NULL);
267 return store.getDevices(networkId);
268 }
269
270 @Override
Brian Stanke7a81b532016-06-14 15:43:51 -0400271 public Set<VirtualHost> getVirtualHosts(NetworkId networkId) {
272 checkNotNull(networkId, NETWORK_NULL);
273 return store.getHosts(networkId);
274 }
275
276 @Override
Thomas Vachuska33979fd2015-07-31 11:41:14 -0700277 public Set<VirtualLink> getVirtualLinks(NetworkId networkId) {
278 checkNotNull(networkId, NETWORK_NULL);
279 return store.getLinks(networkId);
280 }
281
282 @Override
283 public Set<VirtualPort> getVirtualPorts(NetworkId networkId, DeviceId deviceId) {
284 checkNotNull(networkId, NETWORK_NULL);
Thomas Vachuska33979fd2015-07-31 11:41:14 -0700285 return store.getPorts(networkId, deviceId);
286 }
287
Brian Stanke86914282016-05-25 15:36:50 -0400288 private final Map<ServiceKey, VnetService> networkServices = Maps.newConcurrentMap();
289
Thomas Vachuska33979fd2015-07-31 11:41:14 -0700290 @Override
291 public <T> T get(NetworkId networkId, Class<T> serviceClass) {
292 checkNotNull(networkId, NETWORK_NULL);
Brian Stanke86914282016-05-25 15:36:50 -0400293 ServiceKey serviceKey = networkServiceKey(networkId, serviceClass);
294 VnetService service = lookup(serviceKey);
295 if (service == null) {
296 service = create(serviceKey);
297 }
298 return (T) service;
299 }
300
Brian Stanke11f6d532016-07-05 16:17:59 -0400301 /**
302 * Returns the Vnet service matching the service key.
303 *
304 * @param serviceKey service key
305 * @return vnet service
306 */
Brian Stanke86914282016-05-25 15:36:50 -0400307 private VnetService lookup(ServiceKey serviceKey) {
308 return networkServices.get(serviceKey);
309 }
310
Brian Stanke11f6d532016-07-05 16:17:59 -0400311 /**
312 * Creates a new service key using the specified network identifier and service class.
313 *
314 * @param networkId network identifier
315 * @param serviceClass service class
316 * @param <T> type of service
317 * @return service key
318 */
Brian Stanke86914282016-05-25 15:36:50 -0400319 private <T> ServiceKey networkServiceKey(NetworkId networkId, Class<T> serviceClass) {
320 return new ServiceKey(networkId, serviceClass);
321 }
322
323
Brian Stanke11f6d532016-07-05 16:17:59 -0400324 /**
325 * Create a new vnet service instance.
326 *
327 * @param serviceKey service key
328 * @return vnet service
329 */
Brian Stanke86914282016-05-25 15:36:50 -0400330 private VnetService create(ServiceKey serviceKey) {
331 VirtualNetwork network = getVirtualNetwork(serviceKey.networkId());
Brian Stanke11f6d532016-07-05 16:17:59 -0400332 checkNotNull(network, NETWORK_NULL);
Brian Stanke86914282016-05-25 15:36:50 -0400333 VnetService service;
334 if (serviceKey.serviceClass.equals(DeviceService.class)) {
335 service = new VirtualNetworkDeviceService(this, network);
336 } else if (serviceKey.serviceClass.equals(LinkService.class)) {
337 service = new VirtualNetworkLinkService(this, network);
Brian Stanke8e9f8d12016-06-08 14:48:33 -0400338 } else if (serviceKey.serviceClass.equals(TopologyService.class)) {
339 service = new VirtualNetworkTopologyService(this, network);
Brian Stanke11f6d532016-07-05 16:17:59 -0400340 } else if (serviceKey.serviceClass.equals(IntentService.class)) {
341 service = new VirtualNetworkIntentService(this, network, new DefaultServiceDirectory());
Claudine Chiu30a8a2a2016-07-14 17:09:04 -0400342 } else if (serviceKey.serviceClass.equals(HostService.class)) {
343 service = new VirtualNetworkHostService(this, network);
Brian Stanke86914282016-05-25 15:36:50 -0400344 } else {
345 return null;
346 }
347 networkServices.put(serviceKey, service);
348 return service;
349 }
350
Brian Stanke11f6d532016-07-05 16:17:59 -0400351 /**
352 * Service key class.
353 */
Brian Stanke86914282016-05-25 15:36:50 -0400354 private class ServiceKey {
355 final NetworkId networkId;
356 final Class serviceClass;
357
Brian Stanke11f6d532016-07-05 16:17:59 -0400358 /**
359 * Constructor for service key.
360 *
361 * @param networkId network identifier
362 * @param serviceClass service class
363 */
Brian Stanke86914282016-05-25 15:36:50 -0400364 public ServiceKey(NetworkId networkId, Class serviceClass) {
365 checkNotNull(networkId, NETWORK_NULL);
366 this.networkId = networkId;
367 this.serviceClass = serviceClass;
368 }
369
Brian Stanke11f6d532016-07-05 16:17:59 -0400370 /**
371 * Returns the network identifier.
372 *
373 * @return network identifier
374 */
Brian Stanke86914282016-05-25 15:36:50 -0400375 public NetworkId networkId() {
376 return networkId;
377 }
378
Brian Stanke11f6d532016-07-05 16:17:59 -0400379 /**
380 * Returns the service class.
381 *
382 * @return service class
383 */
Brian Stanke86914282016-05-25 15:36:50 -0400384 public Class serviceClass() {
385 return serviceClass;
386 }
Thomas Vachuska33979fd2015-07-31 11:41:14 -0700387 }
388
Brian Stanke11f6d532016-07-05 16:17:59 -0400389 /**
390 * Internal intent event listener.
391 */
392 private class InternalVirtualIntentListener implements IntentListener {
393 @Override
394 public void event(IntentEvent event) {
395
396 // Ignore intent events that are not relevant.
397 if (!isRelevant(event)) {
398 return;
399 }
400
401 VirtualNetworkIntent intent = (VirtualNetworkIntent) event.subject();
402
403 switch (event.type()) {
404 case INSTALL_REQ:
405 store.addOrUpdateIntent(intent, IntentState.INSTALL_REQ);
406 break;
407 case INSTALLED:
408 store.addOrUpdateIntent(intent, IntentState.INSTALLED);
409 break;
410 case WITHDRAW_REQ:
411 store.addOrUpdateIntent(intent, IntentState.WITHDRAW_REQ);
412 break;
413 case WITHDRAWN:
414 store.addOrUpdateIntent(intent, IntentState.WITHDRAWN);
415 break;
416 case FAILED:
417 store.addOrUpdateIntent(intent, IntentState.FAILED);
418 break;
419 case CORRUPT:
420 store.addOrUpdateIntent(intent, IntentState.CORRUPT);
421 break;
422 case PURGED:
423 store.removeIntent(intent.key());
424 default:
425 break;
426 }
427 }
428
429 @Override
430 public boolean isRelevant(IntentEvent event) {
431 if (event.subject() instanceof VirtualNetworkIntent) {
432 return true;
433 }
434 return false;
435 }
436 }
437
438
Thomas Vachuska3d62fd72015-09-25 14:58:13 -0700439 @Override
440 protected VirtualNetworkProviderService createProviderService(VirtualNetworkProvider provider) {
441 return new InternalVirtualNetworkProviderService(provider);
442 }
443
444 // Service issued to registered virtual network providers so that they
445 // can interact with the core.
446 private class InternalVirtualNetworkProviderService
447 extends AbstractProviderService<VirtualNetworkProvider>
448 implements VirtualNetworkProviderService {
449 InternalVirtualNetworkProviderService(VirtualNetworkProvider provider) {
450 super(provider);
451 }
Brian Stanke4d579882016-04-22 13:28:46 -0400452
453 @Override
Brian Stanke612cebf2016-05-02 10:21:33 -0400454 public void tunnelUp(NetworkId networkId, ConnectPoint src, ConnectPoint dst, TunnelId tunnelId) {
Brian Stanke4d579882016-04-22 13:28:46 -0400455
Brian Stanke612cebf2016-05-02 10:21:33 -0400456 ConnectPoint srcVirtualCp = mapPhysicalToVirtualToPort(networkId, src);
457 ConnectPoint dstVirtualCp = mapPhysicalToVirtualToPort(networkId, dst);
458 if ((srcVirtualCp == null) || (dstVirtualCp == null)) {
459 log.error("Src or dst virtual connection point was not found.");
460 }
461
462 VirtualLink virtualLink = store.getLink(networkId, srcVirtualCp, dstVirtualCp);
463 if (virtualLink != null) {
464 store.updateLink(virtualLink, tunnelId, Link.State.ACTIVE);
465 }
Brian Stanke4d579882016-04-22 13:28:46 -0400466 }
467
468 @Override
Brian Stanke612cebf2016-05-02 10:21:33 -0400469 public void tunnelDown(NetworkId networkId, ConnectPoint src, ConnectPoint dst, TunnelId tunnelId) {
470 ConnectPoint srcVirtualCp = mapPhysicalToVirtualToPort(networkId, src);
471 ConnectPoint dstVirtualCp = mapPhysicalToVirtualToPort(networkId, dst);
472 if ((srcVirtualCp == null) || (dstVirtualCp == null)) {
473 log.error("Src or dst virtual connection point was not found.");
474 }
Brian Stanke4d579882016-04-22 13:28:46 -0400475
Brian Stanke612cebf2016-05-02 10:21:33 -0400476 VirtualLink virtualLink = store.getLink(networkId, srcVirtualCp, dstVirtualCp);
477 if (virtualLink != null) {
478 store.updateLink(virtualLink, tunnelId, Link.State.INACTIVE);
479 }
Brian Stanke4d579882016-04-22 13:28:46 -0400480 }
Thomas Vachuska3d62fd72015-09-25 14:58:13 -0700481 }
482
Thomas Vachuska33979fd2015-07-31 11:41:14 -0700483}