Committed by
Gerrit Code Review
CORD-305 Added basic VTN rules for VMs with openstackswitching
Change-Id: I3eebc3c396b6657457363c183ca8c260b6bb8db4
Showing
4 changed files
with
709 additions
and
8 deletions
... | @@ -33,6 +33,10 @@ | ... | @@ -33,6 +33,10 @@ |
33 | 33 | ||
34 | <properties> | 34 | <properties> |
35 | <onos.app.name>org.onosproject.cordvtn</onos.app.name> | 35 | <onos.app.name>org.onosproject.cordvtn</onos.app.name> |
36 | + <onos.app.requires> | ||
37 | + org.onosproject.ovsdb, | ||
38 | + org.onosproject.openstackswitching | ||
39 | + </onos.app.requires> | ||
36 | </properties> | 40 | </properties> |
37 | 41 | ||
38 | <dependencies> | 42 | <dependencies> |
... | @@ -64,6 +68,11 @@ | ... | @@ -64,6 +68,11 @@ |
64 | <artifactId>org.apache.karaf.shell.console</artifactId> | 68 | <artifactId>org.apache.karaf.shell.console</artifactId> |
65 | <version>3.0.3</version> | 69 | <version>3.0.3</version> |
66 | </dependency> | 70 | </dependency> |
71 | + <dependency> | ||
72 | + <groupId>org.onosproject</groupId> | ||
73 | + <artifactId>onos-app-openstackswitching-api</artifactId> | ||
74 | + <version>${project.version}</version> | ||
75 | + </dependency> | ||
67 | </dependencies> | 76 | </dependencies> |
68 | 77 | ||
69 | </project> | 78 | </project> | ... | ... |
... | @@ -15,6 +15,8 @@ | ... | @@ -15,6 +15,8 @@ |
15 | */ | 15 | */ |
16 | package org.onosproject.cordvtn; | 16 | package org.onosproject.cordvtn; |
17 | 17 | ||
18 | +import com.google.common.collect.Lists; | ||
19 | +import com.google.common.collect.Maps; | ||
18 | import com.google.common.collect.Sets; | 20 | import com.google.common.collect.Sets; |
19 | import org.apache.felix.scr.annotations.Activate; | 21 | import org.apache.felix.scr.annotations.Activate; |
20 | import org.apache.felix.scr.annotations.Component; | 22 | import org.apache.felix.scr.annotations.Component; |
... | @@ -23,6 +25,7 @@ import org.apache.felix.scr.annotations.Reference; | ... | @@ -23,6 +25,7 @@ import org.apache.felix.scr.annotations.Reference; |
23 | import org.apache.felix.scr.annotations.ReferenceCardinality; | 25 | import org.apache.felix.scr.annotations.ReferenceCardinality; |
24 | import org.apache.felix.scr.annotations.Service; | 26 | import org.apache.felix.scr.annotations.Service; |
25 | import org.onlab.util.ItemNotFoundException; | 27 | import org.onlab.util.ItemNotFoundException; |
28 | +import org.onlab.packet.IpAddress; | ||
26 | import org.onlab.util.KryoNamespace; | 29 | import org.onlab.util.KryoNamespace; |
27 | import org.onosproject.cluster.ClusterService; | 30 | import org.onosproject.cluster.ClusterService; |
28 | import org.onosproject.core.ApplicationId; | 31 | import org.onosproject.core.ApplicationId; |
... | @@ -31,9 +34,11 @@ import org.onosproject.net.DefaultAnnotations; | ... | @@ -31,9 +34,11 @@ import org.onosproject.net.DefaultAnnotations; |
31 | import org.onosproject.net.Device; | 34 | import org.onosproject.net.Device; |
32 | import org.onosproject.net.DeviceId; | 35 | import org.onosproject.net.DeviceId; |
33 | import org.onosproject.net.Host; | 36 | import org.onosproject.net.Host; |
37 | +import org.onosproject.net.HostId; | ||
34 | import org.onosproject.net.Port; | 38 | import org.onosproject.net.Port; |
35 | import org.onosproject.net.behaviour.BridgeConfig; | 39 | import org.onosproject.net.behaviour.BridgeConfig; |
36 | import org.onosproject.net.behaviour.BridgeName; | 40 | import org.onosproject.net.behaviour.BridgeName; |
41 | +import org.onosproject.net.ConnectPoint; | ||
37 | import org.onosproject.net.behaviour.ControllerInfo; | 42 | import org.onosproject.net.behaviour.ControllerInfo; |
38 | import org.onosproject.net.behaviour.DefaultTunnelDescription; | 43 | import org.onosproject.net.behaviour.DefaultTunnelDescription; |
39 | import org.onosproject.net.behaviour.TunnelConfig; | 44 | import org.onosproject.net.behaviour.TunnelConfig; |
... | @@ -45,9 +50,13 @@ import org.onosproject.net.device.DeviceListener; | ... | @@ -45,9 +50,13 @@ import org.onosproject.net.device.DeviceListener; |
45 | import org.onosproject.net.device.DeviceService; | 50 | import org.onosproject.net.device.DeviceService; |
46 | import org.onosproject.net.driver.DriverHandler; | 51 | import org.onosproject.net.driver.DriverHandler; |
47 | import org.onosproject.net.driver.DriverService; | 52 | import org.onosproject.net.driver.DriverService; |
53 | +import org.onosproject.net.flowobjective.FlowObjectiveService; | ||
48 | import org.onosproject.net.host.HostEvent; | 54 | import org.onosproject.net.host.HostEvent; |
49 | import org.onosproject.net.host.HostListener; | 55 | import org.onosproject.net.host.HostListener; |
50 | import org.onosproject.net.host.HostService; | 56 | import org.onosproject.net.host.HostService; |
57 | +import org.onosproject.openstackswitching.OpenstackNetwork; | ||
58 | +import org.onosproject.openstackswitching.OpenstackPort; | ||
59 | +import org.onosproject.openstackswitching.OpenstackSwitchingService; | ||
51 | import org.onosproject.ovsdb.controller.OvsdbClientService; | 60 | import org.onosproject.ovsdb.controller.OvsdbClientService; |
52 | import org.onosproject.ovsdb.controller.OvsdbController; | 61 | import org.onosproject.ovsdb.controller.OvsdbController; |
53 | import org.onosproject.ovsdb.controller.OvsdbNodeId; | 62 | import org.onosproject.ovsdb.controller.OvsdbNodeId; |
... | @@ -62,8 +71,10 @@ import java.util.HashMap; | ... | @@ -62,8 +71,10 @@ import java.util.HashMap; |
62 | import java.util.List; | 71 | import java.util.List; |
63 | import java.util.Map; | 72 | import java.util.Map; |
64 | import java.util.NoSuchElementException; | 73 | import java.util.NoSuchElementException; |
74 | +import java.util.Set; | ||
65 | import java.util.concurrent.ExecutorService; | 75 | import java.util.concurrent.ExecutorService; |
66 | import java.util.concurrent.Executors; | 76 | import java.util.concurrent.Executors; |
77 | +import java.util.stream.Collectors; | ||
67 | 78 | ||
68 | import static com.google.common.base.Preconditions.checkNotNull; | 79 | import static com.google.common.base.Preconditions.checkNotNull; |
69 | import static org.onlab.util.Tools.groupedThreads; | 80 | import static org.onlab.util.Tools.groupedThreads; |
... | @@ -72,8 +83,8 @@ import static org.onosproject.net.behaviour.TunnelDescription.Type.VXLAN; | ... | @@ -72,8 +83,8 @@ import static org.onosproject.net.behaviour.TunnelDescription.Type.VXLAN; |
72 | import static org.slf4j.LoggerFactory.getLogger; | 83 | import static org.slf4j.LoggerFactory.getLogger; |
73 | 84 | ||
74 | /** | 85 | /** |
75 | - * Provides initial setup or cleanup for provisioning virtual tenant networks | 86 | + * Provisions virtual tenant networks with service chaining capability |
76 | - * on ovsdb, integration bridge and vm when they are added or deleted. | 87 | + * in OpenStack environment. |
77 | */ | 88 | */ |
78 | @Component(immediate = true) | 89 | @Component(immediate = true) |
79 | @Service | 90 | @Service |
... | @@ -86,7 +97,8 @@ public class CordVtn implements CordVtnService { | ... | @@ -86,7 +97,8 @@ public class CordVtn implements CordVtnService { |
86 | .register(KryoNamespaces.API) | 97 | .register(KryoNamespaces.API) |
87 | .register(CordVtnNode.class) | 98 | .register(CordVtnNode.class) |
88 | .register(NodeState.class); | 99 | .register(NodeState.class); |
89 | - private static final String DEFAULT_BRIDGE_NAME = "br-int"; | 100 | + private static final String DEFAULT_BRIDGE = "br-int"; |
101 | + private static final String VPORT_PREFIX = "tap"; | ||
90 | private static final String DEFAULT_TUNNEL = "vxlan"; | 102 | private static final String DEFAULT_TUNNEL = "vxlan"; |
91 | private static final Map<String, String> DEFAULT_TUNNEL_OPTIONS = new HashMap<String, String>() { | 103 | private static final Map<String, String> DEFAULT_TUNNEL_OPTIONS = new HashMap<String, String>() { |
92 | { | 104 | { |
... | @@ -116,11 +128,17 @@ public class CordVtn implements CordVtnService { | ... | @@ -116,11 +128,17 @@ public class CordVtn implements CordVtnService { |
116 | protected DeviceAdminService adminService; | 128 | protected DeviceAdminService adminService; |
117 | 129 | ||
118 | @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) | 130 | @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) |
131 | + protected FlowObjectiveService flowObjectiveService; | ||
132 | + | ||
133 | + @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) | ||
119 | protected OvsdbController controller; | 134 | protected OvsdbController controller; |
120 | 135 | ||
121 | @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) | 136 | @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) |
122 | protected ClusterService clusterService; | 137 | protected ClusterService clusterService; |
123 | 138 | ||
139 | + @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) | ||
140 | + protected OpenstackSwitchingService openstackService; | ||
141 | + | ||
124 | private final ExecutorService eventExecutor = Executors | 142 | private final ExecutorService eventExecutor = Executors |
125 | .newFixedThreadPool(NUM_THREADS, groupedThreads("onos/cordvtn", "event-handler")); | 143 | .newFixedThreadPool(NUM_THREADS, groupedThreads("onos/cordvtn", "event-handler")); |
126 | 144 | ||
... | @@ -132,6 +150,8 @@ public class CordVtn implements CordVtnService { | ... | @@ -132,6 +150,8 @@ public class CordVtn implements CordVtnService { |
132 | private final VmHandler vmHandler = new VmHandler(); | 150 | private final VmHandler vmHandler = new VmHandler(); |
133 | 151 | ||
134 | private ConsistentMap<CordVtnNode, NodeState> nodeStore; | 152 | private ConsistentMap<CordVtnNode, NodeState> nodeStore; |
153 | + private Map<HostId, String> hostNetworkMap = Maps.newHashMap(); | ||
154 | + private CordVtnRuleInstaller ruleInstaller; | ||
135 | 155 | ||
136 | private enum NodeState { | 156 | private enum NodeState { |
137 | 157 | ||
... | @@ -185,6 +205,8 @@ public class CordVtn implements CordVtnService { | ... | @@ -185,6 +205,8 @@ public class CordVtn implements CordVtnService { |
185 | .withApplicationId(appId) | 205 | .withApplicationId(appId) |
186 | .build(); | 206 | .build(); |
187 | 207 | ||
208 | + ruleInstaller = new CordVtnRuleInstaller(appId, flowObjectiveService, | ||
209 | + driverService, DEFAULT_TUNNEL); | ||
188 | deviceService.addListener(deviceListener); | 210 | deviceService.addListener(deviceListener); |
189 | hostService.addListener(hostListener); | 211 | hostService.addListener(hostListener); |
190 | 212 | ||
... | @@ -314,11 +336,27 @@ public class CordVtn implements CordVtnService { | ... | @@ -314,11 +336,27 @@ public class CordVtn implements CordVtnService { |
314 | 336 | ||
315 | /** | 337 | /** |
316 | * Performs tasks after node initialization. | 338 | * Performs tasks after node initialization. |
339 | + * First disconnect unnecessary OVSDB connection and then installs flow rules | ||
340 | + * for existing VMs if there are any. | ||
317 | * | 341 | * |
318 | * @param node cordvtn node | 342 | * @param node cordvtn node |
319 | */ | 343 | */ |
320 | private void postInit(CordVtnNode node) { | 344 | private void postInit(CordVtnNode node) { |
321 | disconnect(node); | 345 | disconnect(node); |
346 | + | ||
347 | + Set<OpenstackNetwork> vNets = Sets.newHashSet(); | ||
348 | + hostService.getConnectedHosts(node.intBrId()) | ||
349 | + .stream() | ||
350 | + .forEach(host -> { | ||
351 | + OpenstackNetwork vNet = getOpenstackNetworkByHost(host); | ||
352 | + if (vNet != null) { | ||
353 | + log.info("VM {} is detected", host.id()); | ||
354 | + | ||
355 | + hostNetworkMap.put(host.id(), vNet.id()); | ||
356 | + vNets.add(vNet); | ||
357 | + } | ||
358 | + }); | ||
359 | + vNets.stream().forEach(this::installFlowRules); | ||
322 | } | 360 | } |
323 | 361 | ||
324 | /** | 362 | /** |
... | @@ -443,7 +481,7 @@ public class CordVtn implements CordVtnService { | ... | @@ -443,7 +481,7 @@ public class CordVtn implements CordVtnService { |
443 | } | 481 | } |
444 | 482 | ||
445 | List<ControllerInfo> controllers = new ArrayList<>(); | 483 | List<ControllerInfo> controllers = new ArrayList<>(); |
446 | - Sets.newHashSet(clusterService.getNodes()) | 484 | + Sets.newHashSet(clusterService.getNodes()).stream() |
447 | .forEach(controller -> { | 485 | .forEach(controller -> { |
448 | ControllerInfo ctrlInfo = new ControllerInfo(controller.ip(), OFPORT, "tcp"); | 486 | ControllerInfo ctrlInfo = new ControllerInfo(controller.ip(), OFPORT, "tcp"); |
449 | controllers.add(ctrlInfo); | 487 | controllers.add(ctrlInfo); |
... | @@ -453,7 +491,7 @@ public class CordVtn implements CordVtnService { | ... | @@ -453,7 +491,7 @@ public class CordVtn implements CordVtnService { |
453 | try { | 491 | try { |
454 | DriverHandler handler = driverService.createHandler(node.ovsdbId()); | 492 | DriverHandler handler = driverService.createHandler(node.ovsdbId()); |
455 | BridgeConfig bridgeConfig = handler.behaviour(BridgeConfig.class); | 493 | BridgeConfig bridgeConfig = handler.behaviour(BridgeConfig.class); |
456 | - bridgeConfig.addBridge(BridgeName.bridgeName(DEFAULT_BRIDGE_NAME), dpid, controllers); | 494 | + bridgeConfig.addBridge(BridgeName.bridgeName(DEFAULT_BRIDGE), dpid, controllers); |
457 | } catch (ItemNotFoundException e) { | 495 | } catch (ItemNotFoundException e) { |
458 | log.warn("Failed to create integration bridge on {}", node.ovsdbId()); | 496 | log.warn("Failed to create integration bridge on {}", node.ovsdbId()); |
459 | } | 497 | } |
... | @@ -474,13 +512,12 @@ public class CordVtn implements CordVtnService { | ... | @@ -474,13 +512,12 @@ public class CordVtn implements CordVtnService { |
474 | optionBuilder.set(key, DEFAULT_TUNNEL_OPTIONS.get(key)); | 512 | optionBuilder.set(key, DEFAULT_TUNNEL_OPTIONS.get(key)); |
475 | } | 513 | } |
476 | TunnelDescription description = | 514 | TunnelDescription description = |
477 | - new DefaultTunnelDescription(null, null, VXLAN, | 515 | + new DefaultTunnelDescription(null, null, VXLAN, TunnelName.tunnelName(DEFAULT_TUNNEL), |
478 | - TunnelName.tunnelName(DEFAULT_TUNNEL), | ||
479 | optionBuilder.build()); | 516 | optionBuilder.build()); |
480 | try { | 517 | try { |
481 | DriverHandler handler = driverService.createHandler(node.ovsdbId()); | 518 | DriverHandler handler = driverService.createHandler(node.ovsdbId()); |
482 | TunnelConfig tunnelConfig = handler.behaviour(TunnelConfig.class); | 519 | TunnelConfig tunnelConfig = handler.behaviour(TunnelConfig.class); |
483 | - tunnelConfig.createTunnelInterface(BridgeName.bridgeName(DEFAULT_BRIDGE_NAME), description); | 520 | + tunnelConfig.createTunnelInterface(BridgeName.bridgeName(DEFAULT_BRIDGE), description); |
484 | } catch (ItemNotFoundException e) { | 521 | } catch (ItemNotFoundException e) { |
485 | log.warn("Failed to create tunnel interface on {}", node.ovsdbId()); | 522 | log.warn("Failed to create tunnel interface on {}", node.ovsdbId()); |
486 | } | 523 | } |
... | @@ -516,6 +553,212 @@ public class CordVtn implements CordVtnService { | ... | @@ -516,6 +553,212 @@ public class CordVtn implements CordVtnService { |
516 | } | 553 | } |
517 | } | 554 | } |
518 | 555 | ||
556 | + /** | ||
557 | + * Returns tunnel port of the device. | ||
558 | + * | ||
559 | + * @param bridgeId device id | ||
560 | + * @return port, null if no tunnel port exists on a given device | ||
561 | + */ | ||
562 | + private Port getTunnelPort(DeviceId bridgeId) { | ||
563 | + try { | ||
564 | + return deviceService.getPorts(bridgeId).stream() | ||
565 | + .filter(p -> p.annotations().value("portName").contains(DEFAULT_TUNNEL) | ||
566 | + && p.isEnabled()) | ||
567 | + .findFirst().get(); | ||
568 | + } catch (NoSuchElementException e) { | ||
569 | + return null; | ||
570 | + } | ||
571 | + } | ||
572 | + | ||
573 | + /** | ||
574 | + * Returns remote ip address for tunneling. | ||
575 | + * | ||
576 | + * @param bridgeId device id | ||
577 | + * @return ip address, null if no such device exists | ||
578 | + */ | ||
579 | + private IpAddress getRemoteIp(DeviceId bridgeId) { | ||
580 | + CordVtnNode node = getNodeByBridgeId(bridgeId); | ||
581 | + if (node != null) { | ||
582 | + // TODO get data plane IP for tunneling | ||
583 | + return node.ovsdbIp(); | ||
584 | + } else { | ||
585 | + return null; | ||
586 | + } | ||
587 | + } | ||
588 | + | ||
589 | + /** | ||
590 | + * Returns destination information of all ports associated with a given | ||
591 | + * OpenStack network. Output of the destination information is set to local | ||
592 | + * port or tunnel port according to a given device id. | ||
593 | + * | ||
594 | + * @param deviceId device id to install flow rules | ||
595 | + * @param vNet OpenStack network | ||
596 | + * @return list of flow information, empty list if no flow information exists | ||
597 | + */ | ||
598 | + private List<DestinationInfo> getSameNetworkPortsInfo(DeviceId deviceId, OpenstackNetwork vNet) { | ||
599 | + List<DestinationInfo> dstInfos = Lists.newArrayList(); | ||
600 | + long tunnelId = Long.valueOf(vNet.segmentId()); | ||
601 | + | ||
602 | + for (OpenstackPort vPort : openstackService.ports(vNet.id())) { | ||
603 | + ConnectPoint cp = getConnectPoint(vPort); | ||
604 | + if (cp == null) { | ||
605 | + log.debug("Couldn't find connection point for OpenStack port {}", vPort.id()); | ||
606 | + continue; | ||
607 | + } | ||
608 | + | ||
609 | + DestinationInfo.Builder dBuilder = cp.deviceId().equals(deviceId) ? | ||
610 | + DestinationInfo.builder(deviceService.getPort(cp.deviceId(), cp.port())) : | ||
611 | + DestinationInfo.builder(getTunnelPort(deviceId)) | ||
612 | + .setRemoteIp(getRemoteIp(cp.deviceId())); | ||
613 | + | ||
614 | + dBuilder.setMac(vPort.macAddress()) | ||
615 | + .setTunnelId(tunnelId); | ||
616 | + dstInfos.add(dBuilder.build()); | ||
617 | + } | ||
618 | + return dstInfos; | ||
619 | + } | ||
620 | + | ||
621 | + /** | ||
622 | + * Returns local ports associated with a given OpenStack network. | ||
623 | + * | ||
624 | + * @param bridgeId device id | ||
625 | + * @param vNet OpenStack network | ||
626 | + * @return port list, empty list if no port exists | ||
627 | + */ | ||
628 | + private List<Port> getLocalSameNetworkPorts(DeviceId bridgeId, OpenstackNetwork vNet) { | ||
629 | + List<Port> ports = new ArrayList<>(); | ||
630 | + openstackService.ports(vNet.id()).stream().forEach(port -> { | ||
631 | + ConnectPoint cp = getConnectPoint(port); | ||
632 | + if (cp != null && cp.deviceId().equals(bridgeId)) { | ||
633 | + ports.add(deviceService.getPort(cp.deviceId(), cp.port())); | ||
634 | + } | ||
635 | + }); | ||
636 | + return ports; | ||
637 | + } | ||
638 | + | ||
639 | + /** | ||
640 | + * Returns OpenStack port associated with a given host. | ||
641 | + * | ||
642 | + * @param host host | ||
643 | + * @return OpenStack port, or null if no port has been found | ||
644 | + */ | ||
645 | + private OpenstackPort getOpenstackPortByHost(Host host) { | ||
646 | + Port port = deviceService.getPort(host.location().deviceId(), | ||
647 | + host.location().port()); | ||
648 | + return openstackService.port(port); | ||
649 | + } | ||
650 | + | ||
651 | + /** | ||
652 | + * Returns OpenStack network associated with a given host. | ||
653 | + * | ||
654 | + * @param host host | ||
655 | + * @return OpenStack network, or null if no network has been found | ||
656 | + */ | ||
657 | + private OpenstackNetwork getOpenstackNetworkByHost(Host host) { | ||
658 | + OpenstackPort vPort = getOpenstackPortByHost(host); | ||
659 | + if (vPort != null) { | ||
660 | + return openstackService.network(vPort.networkId()); | ||
661 | + } else { | ||
662 | + return null; | ||
663 | + } | ||
664 | + } | ||
665 | + | ||
666 | + /** | ||
667 | + * Returns port name with OpenStack port information. | ||
668 | + * | ||
669 | + * @param vPort OpenStack port | ||
670 | + * @return port name | ||
671 | + */ | ||
672 | + private String getPortName(OpenstackPort vPort) { | ||
673 | + checkNotNull(vPort); | ||
674 | + return VPORT_PREFIX + vPort.id().substring(0, 10); | ||
675 | + } | ||
676 | + | ||
677 | + /** | ||
678 | + * Returns connect point of a given OpenStack port. | ||
679 | + * It assumes there's only one physical port associated with an OpenStack port. | ||
680 | + * | ||
681 | + * @param vPort openstack port | ||
682 | + * @return connect point, null if no such port exists | ||
683 | + */ | ||
684 | + private ConnectPoint getConnectPoint(OpenstackPort vPort) { | ||
685 | + try { | ||
686 | + Host host = hostService.getHostsByMac(vPort.macAddress()) | ||
687 | + .stream() | ||
688 | + .findFirst() | ||
689 | + .get(); | ||
690 | + return new ConnectPoint(host.location().deviceId(), host.location().port()); | ||
691 | + } catch (NoSuchElementException e) { | ||
692 | + log.debug("Not a valid host with {}", vPort.macAddress()); | ||
693 | + return null; | ||
694 | + } | ||
695 | + } | ||
696 | + | ||
697 | + /** | ||
698 | + * Installs flow rules for a given OpenStack network. | ||
699 | + * | ||
700 | + * @param vNet OpenStack network | ||
701 | + */ | ||
702 | + private void installFlowRules(OpenstackNetwork vNet) { | ||
703 | + checkNotNull(vNet, "Tenant network should not be null"); | ||
704 | + | ||
705 | + for (Device device : deviceService.getAvailableDevices(SWITCH)) { | ||
706 | + List<DestinationInfo> dstInfos = getSameNetworkPortsInfo(device.id(), vNet); | ||
707 | + | ||
708 | + for (Port inPort : getLocalSameNetworkPorts(device.id(), vNet)) { | ||
709 | + List<DestinationInfo> localInInfos = dstInfos.stream() | ||
710 | + .filter(info -> !info.output().equals(inPort)) | ||
711 | + .collect(Collectors.toList()); | ||
712 | + ruleInstaller.installFlowRulesLocalIn(device.id(), inPort, localInInfos); | ||
713 | + } | ||
714 | + | ||
715 | + Port tunPort = getTunnelPort(device.id()); | ||
716 | + List<DestinationInfo> tunnelInInfos = dstInfos.stream() | ||
717 | + .filter(info -> !info.output().equals(tunPort)) | ||
718 | + .collect(Collectors.toList()); | ||
719 | + ruleInstaller.installFlowRulesTunnelIn(device.id(), tunPort, tunnelInInfos); | ||
720 | + } | ||
721 | + } | ||
722 | + | ||
723 | + /** | ||
724 | + * Uninstalls flow rules associated with a given host for a given OpenStack network. | ||
725 | + * | ||
726 | + * @param vNet OpenStack network | ||
727 | + * @param host removed host | ||
728 | + */ | ||
729 | + private void uninstallFlowRules(OpenstackNetwork vNet, Host host) { | ||
730 | + checkNotNull(vNet, "Tenant network should not be null"); | ||
731 | + | ||
732 | + Port removedPort = deviceService.getPort(host.location().deviceId(), | ||
733 | + host.location().port()); | ||
734 | + | ||
735 | + for (Device device : deviceService.getAvailableDevices(SWITCH)) { | ||
736 | + List<DestinationInfo> dstInfos = getSameNetworkPortsInfo(device.id(), vNet); | ||
737 | + | ||
738 | + for (Port inPort : getLocalSameNetworkPorts(device.id(), vNet)) { | ||
739 | + List<DestinationInfo> localInInfos = Lists.newArrayList( | ||
740 | + DestinationInfo.builder(getTunnelPort(device.id())) | ||
741 | + .setTunnelId(Long.valueOf(vNet.segmentId())) | ||
742 | + .setMac(host.mac()) | ||
743 | + .setRemoteIp(getRemoteIp(host.location().deviceId())) | ||
744 | + .build()); | ||
745 | + ruleInstaller.uninstallFlowRules(device.id(), inPort, localInInfos); | ||
746 | + } | ||
747 | + | ||
748 | + if (device.id().equals(host.location().deviceId())) { | ||
749 | + Port tunPort = getTunnelPort(device.id()); | ||
750 | + List<DestinationInfo> tunnelInInfo = Lists.newArrayList( | ||
751 | + DestinationInfo.builder(removedPort) | ||
752 | + .setTunnelId(Long.valueOf(vNet.segmentId())) | ||
753 | + .setMac(host.mac()) | ||
754 | + .build()); | ||
755 | + | ||
756 | + ruleInstaller.uninstallFlowRules(device.id(), tunPort, tunnelInInfo); | ||
757 | + ruleInstaller.uninstallFlowRules(device.id(), removedPort, dstInfos); | ||
758 | + } | ||
759 | + } | ||
760 | + } | ||
761 | + | ||
519 | private class InternalDeviceListener implements DeviceListener { | 762 | private class InternalDeviceListener implements DeviceListener { |
520 | 763 | ||
521 | @Override | 764 | @Override |
... | @@ -644,12 +887,40 @@ public class CordVtn implements CordVtnService { | ... | @@ -644,12 +887,40 @@ public class CordVtn implements CordVtnService { |
644 | 887 | ||
645 | @Override | 888 | @Override |
646 | public void connected(Host host) { | 889 | public void connected(Host host) { |
890 | + CordVtnNode node = getNodeByBridgeId(host.location().deviceId()); | ||
891 | + if (node == null || !getNodeState(node).equals(NodeState.COMPLETE)) { | ||
892 | + // do nothing for the host on unregistered or unprepared device | ||
893 | + return; | ||
894 | + } | ||
895 | + | ||
896 | + OpenstackNetwork vNet = getOpenstackNetworkByHost(host); | ||
897 | + if (vNet == null) { | ||
898 | + return; | ||
899 | + } | ||
900 | + | ||
647 | log.info("VM {} is detected", host.id()); | 901 | log.info("VM {} is detected", host.id()); |
902 | + | ||
903 | + hostNetworkMap.put(host.id(), vNet.id()); | ||
904 | + installFlowRules(vNet); | ||
648 | } | 905 | } |
649 | 906 | ||
650 | @Override | 907 | @Override |
651 | public void disconnected(Host host) { | 908 | public void disconnected(Host host) { |
909 | + CordVtnNode node = getNodeByBridgeId(host.location().deviceId()); | ||
910 | + if (node == null || !getNodeState(node).equals(NodeState.COMPLETE)) { | ||
911 | + // do nothing for the host on unregistered or unprepared device | ||
912 | + return; | ||
913 | + } | ||
914 | + | ||
915 | + OpenstackNetwork vNet = openstackService.network(hostNetworkMap.get(host.id())); | ||
916 | + if (vNet == null) { | ||
917 | + return; | ||
918 | + } | ||
919 | + | ||
652 | log.info("VM {} is vanished", host.id()); | 920 | log.info("VM {} is vanished", host.id()); |
921 | + | ||
922 | + uninstallFlowRules(vNet, host); | ||
923 | + hostNetworkMap.remove(host.id()); | ||
653 | } | 924 | } |
654 | } | 925 | } |
655 | } | 926 | } | ... | ... |
1 | +/* | ||
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 | + */ | ||
16 | +package org.onosproject.cordvtn; | ||
17 | + | ||
18 | +import org.onlab.packet.Ip4Address; | ||
19 | +import org.onlab.util.ItemNotFoundException; | ||
20 | +import org.onosproject.core.ApplicationId; | ||
21 | +import org.onosproject.net.DeviceId; | ||
22 | +import org.onosproject.net.Port; | ||
23 | +import org.onosproject.net.behaviour.ExtensionTreatmentResolver; | ||
24 | +import org.onosproject.net.driver.DefaultDriverData; | ||
25 | +import org.onosproject.net.driver.DefaultDriverHandler; | ||
26 | +import org.onosproject.net.driver.Driver; | ||
27 | +import org.onosproject.net.driver.DriverHandler; | ||
28 | +import org.onosproject.net.driver.DriverService; | ||
29 | +import org.onosproject.net.flow.DefaultTrafficSelector; | ||
30 | +import org.onosproject.net.flow.DefaultTrafficTreatment; | ||
31 | +import org.onosproject.net.flow.TrafficSelector; | ||
32 | +import org.onosproject.net.flow.TrafficTreatment; | ||
33 | +import org.onosproject.net.flow.instructions.ExtensionPropertyException; | ||
34 | +import org.onosproject.net.flow.instructions.ExtensionTreatment; | ||
35 | +import org.onosproject.net.flowobjective.DefaultForwardingObjective; | ||
36 | +import org.onosproject.net.flowobjective.FlowObjectiveService; | ||
37 | +import org.onosproject.net.flowobjective.ForwardingObjective; | ||
38 | +import org.slf4j.Logger; | ||
39 | + | ||
40 | +import java.util.List; | ||
41 | + | ||
42 | +import static com.google.common.base.Preconditions.checkArgument; | ||
43 | +import static com.google.common.base.Preconditions.checkNotNull; | ||
44 | +import static org.onosproject.net.flow.instructions.ExtensionTreatmentType.ExtensionTreatmentTypes.NICIRA_SET_TUNNEL_DST; | ||
45 | +import static org.slf4j.LoggerFactory.getLogger; | ||
46 | + | ||
47 | +/** | ||
48 | + * Populates rules for virtual tenant network. | ||
49 | + */ | ||
50 | +public final class CordVtnRuleInstaller { | ||
51 | + protected final Logger log = getLogger(getClass()); | ||
52 | + | ||
53 | + private static final int DEFAULT_PRIORITY = 5000; | ||
54 | + | ||
55 | + private final ApplicationId appId; | ||
56 | + private final FlowObjectiveService flowObjectiveService; | ||
57 | + private final DriverService driverService; | ||
58 | + private final String tunnelType; | ||
59 | + | ||
60 | + /** | ||
61 | + * Creates a new rule installer. | ||
62 | + * | ||
63 | + * @param appId application id | ||
64 | + * @param flowObjectiveService flow objective service | ||
65 | + * @param driverService driver service | ||
66 | + * @param tunnelType tunnel type | ||
67 | + */ | ||
68 | + public CordVtnRuleInstaller(ApplicationId appId, | ||
69 | + FlowObjectiveService flowObjectiveService, | ||
70 | + DriverService driverService, | ||
71 | + String tunnelType) { | ||
72 | + this.appId = appId; | ||
73 | + this.flowObjectiveService = flowObjectiveService; | ||
74 | + this.driverService = driverService; | ||
75 | + this.tunnelType = checkNotNull(tunnelType); | ||
76 | + } | ||
77 | + | ||
78 | + /** | ||
79 | + * Installs flow rules for tunnel in traffic. | ||
80 | + * | ||
81 | + * @param deviceId device id to install flow rules | ||
82 | + * @param inPort in port | ||
83 | + * @param dstInfos list of destination info | ||
84 | + */ | ||
85 | + public void installFlowRulesTunnelIn(DeviceId deviceId, Port inPort, List<DestinationInfo> dstInfos) { | ||
86 | + dstInfos.stream().forEach(dstInfo -> { | ||
87 | + ForwardingObjective.Builder fBuilder = vtnRulesSameNode(inPort, dstInfo); | ||
88 | + if (fBuilder != null) { | ||
89 | + flowObjectiveService.forward(deviceId, fBuilder.add()); | ||
90 | + } | ||
91 | + }); | ||
92 | + } | ||
93 | + | ||
94 | + /** | ||
95 | + * Installs flow rules for local in traffic. | ||
96 | + * | ||
97 | + * @param deviceId device id to install flow rules | ||
98 | + * @param inPort in port | ||
99 | + * @param dstInfos list of destination info | ||
100 | + */ | ||
101 | + public void installFlowRulesLocalIn(DeviceId deviceId, Port inPort, List<DestinationInfo> dstInfos) { | ||
102 | + dstInfos.stream().forEach(dstInfo -> { | ||
103 | + ForwardingObjective.Builder fBuilder = isTunnelPort(dstInfo.output()) ? | ||
104 | + vtnRulesRemoteNode(deviceId, inPort, dstInfo) : vtnRulesSameNode(inPort, dstInfo); | ||
105 | + | ||
106 | + if (fBuilder != null) { | ||
107 | + flowObjectiveService.forward(deviceId, fBuilder.add()); | ||
108 | + } | ||
109 | + }); | ||
110 | + } | ||
111 | + | ||
112 | + /** | ||
113 | + * Uninstalls flow rules associated with a given port from a given device. | ||
114 | + * | ||
115 | + * @param deviceId device id | ||
116 | + * @param inPort port associated with removed host | ||
117 | + * @param dstInfos list of destination info | ||
118 | + */ | ||
119 | + public void uninstallFlowRules(DeviceId deviceId, Port inPort, List<DestinationInfo> dstInfos) { | ||
120 | + dstInfos.stream().forEach(dstInfo -> { | ||
121 | + ForwardingObjective.Builder fBuilder = isTunnelPort(dstInfo.output()) ? | ||
122 | + vtnRulesRemoteNode(deviceId, inPort, dstInfo) : vtnRulesSameNode(inPort, dstInfo); | ||
123 | + | ||
124 | + if (fBuilder != null) { | ||
125 | + flowObjectiveService.forward(deviceId, fBuilder.remove()); | ||
126 | + } | ||
127 | + }); | ||
128 | + } | ||
129 | + | ||
130 | + /** | ||
131 | + * Returns forwarding objective builder to provision basic virtual tenant network. | ||
132 | + * This method cares for the traffics whose source and destination device is the same. | ||
133 | + * | ||
134 | + * @param inPort in port | ||
135 | + * @param dstInfo destination information | ||
136 | + * @return forwarding objective builder | ||
137 | + */ | ||
138 | + private ForwardingObjective.Builder vtnRulesSameNode(Port inPort, DestinationInfo dstInfo) { | ||
139 | + checkArgument(inPort.element().id().equals(dstInfo.output().element().id())); | ||
140 | + | ||
141 | + TrafficSelector.Builder sBuilder = DefaultTrafficSelector.builder(); | ||
142 | + TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder(); | ||
143 | + | ||
144 | + sBuilder.matchInPort(inPort.number()) | ||
145 | + .matchEthDst(dstInfo.mac()); | ||
146 | + if (isTunnelPort(inPort)) { | ||
147 | + sBuilder.matchTunnelId(dstInfo.tunnelId()); | ||
148 | + } | ||
149 | + | ||
150 | + tBuilder.setOutput(dstInfo.output().number()); | ||
151 | + | ||
152 | + return DefaultForwardingObjective.builder() | ||
153 | + .withSelector(sBuilder.build()) | ||
154 | + .withTreatment(tBuilder.build()) | ||
155 | + .withPriority(DEFAULT_PRIORITY) | ||
156 | + .withFlag(ForwardingObjective.Flag.VERSATILE) | ||
157 | + .fromApp(appId) | ||
158 | + .makePermanent(); | ||
159 | + } | ||
160 | + | ||
161 | + /** | ||
162 | + * Returns forwarding objective builder to provision basic virtual tenant network. | ||
163 | + * This method cares for the traffics whose source and destination is not the same. | ||
164 | + * | ||
165 | + * @param deviceId device id to install flow rules | ||
166 | + * @param inPort in port | ||
167 | + * @param dstInfo destination information | ||
168 | + * @return forwarding objective, or null if it fails to build it | ||
169 | + */ | ||
170 | + private ForwardingObjective.Builder vtnRulesRemoteNode(DeviceId deviceId, Port inPort, DestinationInfo dstInfo) { | ||
171 | + checkArgument(isTunnelPort(dstInfo.output())); | ||
172 | + | ||
173 | + TrafficSelector.Builder sBuilder = DefaultTrafficSelector.builder(); | ||
174 | + TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder(); | ||
175 | + | ||
176 | + ExtensionTreatment extTreatment = | ||
177 | + getTunnelDstInstruction(deviceId, dstInfo.remoteIp().getIp4Address()); | ||
178 | + if (extTreatment == null) { | ||
179 | + return null; | ||
180 | + } | ||
181 | + | ||
182 | + sBuilder.matchInPort(inPort.number()) | ||
183 | + .matchEthDst(dstInfo.mac()); | ||
184 | + | ||
185 | + tBuilder.extension(extTreatment, deviceId) | ||
186 | + .setTunnelId(dstInfo.tunnelId()) | ||
187 | + .setOutput(dstInfo.output().number()); | ||
188 | + | ||
189 | + return DefaultForwardingObjective.builder() | ||
190 | + .withSelector(sBuilder.build()) | ||
191 | + .withTreatment(tBuilder.build()) | ||
192 | + .withPriority(DEFAULT_PRIORITY) | ||
193 | + .withFlag(ForwardingObjective.Flag.VERSATILE) | ||
194 | + .fromApp(appId) | ||
195 | + .makePermanent(); | ||
196 | + } | ||
197 | + | ||
198 | + /** | ||
199 | + * Checks if a given port is tunnel interface or not. | ||
200 | + * It assumes the tunnel interface contains tunnelType string in its name. | ||
201 | + * | ||
202 | + * @param port port | ||
203 | + * @return true if the port is tunnel interface, false otherwise. | ||
204 | + */ | ||
205 | + private boolean isTunnelPort(Port port) { | ||
206 | + return port.annotations().value("portName").contains(tunnelType); | ||
207 | + } | ||
208 | + | ||
209 | + /** | ||
210 | + * Returns extension instruction to set tunnel destination. | ||
211 | + * | ||
212 | + * @param deviceId device id | ||
213 | + * @param remoteIp tunnel destination address | ||
214 | + * @return extension treatment or null if it fails to get instruction | ||
215 | + */ | ||
216 | + private ExtensionTreatment getTunnelDstInstruction(DeviceId deviceId, Ip4Address remoteIp) { | ||
217 | + try { | ||
218 | + Driver driver = driverService.getDriver(deviceId); | ||
219 | + DriverHandler handler = new DefaultDriverHandler(new DefaultDriverData(driver, deviceId)); | ||
220 | + ExtensionTreatmentResolver resolver = handler.behaviour(ExtensionTreatmentResolver.class); | ||
221 | + | ||
222 | + ExtensionTreatment treatment = resolver.getExtensionInstruction(NICIRA_SET_TUNNEL_DST.type()); | ||
223 | + treatment.setPropertyValue("tunnelDst", remoteIp); | ||
224 | + | ||
225 | + return treatment; | ||
226 | + } catch (ItemNotFoundException | UnsupportedOperationException | ExtensionPropertyException e) { | ||
227 | + log.error("Failed to get extension instruction to set tunnel dst {}", deviceId); | ||
228 | + return null; | ||
229 | + } | ||
230 | + } | ||
231 | +} |
1 | +/* | ||
2 | + * Copyright 2014-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 | + */ | ||
16 | +package org.onosproject.cordvtn; | ||
17 | + | ||
18 | +import org.onlab.packet.IpAddress; | ||
19 | +import org.onlab.packet.MacAddress; | ||
20 | +import org.onosproject.net.Port; | ||
21 | + | ||
22 | +import java.util.List; | ||
23 | + | ||
24 | +import static com.google.common.base.Preconditions.checkNotNull; | ||
25 | + | ||
26 | +/** | ||
27 | + * Contains destination information. | ||
28 | + */ | ||
29 | +public final class DestinationInfo { | ||
30 | + | ||
31 | + private final Port output; | ||
32 | + private final List<IpAddress> ip; | ||
33 | + private final MacAddress mac; | ||
34 | + private final IpAddress remoteIp; | ||
35 | + private final long tunnelId; | ||
36 | + | ||
37 | + /** | ||
38 | + * Creates a new destination information. | ||
39 | + * | ||
40 | + * @param output output port | ||
41 | + * @param ip destination ip address | ||
42 | + * @param mac destination mac address | ||
43 | + * @param remoteIp tunnel remote ip address | ||
44 | + * @param tunnelId segment id | ||
45 | + */ | ||
46 | + public DestinationInfo(Port output, List<IpAddress> ip, MacAddress mac, | ||
47 | + IpAddress remoteIp, long tunnelId) { | ||
48 | + this.output = checkNotNull(output); | ||
49 | + this.ip = ip; | ||
50 | + this.mac = mac; | ||
51 | + this.remoteIp = remoteIp; | ||
52 | + this.tunnelId = tunnelId; | ||
53 | + } | ||
54 | + | ||
55 | + /** | ||
56 | + * Returns output port. | ||
57 | + * | ||
58 | + * @return port | ||
59 | + */ | ||
60 | + public Port output() { | ||
61 | + return output; | ||
62 | + } | ||
63 | + | ||
64 | + /** | ||
65 | + * Returns destination ip addresses. | ||
66 | + * | ||
67 | + * @return list of ip address | ||
68 | + */ | ||
69 | + public List<IpAddress> ip() { | ||
70 | + return ip; | ||
71 | + } | ||
72 | + | ||
73 | + /** | ||
74 | + * Returns destination mac address. | ||
75 | + * | ||
76 | + * @return mac address | ||
77 | + */ | ||
78 | + public MacAddress mac() { | ||
79 | + return mac; | ||
80 | + } | ||
81 | + | ||
82 | + /** | ||
83 | + * Returns tunnel remote ip address. | ||
84 | + * | ||
85 | + * @return ip address | ||
86 | + */ | ||
87 | + public IpAddress remoteIp() { | ||
88 | + return remoteIp; | ||
89 | + } | ||
90 | + | ||
91 | + /** | ||
92 | + * Returns tunnel id. | ||
93 | + * | ||
94 | + * @return tunnel id | ||
95 | + */ | ||
96 | + public long tunnelId() { | ||
97 | + return tunnelId; | ||
98 | + } | ||
99 | + | ||
100 | + /** | ||
101 | + * Returns a new destination info builder. | ||
102 | + * | ||
103 | + * @return destination info builder | ||
104 | + */ | ||
105 | + public static DestinationInfo.Builder builder(Port output) { | ||
106 | + return new Builder(output); | ||
107 | + } | ||
108 | + | ||
109 | + /** | ||
110 | + * DestinationInfo builder class. | ||
111 | + */ | ||
112 | + public static final class Builder { | ||
113 | + | ||
114 | + private final Port output; | ||
115 | + private List<IpAddress> ip; | ||
116 | + private MacAddress mac; | ||
117 | + private IpAddress remoteIp; | ||
118 | + private long tunnelId; | ||
119 | + | ||
120 | + /** | ||
121 | + * Creates a new destination information builder. | ||
122 | + * | ||
123 | + * @param output output port | ||
124 | + */ | ||
125 | + public Builder(Port output) { | ||
126 | + this.output = checkNotNull(output, "Output port cannot be null"); | ||
127 | + } | ||
128 | + | ||
129 | + /** | ||
130 | + * Sets the destination ip address. | ||
131 | + * | ||
132 | + * @param ip ip address | ||
133 | + * @return destination info builder | ||
134 | + */ | ||
135 | + public Builder setIp(List<IpAddress> ip) { | ||
136 | + this.ip = checkNotNull(ip, "IP cannot be null"); | ||
137 | + return this; | ||
138 | + } | ||
139 | + | ||
140 | + /** | ||
141 | + * Sets the destination mac address. | ||
142 | + * | ||
143 | + * @param mac mac address | ||
144 | + * @return destination info builder | ||
145 | + */ | ||
146 | + public Builder setMac(MacAddress mac) { | ||
147 | + this.mac = checkNotNull(mac, "MAC address cannot be null"); | ||
148 | + return this; | ||
149 | + } | ||
150 | + | ||
151 | + /** | ||
152 | + * Sets the tunnel remote ip address. | ||
153 | + * | ||
154 | + * @param remoteIp ip address | ||
155 | + * @return destination info builder | ||
156 | + */ | ||
157 | + public Builder setRemoteIp(IpAddress remoteIp) { | ||
158 | + this.remoteIp = checkNotNull(remoteIp, "Remote IP address cannot be null"); | ||
159 | + return this; | ||
160 | + } | ||
161 | + | ||
162 | + /** | ||
163 | + * Sets the tunnel id. | ||
164 | + * | ||
165 | + * @param tunnelId tunnel id | ||
166 | + * @return destination info builder | ||
167 | + */ | ||
168 | + public Builder setTunnelId(long tunnelId) { | ||
169 | + this.tunnelId = checkNotNull(tunnelId, "Tunnel ID cannot be null"); | ||
170 | + return this; | ||
171 | + } | ||
172 | + | ||
173 | + /** | ||
174 | + * Build a destination information. | ||
175 | + * | ||
176 | + * @return destination info object | ||
177 | + */ | ||
178 | + public DestinationInfo build() { | ||
179 | + return new DestinationInfo(this); | ||
180 | + } | ||
181 | + } | ||
182 | + | ||
183 | + private DestinationInfo(Builder builder) { | ||
184 | + output = builder.output; | ||
185 | + ip = builder.ip; | ||
186 | + mac = builder.mac; | ||
187 | + remoteIp = builder.remoteIp; | ||
188 | + tunnelId = builder.tunnelId; | ||
189 | + } | ||
190 | +} |
-
Please register or login to post a comment