blob: 8fae8962a4653a884efd5efc4c4a2f662d53fa9c [file] [log] [blame]
Pingping Linffa27d32015-04-30 14:41:03 -07001/*
2 * Copyright 2015 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 */
16package org.onosproject.virtualbng;
17
18import static com.google.common.base.Preconditions.checkNotNull;
19
Pingping Lin53ae34f2015-06-09 10:07:08 -070020import com.fasterxml.jackson.databind.JsonNode;
21import com.fasterxml.jackson.databind.node.ArrayNode;
22import com.fasterxml.jackson.databind.node.ObjectNode;
Pingping Lindead2052015-06-08 16:07:23 -070023import com.google.common.collect.Maps;
Pingping Lin4e0c73d2015-05-06 15:41:10 -070024
Pingping Lin53ae34f2015-06-09 10:07:08 -070025import java.util.Iterator;
Pingping Linffa27d32015-04-30 14:41:03 -070026import java.util.Map;
Pingping Lindead2052015-06-08 16:07:23 -070027import java.util.Map.Entry;
Pingping Linffa27d32015-04-30 14:41:03 -070028import java.util.concurrent.ConcurrentHashMap;
29
30import org.apache.felix.scr.annotations.Activate;
31import org.apache.felix.scr.annotations.Component;
32import org.apache.felix.scr.annotations.Deactivate;
33import org.apache.felix.scr.annotations.Reference;
34import org.apache.felix.scr.annotations.ReferenceCardinality;
35import org.apache.felix.scr.annotations.Service;
36import org.onlab.packet.Ethernet;
37import org.onlab.packet.IpAddress;
38import org.onlab.packet.IpPrefix;
39import org.onlab.packet.MacAddress;
40import org.onosproject.core.ApplicationId;
41import org.onosproject.core.CoreService;
42import org.onosproject.net.ConnectPoint;
Pingping Lindead2052015-06-08 16:07:23 -070043import org.onosproject.net.DeviceId;
Pingping Linffa27d32015-04-30 14:41:03 -070044import org.onosproject.net.Host;
Pingping Lindead2052015-06-08 16:07:23 -070045import org.onosproject.net.PortNumber;
Pingping Linffa27d32015-04-30 14:41:03 -070046import org.onosproject.net.flow.DefaultTrafficSelector;
47import org.onosproject.net.flow.DefaultTrafficTreatment;
48import org.onosproject.net.flow.TrafficSelector;
49import org.onosproject.net.flow.TrafficTreatment;
Pingping Lin4e0c73d2015-05-06 15:41:10 -070050import org.onosproject.net.host.HostEvent;
51import org.onosproject.net.host.HostListener;
Pingping Linffa27d32015-04-30 14:41:03 -070052import org.onosproject.net.host.HostService;
53import org.onosproject.net.intent.IntentService;
54import org.onosproject.net.intent.Key;
55import org.onosproject.net.intent.PointToPointIntent;
56import org.slf4j.Logger;
57import org.slf4j.LoggerFactory;
58
59/**
60 * This is a virtual Broadband Network Gateway (BNG) application. It mainly
61 * has 3 functions:
62 * (1) assigns and replies a public IP address to a REST request with a private
63 * IP address
64 * (2) maintains the mapping from the private IP address to the public IP address
65 * (3) installs point to point intents for the host configured with private IP
66 * address to access Internet
67 */
68@Component(immediate = true)
69@Service
70public class VbngManager implements VbngService {
71
72 private static final String APP_NAME = "org.onosproject.virtualbng";
Pingping Lin53ae34f2015-06-09 10:07:08 -070073 private static final String VBNG_MAP_NAME = "vbng_mapping";
Pingping Linffa27d32015-04-30 14:41:03 -070074
75 private final Logger log = LoggerFactory.getLogger(getClass());
76
77 @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
78 protected CoreService coreService;
79
80 @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
81 protected HostService hostService;
82
83 @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
84 protected IntentService intentService;
85
86 @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
87 protected VbngConfigurationService vbngConfigurationService;
88
89 private ApplicationId appId;
90 private Map<IpAddress, PointToPointIntent> p2pIntentsFromHost;
91 private Map<IpAddress, PointToPointIntent> p2pIntentsToHost;
92
Pingping Lindead2052015-06-08 16:07:23 -070093 // This map stores the mapping from the private IP addresses to VcpeHost.
94 // The IP addresses in this map are all the private IP addresses we failed
95 // to create vBNGs due to the next hop host was not in ONOS.
96 private Map<IpAddress, VcpeHost> privateIpAddressMap;
97
98 // Store the mapping from hostname to connect point
99 private Map<String, ConnectPoint> nodeToPort;
Pingping Lin4e0c73d2015-05-06 15:41:10 -0700100
101 private HostListener hostListener;
102 private IpAddress nextHopIpAddress;
103
Pingping Lindead2052015-06-08 16:07:23 -0700104 private static final DeviceId FABRIC_DEVICE_ID =
105 DeviceId.deviceId("of:8f0e486e73000187");
106
Pingping Linffa27d32015-04-30 14:41:03 -0700107 @Activate
108 public void activate() {
109 appId = coreService.registerApplication(APP_NAME);
110 p2pIntentsFromHost = new ConcurrentHashMap<>();
111 p2pIntentsToHost = new ConcurrentHashMap<>();
Pingping Lindead2052015-06-08 16:07:23 -0700112 privateIpAddressMap = new ConcurrentHashMap<>();
113
114 setupMap();
Pingping Lin4e0c73d2015-05-06 15:41:10 -0700115
116 nextHopIpAddress = vbngConfigurationService.getNextHopIpAddress();
117 hostListener = new InternalHostListener();
118 hostService.addListener(hostListener);
119
Pingping Linffa27d32015-04-30 14:41:03 -0700120 log.info("vBNG Started");
Pingping Lin53ae34f2015-06-09 10:07:08 -0700121
122 // Recover the status before vBNG restarts
123 statusRecovery();
Pingping Linffa27d32015-04-30 14:41:03 -0700124 }
125
126 @Deactivate
127 public void deactivate() {
Pingping Lin4e0c73d2015-05-06 15:41:10 -0700128 hostService.removeListener(hostListener);
Pingping Linffa27d32015-04-30 14:41:03 -0700129 log.info("vBNG Stopped");
130 }
131
Pingping Lindead2052015-06-08 16:07:23 -0700132 /**
Pingping Lin53ae34f2015-06-09 10:07:08 -0700133 * Recovers from XOS record. Re-sets up the mapping between private IP
134 * address and public IP address, re-calculates intents and re-installs
135 * those intents.
136 */
137 private void statusRecovery() {
138 log.info("vBNG starts to recover from XOS record......");
139 RestClient restClient = new RestClient();
140 ObjectNode map = restClient.getRest();
141 if (map == null) {
142 log.info("Stop to recover vBNG status due to the vBNG map "
143 + "is null!");
144 return;
145 }
146
147 log.info("Get record from XOS: {}", map);
148
149 ArrayNode array = (ArrayNode) map.get(VBNG_MAP_NAME);
150 Iterator<JsonNode> entries = array.elements();
151 while (entries.hasNext()) {
152 ObjectNode entry = (ObjectNode) entries.next();
153
154 IpAddress hostIpAdddress =
155 IpAddress.valueOf(entry.get("private_ip").asText());
156 IpAddress publicIpAddress =
157 IpAddress.valueOf(entry.get("routeable_subnet").asText());
158 MacAddress macAddress =
159 MacAddress.valueOf(entry.get("mac").asText());
160 String hostName = entry.get("hostname").asText();
161
162 // Create vBNG
163 createVbng(hostIpAdddress, publicIpAddress, macAddress, hostName);
164
165 }
166 }
167
168 /**
Pingping Lindead2052015-06-08 16:07:23 -0700169 * Sets up mapping from hostname to connect point.
170 */
171 private void setupMap() {
172 nodeToPort = Maps.newHashMap();
173
174 nodeToPort.put("cordcompute01.onlab.us",
175 new ConnectPoint(FABRIC_DEVICE_ID,
176 PortNumber.portNumber(48)));
177
178 nodeToPort.put("cordcompute02.onlab.us",
179 new ConnectPoint(FABRIC_DEVICE_ID,
180 PortNumber.portNumber(47)));
181 }
182
Pingping Lin53ae34f2015-06-09 10:07:08 -0700183 /**
184 * Creates a new vBNG.
185 *
186 * @param privateIpAddress a private IP address
187 * @param publicIpAddress the public IP address for the private IP address
188 * @param hostMacAddress the MAC address for the private IP address
189 * @param hostName the host name for the private IP address
190 */
191 private void createVbng(IpAddress privateIpAddress,
192 IpAddress publicIpAddress,
193 MacAddress hostMacAddress,
194 String hostName) {
195 boolean result = vbngConfigurationService
196 .assignSpecifiedPublicIp(publicIpAddress, privateIpAddress);
197 if (!result) {
198 log.info("Assign public IP address {} for private IP address {} "
199 + "failed!", publicIpAddress, privateIpAddress);
200 log.info("Failed to create vBNG for private IP address {}",
201 privateIpAddress);
202 return;
203 }
204 log.info("[ADD] Private IP to Public IP mapping: {} --> {}",
205 privateIpAddress, publicIpAddress);
206
207 // Setup paths between the host configured with private IP and
208 // next hop
209 if (!setupForwardingPaths(privateIpAddress, publicIpAddress,
210 hostMacAddress, hostName)) {
211 privateIpAddressMap.put(privateIpAddress,
212 new VcpeHost(hostMacAddress, hostName));
213 }
214 }
215
Pingping Linffa27d32015-04-30 14:41:03 -0700216 @Override
Pingping Lindead2052015-06-08 16:07:23 -0700217 public IpAddress createVbng(IpAddress privateIpAddress,
218 MacAddress hostMacAddress,
219 String hostName) {
Pingping Linffa27d32015-04-30 14:41:03 -0700220
Pingping Linffa27d32015-04-30 14:41:03 -0700221 IpAddress publicIpAddress =
222 vbngConfigurationService.getAvailablePublicIpAddress(
223 privateIpAddress);
224 if (publicIpAddress == null) {
225 log.info("Did not find an available public IP address to use.");
226 return null;
227 }
Pingping Linde77ee52015-06-03 17:16:07 -0700228 log.info("[ADD] Private IP to Public IP mapping: {} --> {}",
Pingping Linffa27d32015-04-30 14:41:03 -0700229 privateIpAddress, publicIpAddress);
230
231 // Setup paths between the host configured with private IP and
232 // next hop
Pingping Lindead2052015-06-08 16:07:23 -0700233 if (!setupForwardingPaths(privateIpAddress, publicIpAddress,
234 hostMacAddress, hostName)) {
235 privateIpAddressMap.put(privateIpAddress,
236 new VcpeHost(hostMacAddress, hostName));
Pingping Lin4e0c73d2015-05-06 15:41:10 -0700237 }
Pingping Linffa27d32015-04-30 14:41:03 -0700238 return publicIpAddress;
239 }
240
Pingping Lind2afaf22015-06-02 10:46:29 -0700241 @Override
242 public IpAddress deleteVbng(IpAddress privateIpAddress) {
243 // Recycle the public IP address assigned to this private IP address.
244 // Recycling will also delete the mapping entry from the private IP
245 // address to public IP address.
246 IpAddress assignedPublicIpAddress = vbngConfigurationService
247 .recycleAssignedPublicIpAddress(privateIpAddress);
248 if (assignedPublicIpAddress == null) {
249 return null;
250 }
251
Pingping Lindead2052015-06-08 16:07:23 -0700252 // Remove the private IP address from privateIpAddressMap
253 privateIpAddressMap.remove(privateIpAddress);
Pingping Lind2afaf22015-06-02 10:46:29 -0700254
255 // Remove intents
256 removeForwardingPaths(privateIpAddress);
257
258 return assignedPublicIpAddress;
259 }
260
261 /**
262 * Removes the forwarding paths in both two directions between host
263 * configured with private IP and next hop.
264 *
265 * @param privateIp the private IP address of a local host
266 */
267 private void removeForwardingPaths(IpAddress privateIp) {
268 PointToPointIntent toNextHopIntent =
269 p2pIntentsFromHost.remove(privateIp);
270 if (toNextHopIntent != null) {
271 intentService.withdraw(toNextHopIntent);
272 //intentService.purge(toNextHopIntent);
273 }
274 PointToPointIntent toLocalHostIntent =
275 p2pIntentsToHost.remove(privateIp);
276 if (toLocalHostIntent != null) {
277 intentService.withdraw(toLocalHostIntent);
278 //intentService.purge(toLocalHostIntent);
279 }
280 }
281
Pingping Linffa27d32015-04-30 14:41:03 -0700282 /**
283 * Sets up forwarding paths in both two directions between host configured
284 * with private IP and next hop.
285 *
286 * @param privateIp the private IP address of a local host
287 * @param publicIp the public IP address assigned for the private IP address
Pingping Lindead2052015-06-08 16:07:23 -0700288 * @param hostMacAddress the MAC address for the IP address
289 * @param hostName the host name for the IP address
Pingping Linffa27d32015-04-30 14:41:03 -0700290 */
Pingping Lindead2052015-06-08 16:07:23 -0700291 private boolean setupForwardingPaths(IpAddress privateIp,
292 IpAddress publicIp,
293 MacAddress hostMacAddress,
294 String hostName) {
Pingping Linffa27d32015-04-30 14:41:03 -0700295 checkNotNull(privateIp);
296 checkNotNull(publicIp);
Pingping Lindead2052015-06-08 16:07:23 -0700297 checkNotNull(hostMacAddress);
298 checkNotNull(hostName);
Pingping Lin4e0c73d2015-05-06 15:41:10 -0700299
300 if (nextHopIpAddress == null) {
301 log.warn("Did not find next hop IP address");
302 return false;
303 }
Pingping Linffa27d32015-04-30 14:41:03 -0700304
305 // If there are already intents for private IP address in the system,
306 // we will do nothing and directly return.
307 if (p2pIntentsFromHost.containsKey(privateIp)
308 && p2pIntentsToHost.containsKey(privateIp)) {
Pingping Lin4e0c73d2015-05-06 15:41:10 -0700309 return true;
Pingping Linffa27d32015-04-30 14:41:03 -0700310 }
311
Pingping Linffa27d32015-04-30 14:41:03 -0700312 Host nextHopHost = null;
313 if (!hostService.getHostsByIp(nextHopIpAddress).isEmpty()) {
314 nextHopHost = hostService.getHostsByIp(nextHopIpAddress)
315 .iterator().next();
316 } else {
Pingping Linffa27d32015-04-30 14:41:03 -0700317 hostService.startMonitoringIp(nextHopIpAddress);
318 if (hostService.getHostsByIp(privateIp).isEmpty()) {
319 hostService.startMonitoringIp(privateIp);
320 }
Pingping Lin4e0c73d2015-05-06 15:41:10 -0700321 return false;
Pingping Linffa27d32015-04-30 14:41:03 -0700322 }
323
Pingping Linffa27d32015-04-30 14:41:03 -0700324 ConnectPoint nextHopConnectPoint =
325 new ConnectPoint(nextHopHost.location().elementId(),
326 nextHopHost.location().port());
Pingping Lindead2052015-06-08 16:07:23 -0700327 ConnectPoint localHostConnectPoint = nodeToPort.get(hostName);
Pingping Linffa27d32015-04-30 14:41:03 -0700328
329 // Generate and install intent for traffic from host configured with
330 // private IP
331 if (!p2pIntentsFromHost.containsKey(privateIp)) {
332 PointToPointIntent toNextHopIntent
333 = srcMatchIntentGenerator(privateIp,
334 publicIp,
335 nextHopHost.mac(),
336 nextHopConnectPoint,
337 localHostConnectPoint
338 );
339 p2pIntentsFromHost.put(privateIp, toNextHopIntent);
340 intentService.submit(toNextHopIntent);
341 }
342
343 // Generate and install intent for traffic to host configured with
344 // private IP
345 if (!p2pIntentsToHost.containsKey(privateIp)) {
346 PointToPointIntent toLocalHostIntent
347 = dstMatchIntentGenerator(publicIp,
348 privateIp,
Pingping Lindead2052015-06-08 16:07:23 -0700349 hostMacAddress,
Pingping Linffa27d32015-04-30 14:41:03 -0700350 localHostConnectPoint,
351 nextHopConnectPoint);
Pingping Lind2afaf22015-06-02 10:46:29 -0700352 p2pIntentsToHost.put(privateIp, toLocalHostIntent);
Pingping Linffa27d32015-04-30 14:41:03 -0700353 intentService.submit(toLocalHostIntent);
354 }
355
Pingping Lin4e0c73d2015-05-06 15:41:10 -0700356 return true;
357 }
358
359 /**
360 * Listener for host events.
361 */
362 private class InternalHostListener implements HostListener {
363 @Override
364 public void event(HostEvent event) {
365 log.debug("Received HostEvent {}", event);
366
367 Host host = event.subject();
368 if (event.type() != HostEvent.Type.HOST_ADDED) {
369 return;
370 }
371
372 for (IpAddress ipAddress: host.ipAddresses()) {
Pingping Lindead2052015-06-08 16:07:23 -0700373 // The POST method from XOS gives us MAC and host name, so we
374 // do not need to do anything after receive a vCPE host event
375 // for now.
376 /*if (privateIpAddressSet.contains(ipAddress)) {
Pingping Lin4e0c73d2015-05-06 15:41:10 -0700377 createVbngAgain(ipAddress);
Pingping Lindead2052015-06-08 16:07:23 -0700378 }*/
Pingping Lin4e0c73d2015-05-06 15:41:10 -0700379
380 if (nextHopIpAddress != null &&
381 ipAddress.equals(nextHopIpAddress)) {
Pingping Lindead2052015-06-08 16:07:23 -0700382
383 for (Entry<IpAddress, VcpeHost> entry:
384 privateIpAddressMap.entrySet()) {
385 createVbngAgain(entry.getKey());
Pingping Lin4e0c73d2015-05-06 15:41:10 -0700386 }
Pingping Lindead2052015-06-08 16:07:23 -0700387
Pingping Lin4e0c73d2015-05-06 15:41:10 -0700388 }
389 }
390 }
391 }
392
393 /**
394 * Tries to create vBNG again after receiving a host event if the IP
Pingping Lindead2052015-06-08 16:07:23 -0700395 * address of the host is the next hop IP address.
Pingping Lin4e0c73d2015-05-06 15:41:10 -0700396 *
397 * @param privateIpAddress the private IP address
398 */
399 private void createVbngAgain(IpAddress privateIpAddress) {
400 IpAddress publicIpAddress = vbngConfigurationService
401 .getAssignedPublicIpAddress(privateIpAddress);
402 if (publicIpAddress == null) {
403 // We only need to handle the private IP addresses for which we
404 // already returned the REST replies with assigned public IP
405 // addresses. If a private IP addresses does not have an assigned
406 // public IP address, we should not get it an available public IP
407 // address here, and we should delete it in the unhandled private
Pingping Lindead2052015-06-08 16:07:23 -0700408 // IP address map.
409 privateIpAddressMap.remove(privateIpAddress);
Pingping Lin4e0c73d2015-05-06 15:41:10 -0700410 return;
411 }
Pingping Lindead2052015-06-08 16:07:23 -0700412 VcpeHost vcpeHost = privateIpAddressMap.get(privateIpAddress);
413 if (setupForwardingPaths(privateIpAddress, publicIpAddress,
414 vcpeHost.macAddress, vcpeHost.hostName)) {
415 privateIpAddressMap.remove(privateIpAddress);
Pingping Lin4e0c73d2015-05-06 15:41:10 -0700416 }
Pingping Linffa27d32015-04-30 14:41:03 -0700417 }
418
419 /**
420 * PointToPointIntent Generator.
421 * <p>
422 * The intent will match the source IP address in packet, rewrite the
423 * source IP address, and rewrite the destination MAC address.
424 * </p>
425 *
426 * @param srcIpAddress the source IP address in packet to match
427 * @param newSrcIpAddress the new source IP address to set
428 * @param dstMacAddress the destination MAC address to set
429 * @param dstConnectPoint the egress point
430 * @param srcConnectPoint the ingress point
431 * @return a PointToPointIntent
432 */
433 private PointToPointIntent srcMatchIntentGenerator(
434 IpAddress srcIpAddress,
435 IpAddress newSrcIpAddress,
436 MacAddress dstMacAddress,
437 ConnectPoint dstConnectPoint,
438 ConnectPoint srcConnectPoint) {
439 checkNotNull(srcIpAddress);
440 checkNotNull(newSrcIpAddress);
441 checkNotNull(dstMacAddress);
442 checkNotNull(dstConnectPoint);
443 checkNotNull(srcConnectPoint);
444
445 TrafficSelector.Builder selector = DefaultTrafficSelector.builder();
446 selector.matchEthType(Ethernet.TYPE_IPV4);
447 selector.matchIPSrc(IpPrefix.valueOf(srcIpAddress,
448 IpPrefix.MAX_INET_MASK_LENGTH));
449
450 TrafficTreatment.Builder treatment = DefaultTrafficTreatment.builder();
451 treatment.setEthDst(dstMacAddress);
452 treatment.setIpSrc(newSrcIpAddress);
453
454 Key key = Key.of(srcIpAddress.toString() + "MatchSrc", appId);
455 PointToPointIntent intent = PointToPointIntent.builder()
456 .appId(appId)
457 .key(key)
458 .selector(selector.build())
459 .treatment(treatment.build())
460 .egressPoint(dstConnectPoint)
461 .ingressPoint(srcConnectPoint)
462 .build();
463
464 log.info("Generated a PointToPointIntent for traffic from local host "
465 + ": {}", intent);
466 return intent;
467 }
468
469 /**
470 * PointToPointIntent Generator.
471 * <p>
472 * The intent will match the destination IP address in packet, rewrite the
473 * destination IP address, and rewrite the destination MAC address.
474 * </p>
475 *
476 * @param dstIpAddress the destination IP address in packet to match
477 * @param newDstIpAddress the new destination IP address to set
478 * @param dstMacAddress the destination MAC address to set
479 * @param dstConnectPoint the egress point
480 * @param srcConnectPoint the ingress point
481 * @return a PointToPointIntent
482 */
483 private PointToPointIntent dstMatchIntentGenerator(
484 IpAddress dstIpAddress,
485 IpAddress newDstIpAddress,
486 MacAddress dstMacAddress,
487 ConnectPoint dstConnectPoint,
488 ConnectPoint srcConnectPoint) {
489 checkNotNull(dstIpAddress);
490 checkNotNull(newDstIpAddress);
491 checkNotNull(dstMacAddress);
492 checkNotNull(dstConnectPoint);
493 checkNotNull(srcConnectPoint);
494
495 TrafficSelector.Builder selector = DefaultTrafficSelector.builder();
496 selector.matchEthType(Ethernet.TYPE_IPV4);
497 selector.matchIPDst(IpPrefix.valueOf(dstIpAddress,
498 IpPrefix.MAX_INET_MASK_LENGTH));
499
500 TrafficTreatment.Builder treatment = DefaultTrafficTreatment.builder();
501 treatment.setEthDst(dstMacAddress);
502 treatment.setIpDst(newDstIpAddress);
503
504 Key key = Key.of(newDstIpAddress.toString() + "MatchDst", appId);
505 PointToPointIntent intent = PointToPointIntent.builder()
506 .appId(appId)
507 .key(key)
508 .selector(selector.build())
509 .treatment(treatment.build())
510 .egressPoint(dstConnectPoint)
511 .ingressPoint(srcConnectPoint)
512 .build();
513 log.info("Generated a PointToPointIntent for traffic to local host "
514 + ": {}", intent);
515
516 return intent;
517 }
Pingping Lindead2052015-06-08 16:07:23 -0700518
519 /**
520 * Constructor to store the a vCPE host info.
521 */
522 private class VcpeHost {
523 MacAddress macAddress;
524 String hostName;
525 public VcpeHost(MacAddress macAddress, String hostName) {
526 this.macAddress = macAddress;
527 this.hostName = hostName;
528 }
529 }
Pingping Linffa27d32015-04-30 14:41:03 -0700530}