blob: 0a73e3e97f6febbcc0f59c342c477ef4f94af7dc [file] [log] [blame]
Tunahan Sezen03e55272020-04-18 09:18:53 +00001/*
2 * Copyright 2017-present Open Networking Foundation
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.opencord.maclearner.app.impl;
17
18import com.google.common.collect.ImmutableMap;
19import com.google.common.collect.ImmutableSet;
20import com.google.common.collect.Maps;
21import com.google.common.collect.Sets;
22import org.onlab.packet.VlanId;
23import org.onlab.util.Tools;
24import org.onosproject.cfg.ComponentConfigService;
25import org.onosproject.core.ApplicationId;
26import org.onosproject.mastership.MastershipService;
27import org.onosproject.net.device.DeviceEvent;
28import org.onosproject.net.device.DeviceListener;
29import org.onosproject.net.device.DeviceService;
30import org.onosproject.store.service.ConsistentMap;
31import org.onosproject.store.service.StorageService;
32import org.onosproject.store.service.Versioned;
33import org.opencord.maclearner.api.DefaultMacLearner;
34import org.opencord.maclearner.api.MacDeleteResult;
35import org.opencord.maclearner.api.MacLearnerEvent;
36import org.opencord.maclearner.api.MacLearnerKey;
37import org.opencord.maclearner.api.MacLearnerListener;
38import org.opencord.maclearner.api.MacLearnerProvider;
39import org.opencord.maclearner.api.MacLearnerProviderService;
40import org.opencord.maclearner.api.MacLearnerService;
41import org.opencord.maclearner.api.MacLearnerValue;
42import org.osgi.service.component.ComponentContext;
43import org.osgi.service.component.annotations.Activate;
44import org.osgi.service.component.annotations.Component;
45import org.osgi.service.component.annotations.Deactivate;
46import org.osgi.service.component.annotations.Modified;
47import org.osgi.service.component.annotations.Reference;
48import org.onlab.packet.DHCP;
49import org.onlab.packet.Ethernet;
50import org.onlab.packet.IPv4;
51import org.onlab.packet.MacAddress;
52import org.onlab.packet.UDP;
53import org.onlab.packet.dhcp.DhcpOption;
54import org.onlab.util.KryoNamespace;
55import org.onosproject.core.CoreService;
56import org.onosproject.net.DeviceId;
57import org.onosproject.net.PortNumber;
58import org.onosproject.net.packet.PacketContext;
59import org.onosproject.net.packet.PacketProcessor;
60import org.onosproject.net.packet.PacketService;
61import org.onosproject.net.provider.AbstractListenerProviderRegistry;
62import org.onosproject.net.provider.AbstractProviderService;
63import org.onosproject.store.LogicalTimestamp;
64import org.onosproject.store.serializers.KryoNamespaces;
65import org.onosproject.store.service.Serializer;
66import org.onosproject.store.service.WallClockTimestamp;
67import org.slf4j.Logger;
68import org.slf4j.LoggerFactory;
69
70import java.net.URI;
71import java.util.Date;
72import java.util.Dictionary;
73import java.util.Map;
74import java.util.Optional;
75import java.util.Properties;
76import java.util.Set;
77import java.util.concurrent.ExecutorService;
78import java.util.concurrent.Executors;
79import java.util.concurrent.ScheduledExecutorService;
80import java.util.concurrent.ScheduledFuture;
81import java.util.concurrent.TimeUnit;
82import java.util.stream.Collectors;
83
84import static com.google.common.base.Strings.isNullOrEmpty;
85import static org.onlab.packet.DHCP.DHCPOptionCode.OptionCode_MessageType;
86import static org.onlab.util.Tools.groupedThreads;
87import static org.opencord.maclearner.app.impl.OsgiPropertyConstants.CACHE_DURATION_DEFAULT;
88import static org.opencord.maclearner.app.impl.OsgiPropertyConstants.CACHE_DURATION;
89import static org.opencord.maclearner.app.impl.OsgiPropertyConstants.ENABLE_DEVICE_LISTENER;
90import static org.opencord.maclearner.app.impl.OsgiPropertyConstants.ENABLE_DEVICE_LISTENER_DEFAULT;
91import static org.osgi.service.component.annotations.ReferenceCardinality.MANDATORY;
92
93/**
94 * Mac Learner Service implementation.
95 */
96@Component(immediate = true,
97 property = {
98 CACHE_DURATION + ":Integer=" + CACHE_DURATION_DEFAULT,
99 ENABLE_DEVICE_LISTENER + ":Boolean=" + ENABLE_DEVICE_LISTENER_DEFAULT
100 },
101 service = MacLearnerService.class
102)
103public class MacLearnerManager
104 extends AbstractListenerProviderRegistry<MacLearnerEvent, MacLearnerListener,
105 MacLearnerProvider, MacLearnerProviderService>
106 implements MacLearnerService {
107
108 private static final String MAC_LEARNER_APP = "org.opencord.maclearner";
109 private static final String MAC_LEARNER = "maclearner";
110 private ApplicationId appId;
111
112 private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
113 private ScheduledFuture scheduledFuture;
114
115 private final Logger log = LoggerFactory.getLogger(getClass());
116
117 @Reference(cardinality = MANDATORY)
118 protected CoreService coreService;
119
120 @Reference(cardinality = MANDATORY)
121 protected MastershipService mastershipService;
122
123 @Reference(cardinality = MANDATORY)
124 protected DeviceService deviceService;
125
126 @Reference(cardinality = MANDATORY)
127 protected PacketService packetService;
128
129 @Reference(cardinality = MANDATORY)
130 protected StorageService storageService;
131
132
133 @Reference(cardinality = MANDATORY)
134 protected ComponentConfigService componentConfigService;
135
136 private MacLearnerPacketProcessor macLearnerPacketProcessor =
137 new MacLearnerPacketProcessor();
138
139 private DeviceListener deviceListener = new InternalDeviceListener();
140
141 /**
142 * Minimum duration of mapping, mapping can be exist until 2*cacheDuration because of cleanerTimer fixed rate.
143 */
144 protected int cacheDurationSec = CACHE_DURATION_DEFAULT;
145
146 /**
147 * Register a device event listener for removing mappings from MAC Address Map for removed events.
148 */
149 protected boolean enableDeviceListener = ENABLE_DEVICE_LISTENER_DEFAULT;
150
151 private ConsistentMap<DeviceId, Set<PortNumber>> ignoredPortsMap;
152 private ConsistentMap<MacLearnerKey, MacLearnerValue> macAddressMap;
153
154 protected ExecutorService eventExecutor;
155
156 @Activate
157 public void activate() {
158 eventExecutor = Executors.newFixedThreadPool(5, groupedThreads("onos/maclearner",
159 "events-%d", log));
160 appId = coreService.registerApplication(MAC_LEARNER_APP);
161 componentConfigService.registerProperties(getClass());
162 eventDispatcher.addSink(MacLearnerEvent.class, listenerRegistry);
163 macAddressMap = storageService.<MacLearnerKey, MacLearnerValue>consistentMapBuilder()
164 .withName(MAC_LEARNER)
165 .withSerializer(createSerializer())
166 .withApplicationId(appId)
167 .build();
168 ignoredPortsMap = storageService
169 .<DeviceId, Set<PortNumber>>consistentMapBuilder()
170 .withName("maclearner-ignored")
171 .withSerializer(createSerializer())
172 .withApplicationId(appId)
173 .build();
174 //mac learner must process the packet before director processors
175 packetService.addProcessor(macLearnerPacketProcessor,
176 PacketProcessor.advisor(2));
177 if (enableDeviceListener) {
178 deviceService.addListener(deviceListener);
179 }
180 createSchedulerForClearMacMappings();
181 log.info("{} is started.", getClass().getSimpleName());
182 }
183
184 private Serializer createSerializer() {
185 return Serializer.using(KryoNamespace.newBuilder()
186 .register(KryoNamespace.newBuilder().build(MAC_LEARNER))
187 // not so robust way to avoid collision with other
188 // user supplied registrations
189 .nextId(KryoNamespaces.BEGIN_USER_CUSTOM_ID + 100)
190 .register(KryoNamespaces.BASIC)
191 .register(LogicalTimestamp.class)
192 .register(WallClockTimestamp.class)
193 .register(MacLearnerKey.class)
194 .register(MacLearnerValue.class)
195 .register(DeviceId.class)
196 .register(URI.class)
197 .register(PortNumber.class)
198 .register(VlanId.class)
199 .register(MacAddress.class)
200 .build(MAC_LEARNER + "-ecmap"));
201 }
202
203 private void createSchedulerForClearMacMappings() {
204 scheduledFuture = scheduledExecutorService.scheduleAtFixedRate(this::clearExpiredMacMappings,
205 0,
206 cacheDurationSec,
207 TimeUnit.SECONDS);
208 }
209
210 private void clearExpiredMacMappings() {
211 Date curDate = new Date();
212 for (Map.Entry<MacLearnerKey, Versioned<MacLearnerValue>> entry : macAddressMap.entrySet()) {
213 if (!mastershipService.isLocalMaster(entry.getKey().getDeviceId())) {
214 return;
215 }
216 if (curDate.getTime() - entry.getValue().value().getTimestamp() > cacheDurationSec * 1000) {
217 removeFromMacAddressMap(entry.getKey());
218 }
219 }
220 }
221
222 @Deactivate
223 public void deactivate() {
224 if (scheduledFuture != null) {
225 scheduledFuture.cancel(true);
226 }
227 packetService.removeProcessor(macLearnerPacketProcessor);
228 if (enableDeviceListener) {
229 deviceService.removeListener(deviceListener);
230 }
231 eventDispatcher.removeSink(MacLearnerEvent.class);
232 componentConfigService.unregisterProperties(getClass(), false);
233 log.info("{} is stopped.", getClass().getSimpleName());
234 }
235
236 @Modified
237 public void modified(ComponentContext context) {
238 Dictionary<?, ?> properties = context != null ? context.getProperties() : new Properties();
239
240 String cacheDuration = Tools.get(properties, CACHE_DURATION);
241 if (!isNullOrEmpty(cacheDuration)) {
242 int cacheDur = Integer.parseInt(cacheDuration.trim());
243 if (cacheDurationSec != cacheDur) {
244 setMacMappingCacheDuration(cacheDur);
245 }
246 }
247
248 Boolean enableDevListener = Tools.isPropertyEnabled(properties, ENABLE_DEVICE_LISTENER);
249 if (enableDevListener != null && enableDeviceListener != enableDevListener) {
250 enableDeviceListener = enableDevListener;
251 log.info("enableDeviceListener parameter changed to: {}", enableDeviceListener);
252 if (this.enableDeviceListener) {
253 deviceService.addListener(deviceListener);
254 } else {
255 deviceService.removeListener(deviceListener);
256 }
257 }
258 }
259
260 private Integer setMacMappingCacheDuration(Integer second) {
261 if (cacheDurationSec == second) {
262 log.info("Cache duration already: {}", second);
263 return second;
264 }
265 log.info("Changing cache duration to: {} second from {} second...", second, cacheDurationSec);
266 this.cacheDurationSec = second;
267 if (scheduledFuture != null) {
268 scheduledFuture.cancel(false);
269 }
270 createSchedulerForClearMacMappings();
271 return cacheDurationSec;
272 }
273
274 @Override
275 public void addPortToIgnore(DeviceId deviceId, PortNumber portNumber) {
276 log.info("Adding ignore port: {} {}", deviceId, portNumber);
277 Set<PortNumber> updatedPorts = Sets.newHashSet();
278 Versioned<Set<PortNumber>> storedPorts = ignoredPortsMap.get(deviceId);
279 if (storedPorts == null || !storedPorts.value().contains(portNumber)) {
280 if (storedPorts != null) {
281 updatedPorts.addAll(storedPorts.value());
282 }
283 updatedPorts.add(portNumber);
284 ignoredPortsMap.put(deviceId, updatedPorts);
285 log.info("Port:{} of device: {} is added to ignoredPortsMap.", portNumber, deviceId);
286 deleteMacMappings(deviceId, portNumber);
287 } else {
288 log.warn("Port:{} of device: {} is already ignored.", portNumber, deviceId);
289 }
290 }
291
292 @Override
293 public void removeFromIgnoredPorts(DeviceId deviceId, PortNumber portNumber) {
294 log.info("Removing ignore port: {} {}", deviceId, portNumber);
295 Versioned<Set<PortNumber>> storedPorts = ignoredPortsMap.get(deviceId);
296 if (storedPorts != null && storedPorts.value().contains(portNumber)) {
297 if (storedPorts.value().size() == 1) {
298 ignoredPortsMap.remove(deviceId);
299 } else {
300 Set<PortNumber> updatedPorts = Sets.newHashSet();
301 updatedPorts.addAll(storedPorts.value());
302 updatedPorts.remove(portNumber);
303 ignoredPortsMap.put(deviceId, updatedPorts);
304 }
305 log.info("Port:{} of device: {} is removed ignoredPortsMap.", portNumber, deviceId);
306 } else {
307 log.warn("Port:{} of device: {} is not found in ignoredPortsMap.", portNumber, deviceId);
308 }
309 }
310
311 @Override
312 public ImmutableMap<MacLearnerKey, MacAddress> getAllMappings() {
313 log.info("Getting all MAC Mappings");
314 Map<MacLearnerKey, MacAddress> immutableMap = Maps.newHashMap();
315 macAddressMap.entrySet().forEach(entry ->
316 immutableMap.put(entry.getKey(),
317 entry.getValue() != null ? entry.getValue().value().getMacAddress() : null));
318 return ImmutableMap.copyOf(immutableMap);
319 }
320
321 @Override
322 public Optional<MacAddress> getMacMapping(DeviceId deviceId, PortNumber portNumber, VlanId vlanId) {
323 log.info("Getting MAC mapping for: {} {} {}", deviceId, portNumber, vlanId);
324 Versioned<MacLearnerValue> value = macAddressMap.get(new MacLearnerKey(deviceId, portNumber, vlanId));
325 return value != null ? Optional.ofNullable(value.value().getMacAddress()) : Optional.empty();
326 }
327
328 @Override
329 public MacDeleteResult deleteMacMapping(DeviceId deviceId, PortNumber portNumber, VlanId vlanId) {
330 log.info("Deleting MAC mapping for: {} {} {}", deviceId, portNumber, vlanId);
331 MacLearnerKey key = new MacLearnerKey(deviceId, portNumber, vlanId);
332 return removeFromMacAddressMap(key);
333 }
334
335 @Override
336 public boolean deleteMacMappings(DeviceId deviceId, PortNumber portNumber) {
337 log.info("Deleting MAC mappings for: {} {}", deviceId, portNumber);
338 Set<Map.Entry<MacLearnerKey, Versioned<MacLearnerValue>>> entriesToDelete = macAddressMap.entrySet().stream()
339 .filter(entry -> entry.getKey().getDeviceId().equals(deviceId) &&
340 entry.getKey().getPortNumber().equals(portNumber))
341 .collect(Collectors.toSet());
342 if (entriesToDelete.isEmpty()) {
343 log.warn("MAC mapping not found for deviceId: {} and portNumber: {}", deviceId, portNumber);
344 return false;
345 }
346 entriesToDelete.forEach(e -> removeFromMacAddressMap(e.getKey()));
347 return true;
348 }
349
350 @Override
351 public boolean deleteMacMappings(DeviceId deviceId) {
352 log.info("Deleting MAC mappings for: {}", deviceId);
353 Set<Map.Entry<MacLearnerKey, Versioned<MacLearnerValue>>> entriesToDelete = macAddressMap.entrySet().stream()
354 .filter(entry -> entry.getKey().getDeviceId().equals(deviceId))
355 .collect(Collectors.toSet());
356 if (entriesToDelete.isEmpty()) {
357 log.warn("MAC mapping not found for deviceId: {}", deviceId);
358 return false;
359 }
360 entriesToDelete.forEach(e -> removeFromMacAddressMap(e.getKey()));
361 return true;
362 }
363
364 @Override
365 public ImmutableSet<DeviceId> getMappedDevices() {
366 Set<DeviceId> deviceIds = Sets.newHashSet();
367 for (Map.Entry<MacLearnerKey, MacAddress> entry : getAllMappings().entrySet()) {
368 deviceIds.add(entry.getKey().getDeviceId());
369 }
370 return ImmutableSet.copyOf(deviceIds);
371 }
372
373 @Override
374 public ImmutableSet<PortNumber> getMappedPorts() {
375 Set<PortNumber> portNumbers = Sets.newHashSet();
376 for (Map.Entry<MacLearnerKey, MacAddress> entry : getAllMappings().entrySet()) {
377 portNumbers.add(entry.getKey().getPortNumber());
378 }
379 return ImmutableSet.copyOf(portNumbers);
380 }
381
382 @Override
383 public ImmutableMap<DeviceId, Set<PortNumber>> getIgnoredPorts() {
384 log.info("Getting ignored ports");
385 Map<DeviceId, Set<PortNumber>> immutableMap = Maps.newHashMap();
386 ignoredPortsMap.forEach(entry -> immutableMap.put(entry.getKey(),
387 entry.getValue() != null ? entry.getValue().value() : Sets.newHashSet()));
388 return ImmutableMap.copyOf(immutableMap);
389 }
390
391 @Override
392 protected MacLearnerProviderService createProviderService(MacLearnerProvider provider) {
393 return new InternalMacLearnerProviderService(provider);
394 }
395
396 private static class InternalMacLearnerProviderService extends AbstractProviderService<MacLearnerProvider>
397 implements MacLearnerProviderService {
398
399 InternalMacLearnerProviderService(MacLearnerProvider provider) {
400 super(provider);
401 }
402 }
403
404 private void sendMacLearnerEvent(MacLearnerEvent.Type type, DeviceId deviceId,
405 PortNumber portNumber, VlanId vlanId, MacAddress macAddress) {
406 log.info("Sending MAC Learner Event: type: {} deviceId: {} portNumber: {} vlanId: {} macAddress: {}",
407 type, deviceId, portNumber, vlanId.toShort(), macAddress);
408 DefaultMacLearner macLearner = new DefaultMacLearner(deviceId, portNumber, vlanId, macAddress);
409 MacLearnerEvent macLearnerEvent = new MacLearnerEvent(type, macLearner);
410 post(macLearnerEvent);
411 }
412
413 private class MacLearnerPacketProcessor implements PacketProcessor {
414
415 @Override
416 public void process(PacketContext context) {
417 // process the packet and get the payload
418 Ethernet packet = context.inPacket().parsed();
419
420 if (packet == null) {
421 log.warn("Packet is null");
422 return;
423 }
424
425 PortNumber sourcePort = context.inPacket().receivedFrom().port();
426 DeviceId deviceId = context.inPacket().receivedFrom().deviceId();
427
428 Versioned<Set<PortNumber>> ignoredPortsOfDevice = ignoredPortsMap.get(deviceId);
429 if (ignoredPortsOfDevice != null && ignoredPortsOfDevice.value().contains(sourcePort)) {
430 log.warn("Port Number: {} is in ignoredPortsMap. Returning", sourcePort);
431 return;
432 }
433
434 if (packet.getEtherType() == Ethernet.TYPE_IPV4) {
435 IPv4 ipv4Packet = (IPv4) packet.getPayload();
436
437 if (ipv4Packet.getProtocol() == IPv4.PROTOCOL_UDP) {
438 UDP udpPacket = (UDP) ipv4Packet.getPayload();
439 int udpSourcePort = udpPacket.getSourcePort();
440 if ((udpSourcePort == UDP.DHCP_CLIENT_PORT) || (udpSourcePort == UDP.DHCP_SERVER_PORT)) {
441 DHCP dhcpPayload = (DHCP) udpPacket.getPayload();
442 //This packet is dhcp.
443 VlanId vlanId = packet.getQinQVID() != -1 ?
444 VlanId.vlanId(packet.getQinQVID()) : VlanId.vlanId(packet.getVlanID());
445 processDhcpPacket(context, packet, dhcpPayload, sourcePort, deviceId, vlanId);
446 }
447 }
448 }
449 }
450
451 //process the dhcp packet before forwarding
452 private void processDhcpPacket(PacketContext context, Ethernet packet,
453 DHCP dhcpPayload, PortNumber sourcePort, DeviceId deviceId, VlanId vlanId) {
454 if (dhcpPayload == null) {
455 log.warn("DHCP payload is null");
456 return;
457 }
458
459 DHCP.MsgType incomingPacketType = getDhcpPacketType(dhcpPayload);
460
461 if (incomingPacketType == null) {
462 log.warn("Incoming packet type is null!");
463 return;
464 }
465
466 log.info("Received DHCP Packet of type {} from {}",
467 incomingPacketType, context.inPacket().receivedFrom());
468
469 if (incomingPacketType.equals(DHCP.MsgType.DHCPDISCOVER) ||
470 incomingPacketType.equals(DHCP.MsgType.DHCPREQUEST)) {
471 addToMacAddressMap(deviceId, sourcePort, vlanId, packet.getSourceMAC());
472 }
473 }
474
475 // get type of the DHCP packet
476 private DHCP.MsgType getDhcpPacketType(DHCP dhcpPayload) {
477
478 for (DhcpOption option : dhcpPayload.getOptions()) {
479 if (option.getCode() == OptionCode_MessageType.getValue()) {
480 byte[] data = option.getData();
481 return DHCP.MsgType.getType(data[0]);
482 }
483 }
484 return null;
485 }
486
487 private void addToMacAddressMap(DeviceId deviceId, PortNumber portNumber,
488 VlanId vlanId, MacAddress macAddress) {
489 Versioned<MacLearnerValue> prevMacAddress =
490 macAddressMap.put(new MacLearnerKey(deviceId, portNumber, vlanId),
491 new MacLearnerValue(macAddress, new Date().getTime()));
492 if (prevMacAddress != null && !prevMacAddress.value().getMacAddress().equals(macAddress)) {
493 sendMacLearnerEvent(MacLearnerEvent.Type.REMOVED,
494 deviceId,
495 portNumber,
496 vlanId,
497 prevMacAddress.value().getMacAddress());
498 } else if (prevMacAddress == null || !prevMacAddress.value().getMacAddress().equals(macAddress)) {
499 // Not sending event for already mapped
500 log.info("Mapped MAC: {} for port: {} of deviceId: {} and vlanId: {}",
501 macAddress, portNumber, deviceId, vlanId);
502 sendMacLearnerEvent(MacLearnerEvent.Type.ADDED, deviceId, portNumber, vlanId, macAddress);
503 }
504 }
505
506 }
507
508 private MacDeleteResult removeFromMacAddressMap(MacLearnerKey macLearnerKey) {
509 Versioned<MacLearnerValue> verMacAddress = macAddressMap.remove(macLearnerKey);
510 if (verMacAddress != null) {
511 log.info("Mapping removed. deviceId: {} portNumber: {} vlanId: {} macAddress: {}",
512 macLearnerKey.getDeviceId(), macLearnerKey.getPortNumber(),
513 verMacAddress.value(), verMacAddress.value().getMacAddress());
514 sendMacLearnerEvent(MacLearnerEvent.Type.REMOVED,
515 macLearnerKey.getDeviceId(),
516 macLearnerKey.getPortNumber(),
517 macLearnerKey.getVlanId(),
518 verMacAddress.value().getMacAddress());
519 return MacDeleteResult.SUCCESSFUL;
520 } else {
521 log.warn("MAC not removed, because mapping not found for deviceId: {} and portNumber: {} and vlanId: {}",
522 macLearnerKey.getDeviceId(),
523 macLearnerKey.getPortNumber(),
524 macLearnerKey.getVlanId());
525 return MacDeleteResult.NOT_EXIST;
526 }
527 }
528
529 private class InternalDeviceListener implements DeviceListener {
530
531 @Override
532 public void event(DeviceEvent event) {
533 eventExecutor.execute(() -> {
534 switch (event.type()) {
535 case DEVICE_REMOVED:
536 deleteMacMappings(event.subject().id());
537 break;
538 case PORT_REMOVED:
539 deleteMacMappings(event.subject().id(), event.port().number());
540 break;
541 default:
542 log.debug("Unhandled device event for Mac Learner: {}", event.type());
543 }
544 });
545 }
546
547 @Override
548 public boolean isRelevant(DeviceEvent event) {
549 return mastershipService.isLocalMaster(event.subject().id());
550 }
551
552 }
553
554}