blob: 465dd06f0ec9aa6e7da4369300cf3247390d4965 [file] [log] [blame]
Thomas Vachuska781d18b2014-10-27 10:31:25 -07001/*
Brian O'Connor5ab426f2016-04-09 01:19:45 -07002 * Copyright 2015-present Open Networking Laboratory
tom7ef8ff92014-09-17 13:08:06 -07003 *
Thomas Vachuska4f1a60c2014-10-28 13:39:07 -07004 * 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
tom7ef8ff92014-09-17 13:08:06 -07007 *
Thomas Vachuska4f1a60c2014-10-28 13:39:07 -07008 * 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.
Thomas Vachuska781d18b2014-10-27 10:31:25 -070015 */
tom7ef8ff92014-09-17 13:08:06 -070016
Brian O'Connorabafb502014-12-02 22:26:20 -080017package org.onosproject.openflow.controller.driver;
tom7ef8ff92014-09-17 13:08:06 -070018
Brian O'Connore755caa2015-11-16 16:43:09 -080019import com.google.common.collect.Lists;
tom7ef8ff92014-09-17 13:08:06 -070020import org.jboss.netty.channel.Channel;
Ray Milkeye53f1712015-01-16 09:17:16 -080021import org.onlab.packet.IpAddress;
Marc De Leenheerb9311372015-07-09 11:36:49 -070022import org.onosproject.net.Device;
alshabibb452fd72015-04-22 20:46:20 -070023import org.onosproject.net.driver.AbstractHandlerBehaviour;
Brian O'Connorabafb502014-12-02 22:26:20 -080024import org.onosproject.openflow.controller.Dpid;
25import org.onosproject.openflow.controller.RoleState;
tom7ef8ff92014-09-17 13:08:06 -070026import org.projectfloodlight.openflow.protocol.OFDescStatsReply;
Jian Li152b8852015-12-07 14:47:25 -080027import org.projectfloodlight.openflow.protocol.OFErrorMsg;
Marc De Leenheerc662d322016-02-18 16:05:10 -080028import org.projectfloodlight.openflow.protocol.OFExperimenter;
29import org.projectfloodlight.openflow.protocol.OFFactories;
Jian Li152b8852015-12-07 14:47:25 -080030import org.projectfloodlight.openflow.protocol.OFFactory;
Marc De Leenheerc662d322016-02-18 16:05:10 -080031import org.projectfloodlight.openflow.protocol.OFFeaturesReply;
32import org.projectfloodlight.openflow.protocol.OFMessage;
33import org.projectfloodlight.openflow.protocol.OFNiciraControllerRoleRequest;
34import org.projectfloodlight.openflow.protocol.OFPortDesc;
35import org.projectfloodlight.openflow.protocol.OFPortDescStatsReply;
36import org.projectfloodlight.openflow.protocol.OFPortStatus;
Jian Li152b8852015-12-07 14:47:25 -080037import org.projectfloodlight.openflow.protocol.OFRoleReply;
Marc De Leenheerc662d322016-02-18 16:05:10 -080038import org.projectfloodlight.openflow.protocol.OFRoleRequest;
Marc De Leenheerc662d322016-02-18 16:05:10 -080039import org.projectfloodlight.openflow.protocol.OFVersion;
tom7ef8ff92014-09-17 13:08:06 -070040import org.slf4j.Logger;
41import org.slf4j.LoggerFactory;
42
Thomas Vachuska1c681d72015-05-18 14:58:53 -070043import java.io.IOException;
44import java.net.InetSocketAddress;
45import java.net.SocketAddress;
46import java.util.ArrayList;
47import java.util.Collections;
48import java.util.List;
49import java.util.concurrent.atomic.AtomicInteger;
Brian O'Connore755caa2015-11-16 16:43:09 -080050import java.util.concurrent.atomic.AtomicReference;
Thomas Vachuska1c681d72015-05-18 14:58:53 -070051import java.util.stream.Collectors;
52
tom7ef8ff92014-09-17 13:08:06 -070053/**
54 * An abstract representation of an OpenFlow switch. Can be extended by others
55 * to serve as a base for their vendor specific representation of a switch.
56 */
alshabibb452fd72015-04-22 20:46:20 -070057public abstract class AbstractOpenFlowSwitch extends AbstractHandlerBehaviour
58 implements OpenFlowSwitchDriver {
tom7ef8ff92014-09-17 13:08:06 -070059
Yuta HIGUCHIf5416d82014-10-24 21:17:40 -070060 protected final Logger log = LoggerFactory.getLogger(getClass());
tom7ef8ff92014-09-17 13:08:06 -070061
alshabibb452fd72015-04-22 20:46:20 -070062 private Channel channel;
Ray Milkeye53f1712015-01-16 09:17:16 -080063 protected String channelId;
tom7ef8ff92014-09-17 13:08:06 -070064
65 private boolean connected;
66 protected boolean startDriverHandshakeCalled = false;
alshabibb452fd72015-04-22 20:46:20 -070067 private Dpid dpid;
tom7ef8ff92014-09-17 13:08:06 -070068 private OpenFlowAgent agent;
69 private final AtomicInteger xidCounter = new AtomicInteger(0);
70
71 private OFVersion ofVersion;
72
Srikanth Vavilapallif5b234a2015-04-21 13:04:13 -070073 protected List<OFPortDescStatsReply> ports = new ArrayList<>();
tom7ef8ff92014-09-17 13:08:06 -070074
75 protected boolean tableFull;
76
77 private RoleHandler roleMan;
78
Brian O'Connore755caa2015-11-16 16:43:09 -080079 // TODO this is accessed from multiple threads, but volatile may have performance implications
80 protected volatile RoleState role;
tom7ef8ff92014-09-17 13:08:06 -070081
82 protected OFFeaturesReply features;
83 protected OFDescStatsReply desc;
84
HIGUCHI Yuta1979f552015-12-28 21:24:26 -080085 // messagesPendingMastership is used as synchronization variable for
86 // all mastership related changes. In this block, mastership (including
87 // role update) will have either occurred or not.
Brian O'Connore755caa2015-11-16 16:43:09 -080088 private final AtomicReference<List<OFMessage>> messagesPendingMastership
89 = new AtomicReference<>();
Charles Chan5b7ec342015-10-18 20:55:41 -070090
alshabibb452fd72015-04-22 20:46:20 -070091 @Override
92 public void init(Dpid dpid, OFDescStatsReply desc, OFVersion ofv) {
tom7ef8ff92014-09-17 13:08:06 -070093 this.dpid = dpid;
94 this.desc = desc;
alshabibb452fd72015-04-22 20:46:20 -070095 this.ofVersion = ofv;
tom7ef8ff92014-09-17 13:08:06 -070096 }
97
98 //************************
99 // Channel related
100 //************************
101
102 @Override
103 public final void disconnectSwitch() {
Charles Chanecfdfb72015-11-24 19:05:50 -0800104 setConnected(false);
tom7ef8ff92014-09-17 13:08:06 -0700105 this.channel.close();
106 }
107
108 @Override
Charles Chan5b7ec342015-10-18 20:55:41 -0700109 public void sendMsg(OFMessage msg) {
110 this.sendMsg(Collections.singletonList(msg));
tom7ef8ff92014-09-17 13:08:06 -0700111 }
112
113 @Override
114 public final void sendMsg(List<OFMessage> msgs) {
Brian O'Connore755caa2015-11-16 16:43:09 -0800115 /*
116 It is possible that in this block, we transition to SLAVE/EQUAL.
117 If this is the case, the supplied messages will race with the
118 RoleRequest message, and they could be rejected by the switch.
119 In the interest of performance, we will not protect this block with
120 a synchronization primitive, because the message would have just been
121 dropped anyway.
122 */
Jian Lia78cdb22016-04-21 13:03:58 -0700123
Brian O'Connore755caa2015-11-16 16:43:09 -0800124 if (role == RoleState.MASTER) {
125 // fast path send when we are master
Brian O'Connore755caa2015-11-16 16:43:09 -0800126 sendMsgsOnChannel(msgs);
127 return;
128 }
129 // check to see if mastership transition is in progress
130 synchronized (messagesPendingMastership) {
131 /*
132 messagesPendingMastership is used as synchronization variable for
133 all mastership related changes. In this block, mastership (including
134 role update) will have either occurred or not.
135 */
136 if (role == RoleState.MASTER) {
137 // transition to MASTER complete, send messages
138 sendMsgsOnChannel(msgs);
139 return;
140 }
141
142 List<OFMessage> messages = messagesPendingMastership.get();
143 if (messages != null) {
144 // we are transitioning to MASTER, so add messages to queue
145 messages.addAll(msgs);
146 log.debug("Enqueue message for switch {}. queue size after is {}",
147 dpid, messages.size());
148 } else {
149 // not transitioning to MASTER
150 log.warn("Dropping message for switch {} (role: {}, connected: {}): {}",
151 dpid, role, channel.isConnected(), msgs);
152 }
153 }
Jian Li11111972016-04-01 23:49:00 -0700154 }
Jian Li152b8852015-12-07 14:47:25 -0800155
Brian O'Connore755caa2015-11-16 16:43:09 -0800156 private void sendMsgsOnChannel(List<OFMessage> msgs) {
157 if (channel.isConnected()) {
Thomas Vachuska1c681d72015-05-18 14:58:53 -0700158 channel.write(msgs);
Jian Lia78cdb22016-04-21 13:03:58 -0700159 agent.processDownstreamMessage(dpid, msgs);
Charles Chan5b7ec342015-10-18 20:55:41 -0700160 } else {
Brian O'Connore755caa2015-11-16 16:43:09 -0800161 log.warn("Dropping messages for switch {} because channel is not connected: {}",
162 dpid, msgs);
alshabib339a3d92014-09-26 17:54:32 -0700163 }
tom7ef8ff92014-09-17 13:08:06 -0700164 }
165
166 @Override
alshabibb452fd72015-04-22 20:46:20 -0700167 public final void sendRoleRequest(OFMessage msg) {
168 if (msg instanceof OFRoleRequest ||
169 msg instanceof OFNiciraControllerRoleRequest) {
Brian O'Connore755caa2015-11-16 16:43:09 -0800170 sendMsgsOnChannel(Collections.singletonList(msg));
alshabibb452fd72015-04-22 20:46:20 -0700171 return;
172 }
173 throw new IllegalArgumentException("Someone is trying to send " +
174 "a non role request message");
175 }
tom7ef8ff92014-09-17 13:08:06 -0700176
Ayaka Koshibe3c240772015-05-20 16:23:40 -0700177 @Override
Marc De Leenheerc662d322016-02-18 16:05:10 -0800178 public final void
179 sendHandshakeMessage(OFMessage message) {
alshabiba2df7b2a2015-05-06 13:57:10 -0700180 if (!this.isDriverHandshakeComplete()) {
Brian O'Connore755caa2015-11-16 16:43:09 -0800181 sendMsgsOnChannel(Collections.singletonList(message));
alshabiba2df7b2a2015-05-06 13:57:10 -0700182 }
183 }
184
tom7ef8ff92014-09-17 13:08:06 -0700185 @Override
186 public final boolean isConnected() {
187 return this.connected;
188 }
189
190 @Override
191 public final void setConnected(boolean connected) {
192 this.connected = connected;
Thomas Vachuska1c681d72015-05-18 14:58:53 -0700193 }
tom7ef8ff92014-09-17 13:08:06 -0700194
195 @Override
196 public final void setChannel(Channel channel) {
197 this.channel = channel;
Ray Milkeye53f1712015-01-16 09:17:16 -0800198 final SocketAddress address = channel.getRemoteAddress();
199 if (address instanceof InetSocketAddress) {
200 final InetSocketAddress inetAddress = (InetSocketAddress) address;
201 final IpAddress ipAddress = IpAddress.valueOf(inetAddress.getAddress());
Pavlin Radoslavov87dd9302015-03-10 13:53:24 -0700202 if (ipAddress.isIp4()) {
Ray Milkeye53f1712015-01-16 09:17:16 -0800203 channelId = ipAddress.toString() + ':' + inetAddress.getPort();
204 } else {
205 channelId = '[' + ipAddress.toString() + "]:" + inetAddress.getPort();
206 }
207 }
Thomas Vachuska1c681d72015-05-18 14:58:53 -0700208 }
tom7ef8ff92014-09-17 13:08:06 -0700209
Ray Milkeye53f1712015-01-16 09:17:16 -0800210 @Override
211 public String channelId() {
212 return channelId;
213 }
214
tom7ef8ff92014-09-17 13:08:06 -0700215 //************************
216 // Switch features related
217 //************************
218
219 @Override
220 public final long getId() {
221 return this.dpid.value();
Thomas Vachuska1c681d72015-05-18 14:58:53 -0700222 }
tom7ef8ff92014-09-17 13:08:06 -0700223
224 @Override
225 public final String getStringId() {
226 return this.dpid.toString();
227 }
228
229 @Override
230 public final void setOFVersion(OFVersion ofV) {
231 this.ofVersion = ofV;
232 }
233
234 @Override
235 public void setTableFull(boolean full) {
236 this.tableFull = full;
237 }
238
239 @Override
240 public void setFeaturesReply(OFFeaturesReply featuresReply) {
241 this.features = featuresReply;
242 }
243
244 @Override
245 public abstract Boolean supportNxRole();
246
247 //************************
248 // Message handling
249 //************************
250 /**
251 * Handle the message coming from the dataplane.
252 *
253 * @param m the actual message
254 */
255 @Override
256 public final void handleMessage(OFMessage m) {
Thomas Vachuska39274462014-12-02 13:23:50 -0800257 if (this.role == RoleState.MASTER || m instanceof OFPortStatus) {
alshabib339a3d92014-09-26 17:54:32 -0700258 this.agent.processMessage(dpid, m);
HIGUCHI Yuta1979f552015-12-28 21:24:26 -0800259 } else {
260 log.trace("Dropping received message {}, was not MASTER", m);
alshabib339a3d92014-09-26 17:54:32 -0700261 }
tom7ef8ff92014-09-17 13:08:06 -0700262 }
263
264 @Override
265 public RoleState getRole() {
266 return role;
Thomas Vachuska1c681d72015-05-18 14:58:53 -0700267 }
tom7ef8ff92014-09-17 13:08:06 -0700268
269 @Override
270 public final boolean connectSwitch() {
271 return this.agent.addConnectedSwitch(dpid, this);
272 }
273
274 @Override
275 public final boolean activateMasterSwitch() {
276 return this.agent.addActivatedMasterSwitch(dpid, this);
277 }
278
279 @Override
280 public final boolean activateEqualSwitch() {
281 return this.agent.addActivatedEqualSwitch(dpid, this);
282 }
283
284 @Override
285 public final void transitionToEqualSwitch() {
286 this.agent.transitionToEqualSwitch(dpid);
287 }
288
289 @Override
290 public final void transitionToMasterSwitch() {
291 this.agent.transitionToMasterSwitch(dpid);
Brian O'Connore755caa2015-11-16 16:43:09 -0800292 synchronized (messagesPendingMastership) {
293 List<OFMessage> messages = messagesPendingMastership.get();
294 if (messages != null) {
HIGUCHI Yuta1979f552015-12-28 21:24:26 -0800295 // Cannot use sendMsg here. It will only append to pending list.
296 sendMsgsOnChannel(messages);
Brian O'Connore755caa2015-11-16 16:43:09 -0800297 log.debug("Sending {} pending messages to switch {}",
298 messages.size(), dpid);
299 messagesPendingMastership.set(null);
300 }
301 // perform role transition after clearing messages queue
302 this.role = RoleState.MASTER;
Charles Chan5b7ec342015-10-18 20:55:41 -0700303 }
tom7ef8ff92014-09-17 13:08:06 -0700304 }
305
306 @Override
307 public final void removeConnectedSwitch() {
308 this.agent.removeConnectedSwitch(dpid);
309 }
310
311 @Override
312 public OFFactory factory() {
313 return OFFactories.getFactory(ofVersion);
314 }
315
316 @Override
317 public void setPortDescReply(OFPortDescStatsReply portDescReply) {
Srikanth Vavilapallif5b234a2015-04-21 13:04:13 -0700318 this.ports.add(portDescReply);
319 }
320
321 @Override
322 public void setPortDescReplies(List<OFPortDescStatsReply> portDescReplies) {
323 this.ports.addAll(portDescReplies);
tom7ef8ff92014-09-17 13:08:06 -0700324 }
325
326 @Override
Ayaka Koshibe3ef2b0d2014-10-31 13:58:27 -0700327 public void returnRoleReply(RoleState requested, RoleState response) {
328 this.agent.returnRoleReply(dpid, requested, response);
Ayaka Koshibeab91cc42014-09-25 10:20:52 -0700329 }
330
331 @Override
tom7ef8ff92014-09-17 13:08:06 -0700332 public abstract void startDriverHandshake();
333
334 @Override
335 public abstract boolean isDriverHandshakeComplete();
336
337 @Override
338 public abstract void processDriverHandshakeMessage(OFMessage m);
339
alshabib339a3d92014-09-26 17:54:32 -0700340
341 // Role Handling
342
tom7ef8ff92014-09-17 13:08:06 -0700343 @Override
344 public void setRole(RoleState role) {
345 try {
Brian O'Connore755caa2015-11-16 16:43:09 -0800346 if (role == RoleState.SLAVE || role == RoleState.EQUAL) {
347 // perform role transition to SLAVE/EQUAL before sending role request
348 this.role = role;
349 }
tom7ef8ff92014-09-17 13:08:06 -0700350 if (this.roleMan.sendRoleRequest(role, RoleRecvStatus.MATCHED_SET_ROLE)) {
Madan Jampanif2af7712015-05-29 18:43:52 -0700351 log.debug("Sending role {} to switch {}", role, getStringId());
Brian O'Connore755caa2015-11-16 16:43:09 -0800352 if (role == RoleState.MASTER) {
353 synchronized (messagesPendingMastership) {
354 if (messagesPendingMastership.get() == null) {
355 log.debug("Initializing new message queue for switch {}", dpid);
356 /*
357 The presence of messagesPendingMastership indicates that
358 a switch is currently transitioning to MASTER, but
359 is still awaiting role reply from switch.
360 */
361 messagesPendingMastership.set(Lists.newArrayList());
362 }
Charles Chan5b7ec342015-10-18 20:55:41 -0700363 }
alshabib339a3d92014-09-26 17:54:32 -0700364 }
Brian O'Connore755caa2015-11-16 16:43:09 -0800365 } else if (role == RoleState.MASTER) {
366 // role request not support; transition switch to MASTER
alshabib7814e9f2014-09-30 11:52:12 -0700367 this.role = role;
tom7ef8ff92014-09-17 13:08:06 -0700368 }
369 } catch (IOException e) {
370 log.error("Unable to write to switch {}.", this.dpid);
371 }
372 }
373
alshabib339a3d92014-09-26 17:54:32 -0700374 @Override
375 public void reassertRole() {
Brian O'Connore755caa2015-11-16 16:43:09 -0800376 // TODO should messages be sent directly or queue during reassertion?
alshabib339a3d92014-09-26 17:54:32 -0700377 if (this.getRole() == RoleState.MASTER) {
378 log.warn("Received permission error from switch {} while " +
379 "being master. Reasserting master role.",
380 this.getStringId());
381 this.setRole(RoleState.MASTER);
382 }
383 }
384
tom7ef8ff92014-09-17 13:08:06 -0700385 @Override
386 public void handleRole(OFMessage m) throws SwitchStateException {
387 RoleReplyInfo rri = roleMan.extractOFRoleReply((OFRoleReply) m);
388 RoleRecvStatus rrs = roleMan.deliverRoleReply(rri);
389 if (rrs == RoleRecvStatus.MATCHED_SET_ROLE) {
390 if (rri.getRole() == RoleState.MASTER) {
391 this.transitionToMasterSwitch();
392 } else if (rri.getRole() == RoleState.EQUAL ||
Brian O'Connore755caa2015-11-16 16:43:09 -0800393 rri.getRole() == RoleState.SLAVE) {
tom7ef8ff92014-09-17 13:08:06 -0700394 this.transitionToEqualSwitch();
395 }
alshabib339a3d92014-09-26 17:54:32 -0700396 } else {
alshabib4785eec2014-12-04 16:45:45 -0800397 log.warn("Failed to set role for {}", this.getStringId());
tom7ef8ff92014-09-17 13:08:06 -0700398 }
399 }
400
401 @Override
402 public void handleNiciraRole(OFMessage m) throws SwitchStateException {
403 RoleState r = this.roleMan.extractNiciraRoleReply((OFExperimenter) m);
404 if (r == null) {
405 // The message wasn't really a Nicira role reply. We just
406 // dispatch it to the OFMessage listeners in this case.
407 this.handleMessage(m);
alshabibdfc7afb2014-10-21 20:13:27 -0700408 return;
tom7ef8ff92014-09-17 13:08:06 -0700409 }
410
411 RoleRecvStatus rrs = this.roleMan.deliverRoleReply(
412 new RoleReplyInfo(r, null, m.getXid()));
413 if (rrs == RoleRecvStatus.MATCHED_SET_ROLE) {
414 if (r == RoleState.MASTER) {
415 this.transitionToMasterSwitch();
416 } else if (r == RoleState.EQUAL ||
Brian O'Connore755caa2015-11-16 16:43:09 -0800417 r == RoleState.SLAVE) {
tom7ef8ff92014-09-17 13:08:06 -0700418 this.transitionToEqualSwitch();
419 }
alshabib339a3d92014-09-26 17:54:32 -0700420 } else {
alshabibdfc7afb2014-10-21 20:13:27 -0700421 this.disconnectSwitch();
tom7ef8ff92014-09-17 13:08:06 -0700422 }
423 }
424
425 @Override
426 public boolean handleRoleError(OFErrorMsg error) {
427 try {
428 return RoleRecvStatus.OTHER_EXPECTATION != this.roleMan.deliverError(error);
429 } catch (SwitchStateException e) {
430 this.disconnectSwitch();
431 }
432 return true;
433 }
434
tom7ef8ff92014-09-17 13:08:06 -0700435 @Override
436 public final void setAgent(OpenFlowAgent ag) {
437 if (this.agent == null) {
438 this.agent = ag;
439 }
440 }
441
442 @Override
443 public final void setRoleHandler(RoleHandler roleHandler) {
444 if (this.roleMan == null) {
445 this.roleMan = roleHandler;
446 }
447 }
448
449 @Override
450 public void setSwitchDescription(OFDescStatsReply d) {
451 this.desc = d;
452 }
453
454 @Override
455 public int getNextTransactionId() {
456 return this.xidCounter.getAndIncrement();
457 }
458
459 @Override
460 public List<OFPortDesc> getPorts() {
Srikanth Vavilapallif5b234a2015-04-21 13:04:13 -0700461 return this.ports.stream()
Brian O'Connore755caa2015-11-16 16:43:09 -0800462 .flatMap(portReply -> portReply.getEntries().stream())
Srikanth Vavilapallif5b234a2015-04-21 13:04:13 -0700463 .collect(Collectors.toList());
tom7ef8ff92014-09-17 13:08:06 -0700464 }
465
466 @Override
Ray Milkeyd3edd032015-01-16 11:38:58 -0800467 public String manufacturerDescription() {
tom7ef8ff92014-09-17 13:08:06 -0700468 return this.desc.getMfrDesc();
469 }
470
tom7ef8ff92014-09-17 13:08:06 -0700471 @Override
472 public String datapathDescription() {
473 return this.desc.getDpDesc();
474 }
475
tom7ef8ff92014-09-17 13:08:06 -0700476 @Override
477 public String hardwareDescription() {
478 return this.desc.getHwDesc();
479 }
480
481 @Override
482 public String softwareDescription() {
483 return this.desc.getSwDesc();
484 }
485
486 @Override
487 public String serialNumber() {
488 return this.desc.getSerialNum();
489 }
490
Praseed Balakrishnana22eadf2014-10-20 14:21:45 -0700491 @Override
Marc De Leenheerb9311372015-07-09 11:36:49 -0700492 public Device.Type deviceType() {
493 return Device.Type.SWITCH;
Praseed Balakrishnana22eadf2014-10-20 14:21:45 -0700494 }
495
alshabibb452fd72015-04-22 20:46:20 -0700496 @Override
497 public String toString() {
498 return this.getClass().getName() + " [" + ((channel != null)
499 ? channel.getRemoteAddress() : "?")
500 + " DPID[" + ((getStringId() != null) ? getStringId() : "?") + "]]";
501 }
tom7ef8ff92014-09-17 13:08:06 -0700502}