blob: b51785d58cfa6e114a37ff9e04be4aab39c54268 [file] [log] [blame]
alshabib3b1eadc2016-02-01 17:57:00 -08001/*
Brian O'Connorcf85aa82017-08-03 22:46:01 -07002 * Copyright 2016-present Open Networking Foundation
alshabib3b1eadc2016-02-01 17:57:00 -08003 *
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 */
Daniele Moro8ea9e102020-03-24 18:56:52 +010016package org.opencord.cordmcast.impl;
alshabib3b1eadc2016-02-01 17:57:00 -080017
Esin Karaman39b24852019-08-28 13:57:30 +000018import com.google.common.collect.ImmutableSet;
19import com.google.common.collect.Sets;
alshabib09069c92016-02-21 14:49:51 -080020import org.apache.commons.lang3.tuple.ImmutablePair;
alshabib3b1eadc2016-02-01 17:57:00 -080021import org.onlab.packet.Ethernet;
alshabib3b1eadc2016-02-01 17:57:00 -080022import org.onlab.packet.IpAddress;
23import org.onlab.packet.VlanId;
Esin Karaman39b24852019-08-28 13:57:30 +000024import org.onlab.util.KryoNamespace;
Jonathan Hart28271642016-02-10 16:13:54 -080025import org.onosproject.cfg.ComponentConfigService;
Esin Karaman39b24852019-08-28 13:57:30 +000026import org.onosproject.cluster.ClusterService;
27import org.onosproject.cluster.LeadershipService;
28import org.onosproject.cluster.NodeId;
alshabib3b1eadc2016-02-01 17:57:00 -080029import org.onosproject.core.ApplicationId;
30import org.onosproject.core.CoreService;
Esin Karaman39b24852019-08-28 13:57:30 +000031import org.onosproject.mastership.MastershipService;
32import org.onosproject.mcast.api.McastEvent;
33import org.onosproject.mcast.api.McastListener;
34import org.onosproject.mcast.api.McastRoute;
35import org.onosproject.mcast.api.MulticastRouteService;
alshabib3b1eadc2016-02-01 17:57:00 -080036import org.onosproject.net.ConnectPoint;
Daniele Moro8ea9e102020-03-24 18:56:52 +010037import org.onosproject.net.Device;
ke han9590c812017-02-28 15:02:26 +080038import org.onosproject.net.DeviceId;
Esin Karaman39b24852019-08-28 13:57:30 +000039import org.onosproject.net.HostId;
ke han9590c812017-02-28 15:02:26 +080040import org.onosproject.net.PortNumber;
ke hanf1709e82016-08-12 10:48:17 +080041import org.onosproject.net.config.ConfigFactory;
42import org.onosproject.net.config.NetworkConfigEvent;
43import org.onosproject.net.config.NetworkConfigListener;
44import org.onosproject.net.config.NetworkConfigRegistry;
Esin Karaman39b24852019-08-28 13:57:30 +000045import org.onosproject.net.config.basics.McastConfig;
ke hanf1709e82016-08-12 10:48:17 +080046import org.onosproject.net.config.basics.SubjectFactories;
Andrea Campanellac2f3ddd2020-05-18 11:09:11 +020047import org.onosproject.net.device.DeviceEvent;
48import org.onosproject.net.device.DeviceListener;
Esin Karaman39b24852019-08-28 13:57:30 +000049import org.onosproject.net.device.DeviceService;
alshabib3b1eadc2016-02-01 17:57:00 -080050import org.onosproject.net.flow.DefaultTrafficSelector;
51import org.onosproject.net.flow.DefaultTrafficTreatment;
52import org.onosproject.net.flow.TrafficSelector;
53import org.onosproject.net.flowobjective.DefaultForwardingObjective;
54import org.onosproject.net.flowobjective.DefaultNextObjective;
Esin Karaman39b24852019-08-28 13:57:30 +000055import org.onosproject.net.flowobjective.DefaultObjectiveContext;
alshabib3b1eadc2016-02-01 17:57:00 -080056import org.onosproject.net.flowobjective.FlowObjectiveService;
57import org.onosproject.net.flowobjective.ForwardingObjective;
58import org.onosproject.net.flowobjective.NextObjective;
59import org.onosproject.net.flowobjective.Objective;
60import org.onosproject.net.flowobjective.ObjectiveContext;
61import org.onosproject.net.flowobjective.ObjectiveError;
Andrea Campanellac2f3ddd2020-05-18 11:09:11 +020062import org.onosproject.net.group.GroupService;
Esin Karaman39b24852019-08-28 13:57:30 +000063import org.onosproject.store.serializers.KryoNamespaces;
64import org.onosproject.store.service.ConsistentMap;
65import org.onosproject.store.service.Serializer;
66import org.onosproject.store.service.StorageService;
67import org.onosproject.store.service.Versioned;
Daniele Moro8ea9e102020-03-24 18:56:52 +010068import org.opencord.cordmcast.CordMcastService;
69import org.opencord.cordmcast.CordMcastStatisticsService;
70import org.opencord.sadis.SadisService;
71import org.opencord.sadis.SubscriberAndDeviceInformation;
Jonathan Hart28271642016-02-10 16:13:54 -080072import org.osgi.service.component.ComponentContext;
Daniele Moro8ea9e102020-03-24 18:56:52 +010073import org.osgi.service.component.annotations.Activate;
74import org.osgi.service.component.annotations.Component;
75import org.osgi.service.component.annotations.Deactivate;
76import org.osgi.service.component.annotations.Modified;
77import org.osgi.service.component.annotations.Reference;
78import org.osgi.service.component.annotations.ReferenceCardinality;
Ilayda Ozdemir2fccbce2021-02-23 15:36:47 +000079import org.osgi.service.component.annotations.ReferencePolicy;
alshabib3b1eadc2016-02-01 17:57:00 -080080import org.slf4j.Logger;
81
Jonathan Hart28271642016-02-10 16:13:54 -080082import java.util.Dictionary;
alshabib3b1eadc2016-02-01 17:57:00 -080083import java.util.Map;
ke han9590c812017-02-28 15:02:26 +080084import java.util.Objects;
Jonathan Hart0c194962016-05-23 17:08:15 -070085import java.util.Optional;
alshabibfc1cb032016-02-17 15:37:56 -080086import java.util.Properties;
Esin Karaman39b24852019-08-28 13:57:30 +000087import java.util.Set;
88import java.util.concurrent.ExecutorService;
89import java.util.concurrent.locks.Lock;
90import java.util.concurrent.locks.ReentrantLock;
Andrea Campanella03652352020-05-06 16:12:00 +020091import java.util.function.Consumer;
alshabib3b1eadc2016-02-01 17:57:00 -080092
alshabibfc1cb032016-02-17 15:37:56 -080093import static com.google.common.base.Strings.isNullOrEmpty;
Esin Karaman39b24852019-08-28 13:57:30 +000094import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor;
alshabibfc1cb032016-02-17 15:37:56 -080095import static org.onlab.util.Tools.get;
Esin Karaman39b24852019-08-28 13:57:30 +000096import static org.onlab.util.Tools.groupedThreads;
Daniele Moro8ea9e102020-03-24 18:56:52 +010097import static org.opencord.cordmcast.impl.OsgiPropertyConstants.*;
alshabib3b1eadc2016-02-01 17:57:00 -080098import static org.slf4j.LoggerFactory.getLogger;
99
Esin Karaman39b24852019-08-28 13:57:30 +0000100
alshabib3b1eadc2016-02-01 17:57:00 -0800101/**
Jonathan Hartc3f84eb2016-02-19 12:44:36 -0800102 * CORD multicast provisioning application. Operates by listening to
Jonathan Hart28271642016-02-10 16:13:54 -0800103 * events on the multicast rib and provisioning groups to program multicast
alshabib3b1eadc2016-02-01 17:57:00 -0800104 * flows on the dataplane.
105 */
Carmelo Cascone995fd682019-11-14 14:22:39 -0800106@Component(immediate = true,
107 property = {
Ilayda Ozdemir2fccbce2021-02-23 15:36:47 +0000108 VLAN_ENABLED + ":Boolean=" + DEFAULT_VLAN_ENABLED,
109 PRIORITY + ":Integer=" + DEFAULT_PRIORITY,
110 })
Daniele Moro8ea9e102020-03-24 18:56:52 +0100111public class CordMcast implements CordMcastService {
Ilayda Ozdemir2fccbce2021-02-23 15:36:47 +0000112 private static final String MCAST_NOT_RUNNING = "Multicast is not running.";
113 private static final String SADIS_NOT_RUNNING = "Sadis is not running.";
Daniele Moro8ea9e102020-03-24 18:56:52 +0100114 private static final String APP_NAME = "org.opencord.mcast";
alshabib3b1eadc2016-02-01 17:57:00 -0800115
Jonathan Hart0c194962016-05-23 17:08:15 -0700116 private final Logger log = getLogger(getClass());
alshabib09069c92016-02-21 14:49:51 -0800117
alshabib09069c92016-02-21 14:49:51 -0800118 private static final int DEFAULT_PRIORITY = 500;
alshabib3b1eadc2016-02-01 17:57:00 -0800119 private static final short DEFAULT_MCAST_VLAN = 4000;
alshabibfc1cb032016-02-17 15:37:56 -0800120
Ilayda Ozdemir2fccbce2021-02-23 15:36:47 +0000121 @Reference(cardinality = ReferenceCardinality.OPTIONAL,
122 bind = "bindMcastRouteService",
123 unbind = "unbindMcastRouteService",
124 policy = ReferencePolicy.DYNAMIC)
125 protected volatile MulticastRouteService mcastService;
alshabib3b1eadc2016-02-01 17:57:00 -0800126
Carmelo Cascone995fd682019-11-14 14:22:39 -0800127 @Reference(cardinality = ReferenceCardinality.MANDATORY)
alshabib3b1eadc2016-02-01 17:57:00 -0800128 protected FlowObjectiveService flowObjectiveService;
129
Carmelo Cascone995fd682019-11-14 14:22:39 -0800130 @Reference(cardinality = ReferenceCardinality.MANDATORY)
alshabib3b1eadc2016-02-01 17:57:00 -0800131 protected CoreService coreService;
132
Carmelo Cascone995fd682019-11-14 14:22:39 -0800133 @Reference(cardinality = ReferenceCardinality.MANDATORY)
Jonathan Hart28271642016-02-10 16:13:54 -0800134 protected ComponentConfigService componentConfigService;
135
Carmelo Cascone995fd682019-11-14 14:22:39 -0800136 @Reference(cardinality = ReferenceCardinality.MANDATORY)
ke hanf1709e82016-08-12 10:48:17 +0800137 protected NetworkConfigRegistry networkConfig;
138
Carmelo Cascone995fd682019-11-14 14:22:39 -0800139 @Reference(cardinality = ReferenceCardinality.MANDATORY)
Esin Karaman39b24852019-08-28 13:57:30 +0000140 protected StorageService storageService;
141
Carmelo Cascone995fd682019-11-14 14:22:39 -0800142 @Reference(cardinality = ReferenceCardinality.MANDATORY)
Esin Karaman39b24852019-08-28 13:57:30 +0000143 protected MastershipService mastershipService;
144
Carmelo Cascone995fd682019-11-14 14:22:39 -0800145 @Reference(cardinality = ReferenceCardinality.MANDATORY)
Esin Karaman39b24852019-08-28 13:57:30 +0000146 public DeviceService deviceService;
147
Carmelo Cascone995fd682019-11-14 14:22:39 -0800148 @Reference(cardinality = ReferenceCardinality.MANDATORY)
Esin Karaman39b24852019-08-28 13:57:30 +0000149 private ClusterService clusterService;
150
Carmelo Cascone995fd682019-11-14 14:22:39 -0800151 @Reference(cardinality = ReferenceCardinality.MANDATORY)
Esin Karaman39b24852019-08-28 13:57:30 +0000152 private LeadershipService leadershipService;
153
Ilayda Ozdemir2fccbce2021-02-23 15:36:47 +0000154 @Reference(cardinality = ReferenceCardinality.OPTIONAL,
155 bind = "bindSadisService",
156 unbind = "unbindSadisService",
157 policy = ReferencePolicy.DYNAMIC)
158 protected volatile SadisService sadisService;
Esin Karaman996177c2020-03-05 13:21:09 +0000159
Arjun E Kabf9e6e2020-03-02 10:15:21 +0000160 @Reference(cardinality = ReferenceCardinality.MANDATORY)
161 protected CordMcastStatisticsService cordMcastStatisticsService;
162
Andrea Campanellac2f3ddd2020-05-18 11:09:11 +0200163 @Reference(cardinality = ReferenceCardinality.MANDATORY)
164 protected GroupService groupService;
165
alshabib3b1eadc2016-02-01 17:57:00 -0800166 protected McastListener listener = new InternalMulticastListener();
Arjun E Kabf9e6e2020-03-02 10:15:21 +0000167
ke hanf1709e82016-08-12 10:48:17 +0800168 private InternalNetworkConfigListener configListener =
169 new InternalNetworkConfigListener();
alshabib3b1eadc2016-02-01 17:57:00 -0800170
Esin Karaman39b24852019-08-28 13:57:30 +0000171 private ConsistentMap<NextKey, NextContent> groups;
alshabib3b1eadc2016-02-01 17:57:00 -0800172
alshabib3b1eadc2016-02-01 17:57:00 -0800173 private ApplicationId appId;
ke hanf1709e82016-08-12 10:48:17 +0800174 private ApplicationId coreAppId;
Esin Karaman39b24852019-08-28 13:57:30 +0000175 private short mcastVlan = DEFAULT_MCAST_VLAN;
Esin Karamanbb35a3b2020-03-18 13:53:24 +0000176 private VlanId mcastInnerVlan = VlanId.NONE;
alshabib3b1eadc2016-02-01 17:57:00 -0800177
Carmelo Cascone995fd682019-11-14 14:22:39 -0800178 /**
179 * Whether to use VLAN for multicast traffic.
180 **/
alshabib09069c92016-02-21 14:49:51 -0800181 private boolean vlanEnabled = DEFAULT_VLAN_ENABLED;
alshabibfc1cb032016-02-17 15:37:56 -0800182
Carmelo Cascone995fd682019-11-14 14:22:39 -0800183 /**
184 * Priority for multicast rules.
185 **/
alshabib3b1eadc2016-02-01 17:57:00 -0800186 private int priority = DEFAULT_PRIORITY;
187
ke hanf1709e82016-08-12 10:48:17 +0800188 private static final Class<McastConfig> CORD_MCAST_CONFIG_CLASS =
189 McastConfig.class;
190
191 private ConfigFactory<ApplicationId, McastConfig> cordMcastConfigFactory =
192 new ConfigFactory<ApplicationId, McastConfig>(
193 SubjectFactories.APP_SUBJECT_FACTORY, CORD_MCAST_CONFIG_CLASS, "multicast") {
194 @Override
195 public McastConfig createConfig() {
196 return new McastConfig();
197 }
198 };
Jonathan Hart28271642016-02-10 16:13:54 -0800199
Esin Karaman39b24852019-08-28 13:57:30 +0000200 // lock to synchronize local operations
201 private final Lock mcastLock = new ReentrantLock();
Ilayda Ozdemir2fccbce2021-02-23 15:36:47 +0000202
Esin Karaman39b24852019-08-28 13:57:30 +0000203 private void mcastLock() {
204 mcastLock.lock();
205 }
Ilayda Ozdemir2fccbce2021-02-23 15:36:47 +0000206
Esin Karaman39b24852019-08-28 13:57:30 +0000207 private void mcastUnlock() {
208 mcastLock.unlock();
209 }
Ilayda Ozdemir2fccbce2021-02-23 15:36:47 +0000210
Esin Karaman39b24852019-08-28 13:57:30 +0000211 private ExecutorService eventExecutor;
212
Andrea Campanellac2f3ddd2020-05-18 11:09:11 +0200213 //Device listener to purge groups upon device disconnection.
214 private DeviceListener deviceListener = new InternalDeviceListener();
215
alshabib3b1eadc2016-02-01 17:57:00 -0800216 @Activate
Jonathan Hart435ffc42016-02-19 10:32:05 -0800217 public void activate(ComponentContext context) {
Jonathan Hartc3f84eb2016-02-19 12:44:36 -0800218 componentConfigService.registerProperties(getClass());
Jonathan Hart435ffc42016-02-19 10:32:05 -0800219 modified(context);
220
Charles Chanf867c4b2017-01-20 11:22:25 -0800221 appId = coreService.registerApplication(APP_NAME);
ke hanf1709e82016-08-12 10:48:17 +0800222 coreAppId = coreService.registerApplication(CoreService.CORE_APP_NAME);
Jonathan Hart28271642016-02-10 16:13:54 -0800223
Esin Karaman39b24852019-08-28 13:57:30 +0000224 eventExecutor = newSingleThreadScheduledExecutor(groupedThreads("cord/mcast",
225 "events-mcast-%d", log));
226
227 KryoNamespace.Builder groupsKryo = new KryoNamespace.Builder()
228 .register(KryoNamespaces.API)
229 .register(NextKey.class)
230 .register(NextContent.class);
231 groups = storageService
232 .<NextKey, NextContent>consistentMapBuilder()
233 .withName("cord-mcast-groups-store")
234 .withSerializer(Serializer.using(groupsKryo.build("CordMcast-Groups")))
235 .build();
236
ke hanf1709e82016-08-12 10:48:17 +0800237 networkConfig.registerConfigFactory(cordMcastConfigFactory);
238 networkConfig.addListener(configListener);
Ilayda Ozdemir2fccbce2021-02-23 15:36:47 +0000239 if (mcastService != null) {
240 mcastService.addListener(listener);
241 mcastService.getRoutes().stream()
242 .map(r -> new ImmutablePair<>(r, mcastService.sinks(r)))
243 .filter(pair -> pair.getRight() != null && !pair.getRight().isEmpty())
244 .forEach(pair -> pair.getRight().forEach(sink -> addSink(pair.getLeft(),
245 sink)));
246 } else {
247 log.warn(MCAST_NOT_RUNNING);
248 }
ke hanf1709e82016-08-12 10:48:17 +0800249 McastConfig config = networkConfig.getConfig(coreAppId, CORD_MCAST_CONFIG_CLASS);
Esin Karaman39b24852019-08-28 13:57:30 +0000250 updateConfig(config);
Andrea Campanellac2f3ddd2020-05-18 11:09:11 +0200251 deviceService.addListener(deviceListener);
alshabib3b1eadc2016-02-01 17:57:00 -0800252 log.info("Started");
253 }
254
Arjun E Kabf9e6e2020-03-02 10:15:21 +0000255 @Modified
256 public void modified(ComponentContext context) {
257 Dictionary<?, ?> properties = context != null ? context.getProperties() : new Properties();
258
259 String s = get(properties, VLAN_ENABLED);
260 vlanEnabled = isNullOrEmpty(s) ? DEFAULT_VLAN_ENABLED : Boolean.parseBoolean(s.trim());
261
262 try {
263 s = get(properties, PRIORITY);
264 priority = isNullOrEmpty(s) ? DEFAULT_PRIORITY : Integer.parseInt(s.trim());
265 } catch (NumberFormatException ne) {
266 log.error("Unable to parse configuration parameter for priority", ne);
267 priority = DEFAULT_PRIORITY;
268 }
Esin Karamane4890012020-04-19 11:58:54 +0000269 feedStatsServiceWithVlanConfigValues();
Arjun E Kabf9e6e2020-03-02 10:15:21 +0000270 }
271
alshabib3b1eadc2016-02-01 17:57:00 -0800272 @Deactivate
273 public void deactivate() {
Andrea Campanellac2f3ddd2020-05-18 11:09:11 +0200274 deviceService.removeListener(deviceListener);
Jonathan Hartc3f84eb2016-02-19 12:44:36 -0800275 componentConfigService.unregisterProperties(getClass(), false);
Ilayda Ozdemir2fccbce2021-02-23 15:36:47 +0000276 if (mcastService != null) {
277 mcastService.removeListener(listener);
278 }
ke hanf1709e82016-08-12 10:48:17 +0800279 networkConfig.removeListener(configListener);
ke han9590c812017-02-28 15:02:26 +0800280 networkConfig.unregisterConfigFactory(cordMcastConfigFactory);
Esin Karaman39b24852019-08-28 13:57:30 +0000281 eventExecutor.shutdown();
Andrea Campanella03652352020-05-06 16:12:00 +0200282 eventExecutor = null;
alshabib3b1eadc2016-02-01 17:57:00 -0800283 log.info("Stopped");
284 }
285
Ilayda Ozdemir2fccbce2021-02-23 15:36:47 +0000286 protected void bindSadisService(SadisService service) {
287 sadisService = service;
288 log.info("Sadis-service binds to onos.");
289 }
290
291 protected void unbindSadisService(SadisService service) {
292 sadisService = null;
293 log.info("Sadis-service unbinds from onos.");
294 }
295
296 protected void bindMcastRouteService(MulticastRouteService service) {
297 mcastService = service;
298 mcastService.addListener(listener);
299 log.info("Multicast route service binds to onos.");
300 }
301
302 protected void unbindMcastRouteService(MulticastRouteService service) {
303 service.removeListener(listener);
304 mcastService = null;
305 log.info("Multicast route service unbinds from onos.");
306 }
307
Esin Karamane4890012020-04-19 11:58:54 +0000308 /**
309 * Updates the stats service with current VLAN config values.
310 */
311 private void feedStatsServiceWithVlanConfigValues() {
312 cordMcastStatisticsService.setVlanValue(assignedVlan());
313 cordMcastStatisticsService.setInnerVlanValue(assignedInnerVlan());
314 }
315
Andrea Campanella03652352020-05-06 16:12:00 +0200316 private void clearGroups() {
Esin Karaman39b24852019-08-28 13:57:30 +0000317 mcastLock();
318 try {
319 groups.keySet().forEach(groupInfo -> {
Andrea Campanella03652352020-05-06 16:12:00 +0200320 NextContent next = groups.get(groupInfo).value();
Esin Karaman39b24852019-08-28 13:57:30 +0000321 if (!isLocalLeader(groupInfo.getDevice())) {
322 return;
323 }
Sonal Kasliwala0bbe6c2020-01-06 10:46:30 +0000324 if (next != null) {
Andrea Campanella03652352020-05-06 16:12:00 +0200325 //On Success of removing the fwd objective we remove also the group.
326 Consumer<Objective> onSuccess = (objective) -> {
327 log.debug("Successfully removed fwd objective for {} on {}, " +
Ilayda Ozdemir2fccbce2021-02-23 15:36:47 +0000328 "removing next objective {}", groupInfo.group,
329 groupInfo.getDevice(), next.getNextId());
Andrea Campanella03652352020-05-06 16:12:00 +0200330 eventExecutor.submit(() -> flowObjectiveService.next(groupInfo.getDevice(),
Ilayda Ozdemir2fccbce2021-02-23 15:36:47 +0000331 nextObject(next.getNextId(),
332 null,
333 NextType.Remove, groupInfo.group)));
Andrea Campanella03652352020-05-06 16:12:00 +0200334 };
335
336 ObjectiveContext context =
337 new DefaultObjectiveContext(onSuccess, (objective, error) ->
338 log.warn("Failed to remove {} on {}: {}",
Ilayda Ozdemir2fccbce2021-02-23 15:36:47 +0000339 groupInfo.group, next.getNextId(), error));
Sonal Kasliwala0bbe6c2020-01-06 10:46:30 +0000340 // remove the flow rule
Andrea Campanella03652352020-05-06 16:12:00 +0200341 flowObjectiveService.forward(groupInfo.getDevice(),
Ilayda Ozdemir2fccbce2021-02-23 15:36:47 +0000342 fwdObject(next.getNextId(),
343 groupInfo.group).remove(context));
Esin Karaman39b24852019-08-28 13:57:30 +0000344
Sonal Kasliwala0bbe6c2020-01-06 10:46:30 +0000345 }
Esin Karaman39b24852019-08-28 13:57:30 +0000346 });
Esin Karaman39b24852019-08-28 13:57:30 +0000347 } finally {
348 mcastUnlock();
349 }
350 }
351
352 private VlanId multicastVlan() {
353 return VlanId.vlanId(mcastVlan);
354 }
355
Arjun E Kabf9e6e2020-03-02 10:15:21 +0000356 protected VlanId assignedVlan() {
Esin Karaman39b24852019-08-28 13:57:30 +0000357 return vlanEnabled ? multicastVlan() : VlanId.NONE;
ke han9590c812017-02-28 15:02:26 +0800358 }
359
Esin Karamanbb35a3b2020-03-18 13:53:24 +0000360 protected VlanId assignedInnerVlan() {
361 return vlanEnabled ? mcastInnerVlan : VlanId.NONE;
362 }
363
alshabib3b1eadc2016-02-01 17:57:00 -0800364 private class InternalMulticastListener implements McastListener {
365 @Override
366 public void event(McastEvent event) {
Esin Karaman39b24852019-08-28 13:57:30 +0000367 eventExecutor.execute(() -> {
368 switch (event.type()) {
369 case ROUTE_ADDED:
370 case ROUTE_REMOVED:
371 case SOURCES_ADDED:
372 break;
373 case SINKS_ADDED:
374 addSinks(event);
375 break;
376 case SINKS_REMOVED:
377 removeSinks(event);
378 break;
379 default:
380 log.warn("Unknown mcast event {}", event.type());
381 }
382 });
383 }
384 }
385
386 /**
387 * Processes previous, and new sinks then finds the sinks to be removed.
Ilayda Ozdemir2fccbce2021-02-23 15:36:47 +0000388 *
Esin Karaman39b24852019-08-28 13:57:30 +0000389 * @param prevSinks the previous sinks to be evaluated
Ilayda Ozdemir2fccbce2021-02-23 15:36:47 +0000390 * @param newSinks the new sinks to be evaluated
Esin Karaman39b24852019-08-28 13:57:30 +0000391 * @returnt the set of the sinks to be removed
392 */
393 private Set<ConnectPoint> getSinksToBeRemoved(Map<HostId, Set<ConnectPoint>> prevSinks,
394 Map<HostId, Set<ConnectPoint>> newSinks) {
395 return getSinksToBeProcessed(prevSinks, newSinks);
396 }
397
398
399 /**
400 * Processes previous, and new sinks then finds the sinks to be added.
Ilayda Ozdemir2fccbce2021-02-23 15:36:47 +0000401 *
402 * @param newSinks the new sinks to be processed
Esin Karaman39b24852019-08-28 13:57:30 +0000403 * @param allPrevSinks all previous sinks
404 * @return the set of the sinks to be added
405 */
406 private Set<ConnectPoint> getSinksToBeAdded(Map<HostId, Set<ConnectPoint>> newSinks,
407 Map<HostId, Set<ConnectPoint>> allPrevSinks) {
408 return getSinksToBeProcessed(newSinks, allPrevSinks);
409 }
410
411 /**
412 * Gets single-homed sinks that are in set1 but not in set2.
Ilayda Ozdemir2fccbce2021-02-23 15:36:47 +0000413 *
Esin Karaman39b24852019-08-28 13:57:30 +0000414 * @param sinkSet1 the first sink map
415 * @param sinkSet2 the second sink map
416 * @return a set containing all the single-homed sinks found in set1 but not in set2
417 */
418 private Set<ConnectPoint> getSinksToBeProcessed(Map<HostId, Set<ConnectPoint>> sinkSet1,
419 Map<HostId, Set<ConnectPoint>> sinkSet2) {
420 final Set<ConnectPoint> sinksToBeProcessed = Sets.newHashSet();
421 sinkSet1.forEach(((hostId, connectPoints) -> {
422 if (HostId.NONE.equals(hostId)) {
423 //assume all connect points associated with HostId.NONE are single homed sinks
424 sinksToBeProcessed.addAll(connectPoints);
425 return;
426 }
427 }));
428 Set<ConnectPoint> singleHomedSinksOfSet2 = sinkSet2.get(HostId.NONE) == null ?
429 Sets.newHashSet() :
430 sinkSet2.get(HostId.NONE);
431 return Sets.difference(sinksToBeProcessed, singleHomedSinksOfSet2);
Ilayda Ozdemir2fccbce2021-02-23 15:36:47 +0000432 }
Esin Karaman39b24852019-08-28 13:57:30 +0000433
434
435 private void removeSinks(McastEvent event) {
436 mcastLock();
437 try {
438 Set<ConnectPoint> sinksToBeRemoved = getSinksToBeRemoved(event.prevSubject().sinks(),
Ilayda Ozdemir2fccbce2021-02-23 15:36:47 +0000439 event.subject().sinks());
Esin Karaman39b24852019-08-28 13:57:30 +0000440 sinksToBeRemoved.forEach(sink -> removeSink(event.subject().route().group(), sink));
441 } finally {
442 mcastUnlock();
443 }
444 }
445
446 private void removeSink(IpAddress group, ConnectPoint sink) {
447 if (!isLocalLeader(sink.deviceId())) {
448 log.debug("Not the leader of {}. Skip sink_removed event for the sink {} and group {}",
Ilayda Ozdemir2fccbce2021-02-23 15:36:47 +0000449 sink.deviceId(), sink, group);
Esin Karaman39b24852019-08-28 13:57:30 +0000450 return;
451 }
452
Esin Karaman996177c2020-03-05 13:21:09 +0000453 Optional<SubscriberAndDeviceInformation> oltInfo = getSubscriberAndDeviceInformation(sink.deviceId());
Esin Karaman39b24852019-08-28 13:57:30 +0000454
455 if (!oltInfo.isPresent()) {
456 log.warn("Unknown OLT device : {}", sink.deviceId());
457 return;
458 }
459
460 log.debug("Removing sink {} from the group {}", sink, group);
461
462 NextKey key = new NextKey(sink.deviceId(), group);
463 groups.computeIfPresent(key, (k, v) -> {
Esin Karaman39b24852019-08-28 13:57:30 +0000464
465 Set<PortNumber> outPorts = Sets.newHashSet(v.getOutPorts());
466 outPorts.remove(sink.port());
467
468 if (outPorts.isEmpty()) {
Andrea Campanella03652352020-05-06 16:12:00 +0200469 log.debug("No more output ports for group {}, removing next and fwd objectives", group);
470
471 //On Success of removing the fwd objective we remove also the group.
472 Consumer<Objective> onSuccess = (objective) -> {
473 log.debug("Successfully removed fwd objective for {} on {}, " +
474 "removing next objective {}", group, sink, v.getNextId());
475 eventExecutor.execute(() -> {
476 //No port is needed since it's a remove Operation
477 flowObjectiveService.next(sink.deviceId(), nextObject(v.getNextId(),
478 null,
479 NextType.Remove, group));
480 });
481 };
482
Esin Karaman39b24852019-08-28 13:57:30 +0000483 // this is the last sink
Andrea Campanella03652352020-05-06 16:12:00 +0200484 ObjectiveContext context = new DefaultObjectiveContext(onSuccess,
Esin Karaman39b24852019-08-28 13:57:30 +0000485 (objective, error) -> log.warn("Failed to remove {} on {}: {}",
486 group, sink, error));
487 ForwardingObjective fwdObj = fwdObject(v.getNextId(), group).remove(context);
488 flowObjectiveService.forward(sink.deviceId(), fwdObj);
Andrea Campanella03652352020-05-06 16:12:00 +0200489 } else {
490 log.debug("Group {} has remaining {} ports, removing just {} " +
491 "from it's sinks", group, outPorts, sink.port());
492 flowObjectiveService.next(sink.deviceId(), nextObject(v.getNextId(), sink.port(),
493 NextType.RemoveFromExisting, group));
Esin Karaman39b24852019-08-28 13:57:30 +0000494 }
495 // remove the whole entity if no out port exists in the port list
496 return outPorts.isEmpty() ? null : new NextContent(v.getNextId(),
497 ImmutableSet.copyOf(outPorts));
498 });
499 }
500
501 private void addSinks(McastEvent event) {
502 mcastLock();
503 try {
504 Set<ConnectPoint> sinksToBeAdded = getSinksToBeAdded(event.subject().sinks(),
505 event.prevSubject().sinks());
506 sinksToBeAdded.forEach(sink -> addSink(event.subject().route(), sink));
507 } finally {
508 mcastUnlock();
509 }
510 }
511
512 private void addSink(McastRoute route, ConnectPoint sink) {
Arjun E Kabf9e6e2020-03-02 10:15:21 +0000513
Esin Karaman39b24852019-08-28 13:57:30 +0000514 if (!isLocalLeader(sink.deviceId())) {
515 log.debug("Not the leader of {}. Skip sink_added event for the sink {} and group {}",
516 sink.deviceId(), sink, route.group());
517 return;
518 }
519
Esin Karaman996177c2020-03-05 13:21:09 +0000520 Optional<SubscriberAndDeviceInformation> oltInfo = getSubscriberAndDeviceInformation(sink.deviceId());
Esin Karaman39b24852019-08-28 13:57:30 +0000521
522 if (!oltInfo.isPresent()) {
523 log.warn("Unknown OLT device : {}", sink.deviceId());
524 return;
525 }
526
527 log.debug("Adding sink {} to the group {}", sink, route.group());
528
529 NextKey key = new NextKey(sink.deviceId(), route.group());
530 NextObjective newNextObj;
531
532 boolean theFirstSinkOfGroup = false;
533 if (!groups.containsKey(key)) {
534 // First time someone request this mcast group via this device
535 Integer nextId = flowObjectiveService.allocateNextId();
536 newNextObj = nextObject(nextId, sink.port(), NextType.AddNew, route.group());
537 // Store the new port
538 groups.put(key, new NextContent(nextId, ImmutableSet.of(sink.port())));
539 theFirstSinkOfGroup = true;
540 } else {
541 // This device already serves some subscribers of this mcast group
542 Versioned<NextContent> nextObj = groups.get(key);
543 if (nextObj.value().getOutPorts().contains(sink.port())) {
544 log.info("Group {} already serves the sink connected to {}", route.group(), sink);
545 return;
546 }
547 newNextObj = nextObject(nextObj.value().getNextId(), sink.port(),
548 NextType.AddToExisting, route.group());
549 // add new port to the group
550 Set<PortNumber> outPorts = Sets.newHashSet(nextObj.value().getOutPorts());
551 outPorts.add(sink.port());
552 groups.put(key, new NextContent(newNextObj.id(), ImmutableSet.copyOf(outPorts)));
553 }
554
555 ObjectiveContext context = new DefaultObjectiveContext(
Esin Karamanbb35a3b2020-03-18 13:53:24 +0000556 (objective) -> log.debug("Successfully add {} on {}/{}, vlan {}, inner vlan {}",
Esin Karaman39b24852019-08-28 13:57:30 +0000557 route.group(), sink.deviceId(), sink.port().toLong(),
Esin Karamanbb35a3b2020-03-18 13:53:24 +0000558 assignedVlan(), assignedInnerVlan()),
Esin Karaman39b24852019-08-28 13:57:30 +0000559 (objective, error) -> {
Esin Karamanbb35a3b2020-03-18 13:53:24 +0000560 log.warn("Failed to add {} on {}/{}, vlan {}, inner vlan {}: {}",
Esin Karaman39b24852019-08-28 13:57:30 +0000561 route.group(), sink.deviceId(), sink.port().toLong(), assignedVlan(),
Esin Karamanbb35a3b2020-03-18 13:53:24 +0000562 assignedInnerVlan(), error);
Esin Karaman39b24852019-08-28 13:57:30 +0000563 });
564
565 flowObjectiveService.next(sink.deviceId(), newNextObj);
566
567 if (theFirstSinkOfGroup) {
568 // create the necessary flow rule if this is the first sink request for the group
569 // on this device
570 flowObjectiveService.forward(sink.deviceId(), fwdObject(newNextObj.id(),
571 route.group()).add(context));
572 }
573 }
574
Esin Karaman996177c2020-03-05 13:21:09 +0000575 /**
576 * Fetches device information associated with the device serial number from SADIS.
577 *
578 * @param serialNumber serial number of a device
579 * @return device information; an empty Optional otherwise.
580 */
581 private Optional<SubscriberAndDeviceInformation> getSubscriberAndDeviceInformation(String serialNumber) {
582 long start = System.currentTimeMillis();
583 try {
Ilayda Ozdemir2fccbce2021-02-23 15:36:47 +0000584 if (sadisService == null) {
585 log.warn(SADIS_NOT_RUNNING);
586 return Optional.empty();
587 }
Esin Karaman996177c2020-03-05 13:21:09 +0000588 return Optional.ofNullable(sadisService.getSubscriberInfoService().get(serialNumber));
589 } finally {
590 if (log.isDebugEnabled()) {
591 // SADIS may call remote systems to fetch device data and this calls can take a long time.
592 // This measurement is just for monitoring these kinds of situations.
593 log.debug("Device fetched from SADIS. Elapsed {} msec", System.currentTimeMillis() - start);
594 }
595
596 }
597 }
598
599 /**
600 * Fetches device information associated with the device serial number from SADIS.
601 *
602 * @param deviceId device id
603 * @return device information; an empty Optional otherwise.
604 */
605 private Optional<SubscriberAndDeviceInformation> getSubscriberAndDeviceInformation(DeviceId deviceId) {
606 Device device = deviceService.getDevice(deviceId);
607 if (device == null || device.serialNumber() == null) {
608 return Optional.empty();
609 }
610 return getSubscriberAndDeviceInformation(device.serialNumber());
611 }
612
Esin Karaman39b24852019-08-28 13:57:30 +0000613 private class InternalNetworkConfigListener implements NetworkConfigListener {
614 @Override
615 public void event(NetworkConfigEvent event) {
616 eventExecutor.execute(() -> {
617 switch (event.type()) {
618
619 case CONFIG_ADDED:
620 case CONFIG_UPDATED:
621 if (event.configClass().equals(CORD_MCAST_CONFIG_CLASS)) {
622 McastConfig config = networkConfig.getConfig(coreAppId, CORD_MCAST_CONFIG_CLASS);
623 if (config != null) {
624 //TODO: Simply remove flows/groups, hosts will response period query
625 // and re-sent IGMP report, so the flows can be rebuild.
626 // However, better to remove and re-add mcast flow rules here
Esin Karamanbb35a3b2020-03-18 13:53:24 +0000627 if (vlanEnabled && (mcastVlan != config.egressVlan().toShort() ||
628 !mcastInnerVlan.equals(config.egressInnerVlan()))) {
Esin Karaman39b24852019-08-28 13:57:30 +0000629 clearGroups();
Andrea Campanellac2f3ddd2020-05-18 11:09:11 +0200630 groups.clear();
Esin Karaman39b24852019-08-28 13:57:30 +0000631 }
632 updateConfig(config);
633 }
634 }
635 break;
636 case CONFIG_REGISTERED:
637 case CONFIG_UNREGISTERED:
638 case CONFIG_REMOVED:
639 break;
640 default:
641 break;
642 }
643 });
644 }
645 }
646
647 private void updateConfig(McastConfig config) {
648 if (config == null) {
649 return;
650 }
651 log.debug("multicast config received: {}", config);
652
653 if (config.egressVlan() != null) {
654 mcastVlan = config.egressVlan().toShort();
655 }
Esin Karamanbb35a3b2020-03-18 13:53:24 +0000656 if (config.egressInnerVlan() != null) {
657 mcastInnerVlan = config.egressInnerVlan();
658 }
Esin Karamane4890012020-04-19 11:58:54 +0000659 feedStatsServiceWithVlanConfigValues();
Esin Karaman39b24852019-08-28 13:57:30 +0000660 }
661
662 private class NextKey {
663 private DeviceId device;
664 private IpAddress group;
665
666 public NextKey(DeviceId deviceId, IpAddress groupAddress) {
667 device = deviceId;
668 group = groupAddress;
669 }
670
671 public DeviceId getDevice() {
672 return device;
673 }
674
675 public int hashCode() {
676 return Objects.hash(this.device, this.group);
677 }
678
679 public boolean equals(Object obj) {
680 if (this == obj) {
681 return true;
682 } else if (!(obj instanceof NextKey)) {
683 return false;
684 } else {
685 NextKey that = (NextKey) obj;
686 return this.getClass() == that.getClass() &&
687 Objects.equals(this.device, that.device) &&
688 Objects.equals(this.group, that.group);
689 }
690 }
691 }
692
693 private class NextContent {
694 private Integer nextId;
695 private Set<PortNumber> outPorts;
696
697 public NextContent(Integer nextId, Set<PortNumber> outPorts) {
698 this.nextId = nextId;
699 this.outPorts = outPorts;
700 }
701
702 public Integer getNextId() {
703 return nextId;
704 }
705
706 public Set<PortNumber> getOutPorts() {
707 return ImmutableSet.copyOf(outPorts);
708 }
709
710 public int hashCode() {
711 return Objects.hash(this.nextId, this.outPorts);
712 }
713
714 public boolean equals(Object obj) {
715 if (this == obj) {
716 return true;
717 } else if (!(obj instanceof NextContent)) {
718 return false;
719 } else {
720 NextContent that = (NextContent) obj;
721 return this.getClass() == that.getClass() &&
722 Objects.equals(this.nextId, that.nextId) &&
723 Objects.equals(this.outPorts, that.outPorts);
alshabib3b1eadc2016-02-01 17:57:00 -0800724 }
725 }
726 }
727
Ilayda Ozdemir2fccbce2021-02-23 15:36:47 +0000728 private enum NextType { AddNew, AddToExisting, Remove, RemoveFromExisting }
ke han9590c812017-02-28 15:02:26 +0800729
Esin Karaman39b24852019-08-28 13:57:30 +0000730 private NextObjective nextObject(Integer nextId, PortNumber port,
731 NextType nextType, IpAddress mcastIp) {
ke han9590c812017-02-28 15:02:26 +0800732
Esin Karaman39b24852019-08-28 13:57:30 +0000733 // Build the meta selector with the fwd objective info
734 TrafficSelector.Builder metadata = DefaultTrafficSelector.builder()
735 .matchIPDst(mcastIp.toIpPrefix());
736
737 if (vlanEnabled) {
738 metadata.matchVlanId(multicastVlan());
Esin Karamanbb35a3b2020-03-18 13:53:24 +0000739
740 if (!mcastInnerVlan.equals(VlanId.NONE)) {
741 metadata.matchInnerVlanId(mcastInnerVlan);
742 }
Esin Karaman39b24852019-08-28 13:57:30 +0000743 }
744
Andrea Campanella03652352020-05-06 16:12:00 +0200745 DefaultNextObjective.Builder builder = DefaultNextObjective.builder()
ke han9590c812017-02-28 15:02:26 +0800746 .fromApp(appId)
ke han9590c812017-02-28 15:02:26 +0800747 .withType(NextObjective.Type.BROADCAST)
Esin Karaman39b24852019-08-28 13:57:30 +0000748 .withId(nextId)
749 .withMeta(metadata.build());
750
Andrea Campanella03652352020-05-06 16:12:00 +0200751 if (port == null && !nextType.equals(NextType.Remove)) {
752 log.error("Port can't be null with operation {}", nextType);
753 return null;
754 } else if (port != null && !nextType.equals(NextType.Remove)) {
755 builder.addTreatment(DefaultTrafficTreatment.builder().setOutput(port).build());
756 }
757
758 ObjectiveContext context = new ObjectiveContext() {
ke han9590c812017-02-28 15:02:26 +0800759 @Override
760 public void onSuccess(Objective objective) {
Andrea Campanella03652352020-05-06 16:12:00 +0200761 log.debug("Success for operation {} on Next Objective {}", objective.id(), nextType);
ke han9590c812017-02-28 15:02:26 +0800762 }
763
764 @Override
765 public void onError(Objective objective, ObjectiveError error) {
Esin Karaman39b24852019-08-28 13:57:30 +0000766 log.debug("Next Objective {} failed, because {}",
767 objective.id(),
768 error);
ke han9590c812017-02-28 15:02:26 +0800769 }
770 };
771
772 switch (nextType) {
773 case AddNew:
Andrea Campanella03652352020-05-06 16:12:00 +0200774 return builder.add(context);
ke han9590c812017-02-28 15:02:26 +0800775 case AddToExisting:
Andrea Campanella03652352020-05-06 16:12:00 +0200776 return builder.addToExisting(context);
ke han9590c812017-02-28 15:02:26 +0800777 case Remove:
Andrea Campanella03652352020-05-06 16:12:00 +0200778 return builder.remove(context);
ke han9590c812017-02-28 15:02:26 +0800779 case RemoveFromExisting:
Andrea Campanella03652352020-05-06 16:12:00 +0200780 return builder.removeFromExisting(context);
ke han9590c812017-02-28 15:02:26 +0800781 default:
782 return null;
783 }
784 }
785
Esin Karaman39b24852019-08-28 13:57:30 +0000786 private ForwardingObjective.Builder fwdObject(int nextId, IpAddress mcastIp) {
787 TrafficSelector.Builder mcast = DefaultTrafficSelector.builder()
788 .matchEthType(Ethernet.TYPE_IPV4)
789 .matchIPDst(mcastIp.toIpPrefix());
ke han9590c812017-02-28 15:02:26 +0800790
Esin Karaman39b24852019-08-28 13:57:30 +0000791 //build the meta selector
792 TrafficSelector.Builder metabuilder = DefaultTrafficSelector.builder();
793 if (vlanEnabled) {
794 metabuilder.matchVlanId(multicastVlan());
Esin Karamanbb35a3b2020-03-18 13:53:24 +0000795
796 if (!mcastInnerVlan.equals(VlanId.NONE)) {
797 metabuilder.matchInnerVlanId(mcastInnerVlan);
798 }
Jonathan Hart718c0452016-02-18 15:56:22 -0800799 }
800
Esin Karaman39b24852019-08-28 13:57:30 +0000801 ForwardingObjective.Builder fwdBuilder = DefaultForwardingObjective.builder()
802 .fromApp(appId)
803 .nextStep(nextId)
804 .makePermanent()
805 .withFlag(ForwardingObjective.Flag.SPECIFIC)
806 .withPriority(priority)
807 .withSelector(mcast.build())
808 .withMeta(metabuilder.build());
alshabibfc1cb032016-02-17 15:37:56 -0800809
Esin Karaman39b24852019-08-28 13:57:30 +0000810 return fwdBuilder;
alshabibfc1cb032016-02-17 15:37:56 -0800811 }
812
Esin Karaman39b24852019-08-28 13:57:30 +0000813 // Custom-built function, when the device is not available we need a fallback mechanism
814 private boolean isLocalLeader(DeviceId deviceId) {
815 if (!mastershipService.isLocalMaster(deviceId)) {
816 // When the device is available we just check the mastership
817 if (deviceService.isAvailable(deviceId)) {
ke han9590c812017-02-28 15:02:26 +0800818 return false;
ke han9590c812017-02-28 15:02:26 +0800819 }
Esin Karaman39b24852019-08-28 13:57:30 +0000820 // Fallback with Leadership service - device id is used as topic
821 NodeId leader = leadershipService.runForLeadership(
822 deviceId.toString()).leaderNodeId();
823 // Verify if this node is the leader
824 return clusterService.getLocalNode().id().equals(leader);
ke han9590c812017-02-28 15:02:26 +0800825 }
Esin Karaman39b24852019-08-28 13:57:30 +0000826 return true;
ke han9590c812017-02-28 15:02:26 +0800827 }
Esin Karaman39b24852019-08-28 13:57:30 +0000828
Andrea Campanellac2f3ddd2020-05-18 11:09:11 +0200829 private class InternalDeviceListener implements DeviceListener {
830
831 @Override
832 public void event(DeviceEvent event) {
833 eventExecutor.execute(() -> {
834 DeviceId devId = event.subject().id();
Andrea Campanella86bee262020-05-18 20:15:01 +0200835 if (!deviceService.isAvailable(devId) &&
836 isLocalLeader(event.subject().id())) {
Andrea Campanellac2f3ddd2020-05-18 11:09:11 +0200837 if (deviceService.getPorts(devId).isEmpty()) {
838 log.info("Handling controlled device disconnection .. "
839 + "flushing all state for dev:{}", devId);
840 groupService.purgeGroupEntries(devId);
841 groups.keySet().iterator().forEachRemaining(groupInfo -> {
842 if (groupInfo.device.equals(devId)) {
843 log.debug("Removing next key {} from distributed mcast map", groupInfo.group);
844 groups.remove(groupInfo);
845 }
846 });
847 } else {
848 log.info("Disconnected device has available ports .. "
849 + "assuming temporary disconnection, "
850 + "retaining state for device {}", devId);
851 }
Andrea Campanellac2f3ddd2020-05-18 11:09:11 +0200852 }
853 });
854
855 }
856
857 @Override
858 public boolean isRelevant(DeviceEvent event) {
Andrea Campanella86bee262020-05-18 20:15:01 +0200859 return event.type().equals(DeviceEvent.Type.DEVICE_AVAILABILITY_CHANGED);
Andrea Campanellac2f3ddd2020-05-18 11:09:11 +0200860 }
861 }
862
alshabib3b1eadc2016-02-01 17:57:00 -0800863}
ke hanf1709e82016-08-12 10:48:17 +0800864
ke han9590c812017-02-28 15:02:26 +0800865