Committed by
Gerrit Code Review
[GEANT] Add/remove-bgp-speaker/peer commands.
Change-Id: Iad8001095cc81be0c34c976adfa32ef9c7eff685
Showing
9 changed files
with
797 additions
and
1 deletions
| ... | @@ -53,6 +53,21 @@ | ... | @@ -53,6 +53,21 @@ |
| 53 | 53 | ||
| 54 | <dependency> | 54 | <dependency> |
| 55 | <groupId>org.onosproject</groupId> | 55 | <groupId>org.onosproject</groupId> |
| 56 | + <artifactId>onos-cli</artifactId> | ||
| 57 | + <version>${project.version}</version> | ||
| 58 | + <scope>test</scope> | ||
| 59 | + </dependency> | ||
| 60 | + | ||
| 61 | + <dependency> | ||
| 62 | + <groupId>org.onosproject</groupId> | ||
| 63 | + <artifactId>onos-api</artifactId> | ||
| 64 | + <version>${project.version}</version> | ||
| 65 | + <scope>test</scope> | ||
| 66 | + <classifier>tests</classifier> | ||
| 67 | + </dependency> | ||
| 68 | + | ||
| 69 | + <dependency> | ||
| 70 | + <groupId>org.onosproject</groupId> | ||
| 56 | <artifactId>onlab-junit</artifactId> | 71 | <artifactId>onlab-junit</artifactId> |
| 57 | <scope>test</scope> | 72 | <scope>test</scope> |
| 58 | </dependency> | 73 | </dependency> | ... | ... |
| ... | @@ -17,12 +17,16 @@ | ... | @@ -17,12 +17,16 @@ |
| 17 | package org.onosproject.routing.config; | 17 | package org.onosproject.routing.config; |
| 18 | 18 | ||
| 19 | import com.fasterxml.jackson.databind.JsonNode; | 19 | import com.fasterxml.jackson.databind.JsonNode; |
| 20 | +import com.fasterxml.jackson.databind.node.ArrayNode; | ||
| 21 | +import com.fasterxml.jackson.databind.node.JsonNodeFactory; | ||
| 22 | +import com.fasterxml.jackson.databind.node.ObjectNode; | ||
| 20 | import com.google.common.collect.Sets; | 23 | import com.google.common.collect.Sets; |
| 21 | import org.onlab.packet.IpAddress; | 24 | import org.onlab.packet.IpAddress; |
| 22 | import org.onosproject.core.ApplicationId; | 25 | import org.onosproject.core.ApplicationId; |
| 23 | import org.onosproject.net.ConnectPoint; | 26 | import org.onosproject.net.ConnectPoint; |
| 24 | import org.onosproject.net.config.Config; | 27 | import org.onosproject.net.config.Config; |
| 25 | 28 | ||
| 29 | +import java.util.Objects; | ||
| 26 | import java.util.Optional; | 30 | import java.util.Optional; |
| 27 | import java.util.Set; | 31 | import java.util.Set; |
| 28 | 32 | ||
| ... | @@ -49,6 +53,11 @@ public class BgpConfig extends Config<ApplicationId> { | ... | @@ -49,6 +53,11 @@ public class BgpConfig extends Config<ApplicationId> { |
| 49 | Set<BgpSpeakerConfig> speakers = Sets.newHashSet(); | 53 | Set<BgpSpeakerConfig> speakers = Sets.newHashSet(); |
| 50 | 54 | ||
| 51 | JsonNode speakersNode = object.get(SPEAKERS); | 55 | JsonNode speakersNode = object.get(SPEAKERS); |
| 56 | + | ||
| 57 | + if (speakersNode == null) { | ||
| 58 | + return speakers; | ||
| 59 | + } | ||
| 60 | + | ||
| 52 | speakersNode.forEach(jsonNode -> { | 61 | speakersNode.forEach(jsonNode -> { |
| 53 | Set<IpAddress> listenAddresses = Sets.newHashSet(); | 62 | Set<IpAddress> listenAddresses = Sets.newHashSet(); |
| 54 | jsonNode.path(PEERS).forEach(addressNode -> | 63 | jsonNode.path(PEERS).forEach(addressNode -> |
| ... | @@ -71,6 +80,130 @@ public class BgpConfig extends Config<ApplicationId> { | ... | @@ -71,6 +80,130 @@ public class BgpConfig extends Config<ApplicationId> { |
| 71 | } | 80 | } |
| 72 | 81 | ||
| 73 | /** | 82 | /** |
| 83 | + * Examines whether a name of BGP speaker exists in configuration. | ||
| 84 | + * | ||
| 85 | + * @param name name of BGP speaker being search | ||
| 86 | + * @return speaker | ||
| 87 | + */ | ||
| 88 | + public BgpSpeakerConfig getSpeakerWithName(String name) { | ||
| 89 | + for (BgpConfig.BgpSpeakerConfig speaker : bgpSpeakers()) { | ||
| 90 | + if (speaker.name().isPresent() && speaker.name().get().equals(name)) { | ||
| 91 | + return speaker; | ||
| 92 | + } | ||
| 93 | + } | ||
| 94 | + return null; | ||
| 95 | + } | ||
| 96 | + | ||
| 97 | + /** | ||
| 98 | + * Adds BGP speaker to configuration. | ||
| 99 | + * | ||
| 100 | + * @param speaker BGP speaker configuration entry | ||
| 101 | + */ | ||
| 102 | + public void addSpeaker(BgpSpeakerConfig speaker) { | ||
| 103 | + ObjectNode speakerNode = JsonNodeFactory.instance.objectNode(); | ||
| 104 | + | ||
| 105 | + speakerNode.put(NAME, speaker.name().get()); | ||
| 106 | + | ||
| 107 | + speakerNode.put(CONNECT_POINT, speaker.connectPoint().elementId().toString() | ||
| 108 | + + "/" + speaker.connectPoint().port().toString()); | ||
| 109 | + | ||
| 110 | + ArrayNode peersNode = speakerNode.putArray(PEERS); | ||
| 111 | + for (IpAddress peerAddress: speaker.peers()) { | ||
| 112 | + peersNode.add(peerAddress.toString()); | ||
| 113 | + } | ||
| 114 | + | ||
| 115 | + ArrayNode speakersArray = bgpSpeakers().isEmpty() ? | ||
| 116 | + initBgpConfiguration() : (ArrayNode) object.get(SPEAKERS); | ||
| 117 | + speakersArray.add(speakerNode); | ||
| 118 | + } | ||
| 119 | + | ||
| 120 | + /** | ||
| 121 | + * Removes BGP speaker from configuration. | ||
| 122 | + * | ||
| 123 | + * @param speakerName BGP speaker name | ||
| 124 | + */ | ||
| 125 | + public void removeSpeaker(String speakerName) { | ||
| 126 | + ArrayNode speakersArray = (ArrayNode) object.get(SPEAKERS); | ||
| 127 | + | ||
| 128 | + for (int i = 0; i < speakersArray.size(); i++) { | ||
| 129 | + if (speakersArray.get(i).hasNonNull(NAME) && | ||
| 130 | + speakersArray.get(i).get(NAME).asText().equals(speakerName)) { | ||
| 131 | + speakersArray.remove(i); | ||
| 132 | + return; | ||
| 133 | + } | ||
| 134 | + } | ||
| 135 | + } | ||
| 136 | + | ||
| 137 | + /** | ||
| 138 | + * Adds peering address to BGP speaker. | ||
| 139 | + * | ||
| 140 | + * @param speakerName name of BGP speaker | ||
| 141 | + * @param peerAddress peering address to be added | ||
| 142 | + */ | ||
| 143 | + public void addPeerToSpeaker(String speakerName, IpAddress peerAddress) { | ||
| 144 | + JsonNode speakersNode = object.get(SPEAKERS); | ||
| 145 | + speakersNode.forEach(jsonNode -> { | ||
| 146 | + if (jsonNode.hasNonNull(NAME) && | ||
| 147 | + jsonNode.get(NAME).asText().equals(speakerName)) { | ||
| 148 | + ArrayNode peersNode = (ArrayNode) jsonNode.get(PEERS); | ||
| 149 | + for (int i = 0; i < peersNode.size(); i++) { | ||
| 150 | + if (peersNode.get(i).asText().equals(peerAddress.toString())) { | ||
| 151 | + return; // Peer already exists. | ||
| 152 | + } | ||
| 153 | + } | ||
| 154 | + peersNode.add(peerAddress.toString()); | ||
| 155 | + } | ||
| 156 | + }); | ||
| 157 | + } | ||
| 158 | + | ||
| 159 | + /** | ||
| 160 | + * Finds BGP speaker peering with a given external peer. | ||
| 161 | + * | ||
| 162 | + * @param peerAddress peering address to be removed | ||
| 163 | + * @return speaker | ||
| 164 | + */ | ||
| 165 | + public BgpSpeakerConfig getSpeakerFromPeer(IpAddress peerAddress) { | ||
| 166 | + for (BgpConfig.BgpSpeakerConfig speaker : bgpSpeakers()) { | ||
| 167 | + if (speaker.peers().contains(peerAddress)) { | ||
| 168 | + return speaker; | ||
| 169 | + } | ||
| 170 | + } | ||
| 171 | + return null; | ||
| 172 | + } | ||
| 173 | + | ||
| 174 | + /** | ||
| 175 | + * Removes peering address from BGP speaker. | ||
| 176 | + * | ||
| 177 | + * @param speaker BGP speaker configuration entries | ||
| 178 | + * @param peerAddress peering address to be removed | ||
| 179 | + */ | ||
| 180 | + public void removePeerFromSpeaker(BgpSpeakerConfig speaker, IpAddress peerAddress) { | ||
| 181 | + JsonNode speakersNode = object.get(SPEAKERS); | ||
| 182 | + speakersNode.forEach(jsonNode -> { | ||
| 183 | + if (jsonNode.hasNonNull(NAME) && | ||
| 184 | + jsonNode.get(NAME).asText().equals(speaker.name().get())) { | ||
| 185 | + ArrayNode peersNode = (ArrayNode) jsonNode.get(PEERS); | ||
| 186 | + for (int i = 0; i < peersNode.size(); i++) { | ||
| 187 | + if (peersNode.get(i).asText().equals(peerAddress.toString())) { | ||
| 188 | + peersNode.remove(i); | ||
| 189 | + return; | ||
| 190 | + } | ||
| 191 | + } | ||
| 192 | + } | ||
| 193 | + }); | ||
| 194 | + } | ||
| 195 | + | ||
| 196 | + /** | ||
| 197 | + * Creates empty configuration for BGP speakers. | ||
| 198 | + * | ||
| 199 | + * @return empty array of BGP speakers | ||
| 200 | + */ | ||
| 201 | + private ArrayNode initBgpConfiguration() { | ||
| 202 | + return object.putArray(SPEAKERS); | ||
| 203 | + } | ||
| 204 | + | ||
| 205 | + | ||
| 206 | + /** | ||
| 74 | * Configuration for a BGP speaker. | 207 | * Configuration for a BGP speaker. |
| 75 | */ | 208 | */ |
| 76 | public static class BgpSpeakerConfig { | 209 | public static class BgpSpeakerConfig { |
| ... | @@ -97,5 +230,39 @@ public class BgpConfig extends Config<ApplicationId> { | ... | @@ -97,5 +230,39 @@ public class BgpConfig extends Config<ApplicationId> { |
| 97 | public Set<IpAddress> peers() { | 230 | public Set<IpAddress> peers() { |
| 98 | return peers; | 231 | return peers; |
| 99 | } | 232 | } |
| 233 | + | ||
| 234 | + /** | ||
| 235 | + * Examines if BGP peer is connected. | ||
| 236 | + * | ||
| 237 | + * @param peer IP address of peer | ||
| 238 | + * @return result of search | ||
| 239 | + */ | ||
| 240 | + public boolean isConnectedToPeer(IpAddress peer) { | ||
| 241 | + for (final IpAddress entry : peers()) { | ||
| 242 | + if (entry.equals(peer)) { | ||
| 243 | + return true; | ||
| 244 | + } | ||
| 245 | + } | ||
| 246 | + return false; | ||
| 247 | + } | ||
| 248 | + | ||
| 249 | + @Override | ||
| 250 | + public boolean equals(Object obj) { | ||
| 251 | + if (this == obj) { | ||
| 252 | + return true; | ||
| 253 | + } | ||
| 254 | + if (obj instanceof BgpSpeakerConfig) { | ||
| 255 | + final BgpSpeakerConfig that = (BgpSpeakerConfig) obj; | ||
| 256 | + return Objects.equals(this.name, that.name) && | ||
| 257 | + Objects.equals(this.connectPoint, that.connectPoint) && | ||
| 258 | + Objects.equals(this.peers, that.peers); | ||
| 259 | + } | ||
| 260 | + return false; | ||
| 261 | + } | ||
| 262 | + | ||
| 263 | + @Override | ||
| 264 | + public int hashCode() { | ||
| 265 | + return Objects.hash(name, connectPoint, peers); | ||
| 266 | + } | ||
| 100 | } | 267 | } |
| 101 | } | 268 | } | ... | ... |
| 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 | + | ||
| 17 | +package org.onosproject.routing.config; | ||
| 18 | + | ||
| 19 | +import com.fasterxml.jackson.databind.JsonNode; | ||
| 20 | +import com.fasterxml.jackson.databind.ObjectMapper; | ||
| 21 | +import org.junit.Before; | ||
| 22 | +import org.junit.Test; | ||
| 23 | +import org.onlab.packet.IpAddress; | ||
| 24 | +import org.onosproject.TestApplicationId; | ||
| 25 | +import org.onosproject.core.ApplicationId; | ||
| 26 | +import org.onosproject.net.ConnectPoint; | ||
| 27 | +import org.onosproject.net.config.Config; | ||
| 28 | +import org.onosproject.net.config.ConfigApplyDelegate; | ||
| 29 | +import org.onosproject.routing.RoutingService; | ||
| 30 | + | ||
| 31 | +import java.util.Arrays; | ||
| 32 | +import java.util.HashSet; | ||
| 33 | +import java.util.Optional; | ||
| 34 | +import java.util.Set; | ||
| 35 | + | ||
| 36 | +import static junit.framework.Assert.assertNotNull; | ||
| 37 | +import static junit.framework.Assert.assertNull; | ||
| 38 | +import static junit.framework.TestCase.assertEquals; | ||
| 39 | +import static org.junit.Assert.assertFalse; | ||
| 40 | +import static org.junit.Assert.assertTrue; | ||
| 41 | + | ||
| 42 | +public class BgpConfigTest { | ||
| 43 | + | ||
| 44 | + private static final ApplicationId APP_ID = | ||
| 45 | + new TestApplicationId(RoutingService.ROUTER_APP_ID); | ||
| 46 | + | ||
| 47 | + private static final IpAddress IP1 = IpAddress.valueOf("10.0.1.1"); | ||
| 48 | + private static final IpAddress IP2 = IpAddress.valueOf("10.0.2.1"); | ||
| 49 | + private static final IpAddress IP3 = IpAddress.valueOf("10.0.3.1"); | ||
| 50 | + private static final IpAddress IP4 = IpAddress.valueOf("10.0.101.1"); | ||
| 51 | + private static final IpAddress IP5 = IpAddress.valueOf("10.0.201.1"); | ||
| 52 | + public static final IpAddress IP_NON_EXIST = IpAddress.valueOf("10.101.1.1"); | ||
| 53 | + | ||
| 54 | + public static final ConnectPoint CONNECT_POINT1 = ConnectPoint. | ||
| 55 | + deviceConnectPoint("of:0000000000000001/1"); | ||
| 56 | + public static final ConnectPoint CONNECT_POINT2 = ConnectPoint. | ||
| 57 | + deviceConnectPoint("of:00000000000000a3/1"); | ||
| 58 | + | ||
| 59 | + private static final String JSON_TREE = "{\"" + BgpConfig.SPEAKERS + | ||
| 60 | + "\" : [{\"" + BgpConfig.NAME + "\" : \"bgp1\"," + | ||
| 61 | + "\"" + BgpConfig.CONNECT_POINT + | ||
| 62 | + "\" : \"of:0000000000000001/1\"," + | ||
| 63 | + "\"" + BgpConfig.PEERS + "\" : [" + | ||
| 64 | + "\"10.0.1.1\",\"10.0.2.1\",\"10.0.3.1\"]}]}"; | ||
| 65 | + private static final String EMPTY_JSON_TREE = "{}"; | ||
| 66 | + | ||
| 67 | + private final ObjectMapper mapper = new ObjectMapper(); | ||
| 68 | + private final ConfigApplyDelegate delegate = new MockCfgDelegate(); | ||
| 69 | + private final BgpConfig.BgpSpeakerConfig initialSpeaker = createInitialSpeaker(); | ||
| 70 | + | ||
| 71 | + private Set<BgpConfig.BgpSpeakerConfig> speakers = new HashSet<>(); | ||
| 72 | + private BgpConfig bgpConfig = new BgpConfig(); | ||
| 73 | + private BgpConfig emptyBgpConfig = new BgpConfig(); | ||
| 74 | + | ||
| 75 | + @Before | ||
| 76 | + public void setUp() throws Exception { | ||
| 77 | + JsonNode tree = new ObjectMapper().readTree(JSON_TREE); | ||
| 78 | + bgpConfig.init(APP_ID, "bgp-test", tree, mapper, delegate); | ||
| 79 | + JsonNode emptyTree = new ObjectMapper().readTree(EMPTY_JSON_TREE); | ||
| 80 | + emptyBgpConfig.init(APP_ID, "bgp-test", emptyTree, mapper, delegate); | ||
| 81 | + speakers.add(initialSpeaker); | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | + /** | ||
| 85 | + * Tests if speakers can be retrieved from JSON. | ||
| 86 | + */ | ||
| 87 | + @Test | ||
| 88 | + public void testBgpSpeakers() throws Exception { | ||
| 89 | + assertEquals(speakers, bgpConfig.bgpSpeakers()); | ||
| 90 | + } | ||
| 91 | + | ||
| 92 | + /** | ||
| 93 | + * Tests if speakers can be retrieved from empty JSON. | ||
| 94 | + */ | ||
| 95 | + @Test | ||
| 96 | + public void testEmptyBgpSpeakers() throws Exception { | ||
| 97 | + assertTrue(emptyBgpConfig.bgpSpeakers().isEmpty()); | ||
| 98 | + } | ||
| 99 | + | ||
| 100 | + /** | ||
| 101 | + * Tests if speaker can be found by name. | ||
| 102 | + */ | ||
| 103 | + @Test | ||
| 104 | + public void testGetSpeakerWithName() throws Exception { | ||
| 105 | + assertNotNull(bgpConfig.getSpeakerWithName("bgp1")); | ||
| 106 | + assertNull(bgpConfig.getSpeakerWithName("bgp2")); | ||
| 107 | + } | ||
| 108 | + | ||
| 109 | + /** | ||
| 110 | + * Tests addition of new speaker. | ||
| 111 | + */ | ||
| 112 | + @Test | ||
| 113 | + public void testAddSpeaker() throws Exception { | ||
| 114 | + int initialSize = bgpConfig.bgpSpeakers().size(); | ||
| 115 | + BgpConfig.BgpSpeakerConfig newSpeaker = createNewSpeaker(); | ||
| 116 | + bgpConfig.addSpeaker(newSpeaker); | ||
| 117 | + assertEquals(initialSize + 1, bgpConfig.bgpSpeakers().size()); | ||
| 118 | + speakers.add(newSpeaker); | ||
| 119 | + assertEquals(speakers, bgpConfig.bgpSpeakers()); | ||
| 120 | + } | ||
| 121 | + | ||
| 122 | + /** | ||
| 123 | + * Tests addition of new speaker to empty configuration. | ||
| 124 | + */ | ||
| 125 | + @Test | ||
| 126 | + public void testAddSpeakerToEmpty() throws Exception { | ||
| 127 | + BgpConfig.BgpSpeakerConfig newSpeaker = createNewSpeaker(); | ||
| 128 | + emptyBgpConfig.addSpeaker(newSpeaker); | ||
| 129 | + | ||
| 130 | + assertFalse(emptyBgpConfig.bgpSpeakers().isEmpty()); | ||
| 131 | + } | ||
| 132 | + | ||
| 133 | + /** | ||
| 134 | + * Tests removal of existing speaker. | ||
| 135 | + */ | ||
| 136 | + @Test | ||
| 137 | + public void testRemoveExistingSpeaker() throws Exception { | ||
| 138 | + int initialSize = bgpConfig.bgpSpeakers().size(); | ||
| 139 | + bgpConfig.removeSpeaker("bgp1"); | ||
| 140 | + | ||
| 141 | + assertEquals(initialSize - 1, bgpConfig.bgpSpeakers().size()); | ||
| 142 | + } | ||
| 143 | + | ||
| 144 | + /** | ||
| 145 | + * Tests removal of non-existing speaker. | ||
| 146 | + */ | ||
| 147 | + @Test | ||
| 148 | + public void testRemoveInexistingSpeaker() throws Exception { | ||
| 149 | + int initialSize = bgpConfig.bgpSpeakers().size(); | ||
| 150 | + bgpConfig.removeSpeaker("bgp2"); | ||
| 151 | + | ||
| 152 | + assertEquals(initialSize, bgpConfig.bgpSpeakers().size()); | ||
| 153 | + } | ||
| 154 | + | ||
| 155 | + /** | ||
| 156 | + * Tests addition of new speaker. | ||
| 157 | + */ | ||
| 158 | + @Test | ||
| 159 | + public void testAddPeerToSpeaker() throws Exception { | ||
| 160 | + int initialSize = bgpConfig.getSpeakerWithName("bgp1").peers().size(); | ||
| 161 | + bgpConfig.addPeerToSpeaker("bgp1", IP4); | ||
| 162 | + | ||
| 163 | + assertEquals(initialSize + 1, bgpConfig.getSpeakerWithName("bgp1").peers().size()); | ||
| 164 | + } | ||
| 165 | + | ||
| 166 | + /** | ||
| 167 | + * Tests addition of new speaker when peer already exists. | ||
| 168 | + */ | ||
| 169 | + @Test | ||
| 170 | + public void testAddExistingPeerToSpeaker() throws Exception { | ||
| 171 | + int initialSize = bgpConfig.getSpeakerWithName("bgp1").peers().size(); | ||
| 172 | + bgpConfig.addPeerToSpeaker("bgp1", IP1); | ||
| 173 | + | ||
| 174 | + assertEquals(initialSize, bgpConfig.getSpeakerWithName("bgp1").peers().size()); | ||
| 175 | + } | ||
| 176 | + | ||
| 177 | + /** | ||
| 178 | + * Tests retrieval of speaker based on peering address. | ||
| 179 | + */ | ||
| 180 | + @Test | ||
| 181 | + public void testGetSpeakerFromPeer() throws Exception { | ||
| 182 | + assertNotNull(bgpConfig.getSpeakerFromPeer(IP1)); | ||
| 183 | + assertNull(bgpConfig.getSpeakerFromPeer(IP_NON_EXIST)); | ||
| 184 | + } | ||
| 185 | + | ||
| 186 | + /** | ||
| 187 | + * Tests removal of peer. | ||
| 188 | + */ | ||
| 189 | + @Test | ||
| 190 | + public void testRemoveExistingPeerFromSpeaker() throws Exception { | ||
| 191 | + int initialSize = bgpConfig.getSpeakerWithName("bgp1").peers().size(); | ||
| 192 | + bgpConfig.removePeerFromSpeaker(initialSpeaker, IP1); | ||
| 193 | + | ||
| 194 | + assertEquals(initialSize - 1, bgpConfig.getSpeakerWithName("bgp1").peers().size()); | ||
| 195 | + } | ||
| 196 | + | ||
| 197 | + /** | ||
| 198 | + * Tests peer removal when peer does not exist. | ||
| 199 | + */ | ||
| 200 | + @Test | ||
| 201 | + public void testRemoveNonExistingPeerFromSpeaker() throws Exception { | ||
| 202 | + int initialSize = bgpConfig.getSpeakerWithName("bgp1").peers().size(); | ||
| 203 | + bgpConfig.removePeerFromSpeaker(initialSpeaker, IP_NON_EXIST); | ||
| 204 | + | ||
| 205 | + assertEquals(initialSize, bgpConfig.getSpeakerWithName("bgp1").peers().size()); | ||
| 206 | + } | ||
| 207 | + | ||
| 208 | + /** | ||
| 209 | + * Tests if connections to peers are found. | ||
| 210 | + */ | ||
| 211 | + @Test | ||
| 212 | + public void testIsConnectedToPeer() { | ||
| 213 | + BgpConfig.BgpSpeakerConfig speaker = createNewSpeaker(); | ||
| 214 | + | ||
| 215 | + assertTrue(speaker.isConnectedToPeer(IP4)); | ||
| 216 | + assertFalse(speaker.isConnectedToPeer(IP_NON_EXIST)); | ||
| 217 | + } | ||
| 218 | + | ||
| 219 | + private class MockCfgDelegate implements ConfigApplyDelegate { | ||
| 220 | + | ||
| 221 | + @Override | ||
| 222 | + public void onApply(@SuppressWarnings("rawtypes") Config config) { | ||
| 223 | + config.apply(); | ||
| 224 | + } | ||
| 225 | + | ||
| 226 | + } | ||
| 227 | + | ||
| 228 | + private BgpConfig.BgpSpeakerConfig createInitialSpeaker() { | ||
| 229 | + Optional<String> speakerName = Optional.of("bgp1"); | ||
| 230 | + ConnectPoint connectPoint = CONNECT_POINT1; | ||
| 231 | + Set<IpAddress> connectedPeers = new HashSet<>(Arrays.asList(IP1, IP2, IP3)); | ||
| 232 | + | ||
| 233 | + return new BgpConfig.BgpSpeakerConfig(speakerName, connectPoint, connectedPeers); | ||
| 234 | + } | ||
| 235 | + | ||
| 236 | + private BgpConfig.BgpSpeakerConfig createNewSpeaker() { | ||
| 237 | + Optional<String> speakerName = Optional.of("newSpeaker"); | ||
| 238 | + ConnectPoint connectPoint = CONNECT_POINT2; | ||
| 239 | + Set<IpAddress> connectedPeers = new HashSet<>( | ||
| 240 | + Arrays.asList(IP4, IP5)); | ||
| 241 | + | ||
| 242 | + return new BgpConfig.BgpSpeakerConfig(speakerName, connectPoint, connectedPeers); | ||
| 243 | + } | ||
| 244 | +} |
| ... | @@ -50,13 +50,17 @@ public class BgpSpeakersListCommand extends AbstractShellCommand { | ... | @@ -50,13 +50,17 @@ public class BgpSpeakersListCommand extends AbstractShellCommand { |
| 50 | ApplicationId appId = coreService.getAppId(RoutingService.ROUTER_APP_ID); | 50 | ApplicationId appId = coreService.getAppId(RoutingService.ROUTER_APP_ID); |
| 51 | 51 | ||
| 52 | BgpConfig config = configService.getConfig(appId, BgpConfig.class); | 52 | BgpConfig config = configService.getConfig(appId, BgpConfig.class); |
| 53 | + if (config == null) { | ||
| 54 | + print("No speakers configured"); | ||
| 55 | + return; | ||
| 56 | + } | ||
| 53 | 57 | ||
| 54 | List<BgpConfig.BgpSpeakerConfig> bgpSpeakers = | 58 | List<BgpConfig.BgpSpeakerConfig> bgpSpeakers = |
| 55 | Lists.newArrayList(config.bgpSpeakers()); | 59 | Lists.newArrayList(config.bgpSpeakers()); |
| 56 | 60 | ||
| 57 | Collections.sort(bgpSpeakers, SPEAKERS_COMPARATOR); | 61 | Collections.sort(bgpSpeakers, SPEAKERS_COMPARATOR); |
| 58 | 62 | ||
| 59 | - if (config == null || config.bgpSpeakers().isEmpty()) { | 63 | + if (config.bgpSpeakers().isEmpty()) { |
| 60 | print("No speakers configured"); | 64 | print("No speakers configured"); |
| 61 | } else { | 65 | } else { |
| 62 | bgpSpeakers.forEach( | 66 | bgpSpeakers.forEach( | ... | ... |
| 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 | + | ||
| 17 | +package org.onosproject.sdnip.cli; | ||
| 18 | + | ||
| 19 | +import org.apache.karaf.shell.commands.Argument; | ||
| 20 | +import org.apache.karaf.shell.commands.Command; | ||
| 21 | +import org.onlab.packet.IpAddress; | ||
| 22 | +import org.onosproject.cli.AbstractShellCommand; | ||
| 23 | +import org.onosproject.core.ApplicationId; | ||
| 24 | +import org.onosproject.core.CoreService; | ||
| 25 | +import org.onosproject.incubator.net.intf.InterfaceService; | ||
| 26 | +import org.onosproject.net.config.NetworkConfigService; | ||
| 27 | +import org.onosproject.routing.RoutingService; | ||
| 28 | +import org.onosproject.routing.config.BgpConfig; | ||
| 29 | + | ||
| 30 | +/** | ||
| 31 | + * Command to add new BGP peer to existing internal speaker. | ||
| 32 | + */ | ||
| 33 | +@Command(scope = "onos", name = "add-bgp-peer", | ||
| 34 | + description = "Adds an external BGP router as peer to an existing BGP speaker") | ||
| 35 | +public class AddPeerCommand extends AbstractShellCommand { | ||
| 36 | + | ||
| 37 | + @Argument(index = 0, name = "name", | ||
| 38 | + description = "Name of the internal BGP speaker", | ||
| 39 | + required = true, multiValued = false) | ||
| 40 | + String name = null; | ||
| 41 | + | ||
| 42 | + @Argument(index = 1, name = "ip", | ||
| 43 | + description = "IP address of the BGP peer", | ||
| 44 | + required = true, multiValued = false) | ||
| 45 | + String ip = null; | ||
| 46 | + | ||
| 47 | + private static final String PEER_ADD_SUCCESS = "Peer Successfully Added."; | ||
| 48 | + private static final String NO_CONFIGURATION = "No speakers configured"; | ||
| 49 | + private static final String SPEAKER_NOT_FOUND = | ||
| 50 | + "Speaker with name \'%s\' not found"; | ||
| 51 | + private static final String NO_INTERFACE = | ||
| 52 | + "No matching interface found for IP \'%s\'"; | ||
| 53 | + | ||
| 54 | + private IpAddress peerAddress = null; | ||
| 55 | + | ||
| 56 | + @Override | ||
| 57 | + protected void execute() { | ||
| 58 | + peerAddress = IpAddress.valueOf(ip); | ||
| 59 | + | ||
| 60 | + NetworkConfigService configService = get(NetworkConfigService.class); | ||
| 61 | + CoreService coreService = get(CoreService.class); | ||
| 62 | + ApplicationId appId = coreService.getAppId(RoutingService.ROUTER_APP_ID); | ||
| 63 | + | ||
| 64 | + BgpConfig config = configService.getConfig(appId, BgpConfig.class); | ||
| 65 | + if (config == null || config.bgpSpeakers().isEmpty()) { | ||
| 66 | + print(NO_CONFIGURATION); | ||
| 67 | + return; | ||
| 68 | + } | ||
| 69 | + | ||
| 70 | + BgpConfig.BgpSpeakerConfig speaker = config.getSpeakerWithName(name); | ||
| 71 | + if (speaker == null) { | ||
| 72 | + print(SPEAKER_NOT_FOUND, name); | ||
| 73 | + return; | ||
| 74 | + } else { | ||
| 75 | + if (speaker.isConnectedToPeer(peerAddress)) { | ||
| 76 | + return; // Peering already exists. | ||
| 77 | + } | ||
| 78 | + } | ||
| 79 | + | ||
| 80 | + InterfaceService interfaceService = get(InterfaceService.class); | ||
| 81 | + if (interfaceService.getMatchingInterface(peerAddress) == null) { | ||
| 82 | + print(NO_INTERFACE, ip); | ||
| 83 | + return; | ||
| 84 | + } | ||
| 85 | + | ||
| 86 | + addPeerToSpeakerConf(config); | ||
| 87 | + configService.applyConfig(appId, BgpConfig.class, config.node()); | ||
| 88 | + | ||
| 89 | + print(PEER_ADD_SUCCESS); | ||
| 90 | + } | ||
| 91 | + | ||
| 92 | + private void addPeerToSpeakerConf(BgpConfig config) { | ||
| 93 | + log.debug("Creating BGP configuration for new peer: {}", ip); | ||
| 94 | + config.addPeerToSpeaker(name, peerAddress); | ||
| 95 | + } | ||
| 96 | +} |
| 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 | + | ||
| 17 | +package org.onosproject.sdnip.cli; | ||
| 18 | + | ||
| 19 | +import org.apache.karaf.shell.commands.Argument; | ||
| 20 | +import org.apache.karaf.shell.commands.Command; | ||
| 21 | +import org.onlab.packet.IpAddress; | ||
| 22 | +import org.onosproject.cli.AbstractShellCommand; | ||
| 23 | +import org.onosproject.core.ApplicationId; | ||
| 24 | +import org.onosproject.core.CoreService; | ||
| 25 | +import org.onosproject.net.ConnectPoint; | ||
| 26 | +import org.onosproject.net.config.NetworkConfigService; | ||
| 27 | +import org.onosproject.routing.RoutingService; | ||
| 28 | +import org.onosproject.routing.config.BgpConfig; | ||
| 29 | + | ||
| 30 | +import java.util.HashSet; | ||
| 31 | +import java.util.Optional; | ||
| 32 | + | ||
| 33 | +/** | ||
| 34 | + * Command to add a new internal BGP speaker. | ||
| 35 | + */ | ||
| 36 | +@Command(scope = "onos", name = "add-bgp-speaker", | ||
| 37 | + description = "Adds an internal BGP speaker") | ||
| 38 | +public class AddSpeakerCommand extends AbstractShellCommand { | ||
| 39 | + | ||
| 40 | + @Argument(index = 0, name = "name", | ||
| 41 | + description = "Name of the internal BGP speaker", | ||
| 42 | + required = true, multiValued = false) | ||
| 43 | + String name = null; | ||
| 44 | + | ||
| 45 | + @Argument(index = 1, name = "connectionPoint", | ||
| 46 | + description = "Interface to the BGP speaker", | ||
| 47 | + required = true, multiValued = false) | ||
| 48 | + String connectionPoint = null; | ||
| 49 | + | ||
| 50 | + private static final String SPEAKER_ADD_SUCCESS = "Speaker Successfully Added."; | ||
| 51 | + | ||
| 52 | + @Override | ||
| 53 | + protected void execute() { | ||
| 54 | + NetworkConfigService configService = get(NetworkConfigService.class); | ||
| 55 | + CoreService coreService = get(CoreService.class); | ||
| 56 | + ApplicationId appId = coreService.getAppId(RoutingService.ROUTER_APP_ID); | ||
| 57 | + | ||
| 58 | + BgpConfig config = configService.addConfig(appId, BgpConfig.class); | ||
| 59 | + | ||
| 60 | + BgpConfig.BgpSpeakerConfig speaker = config.getSpeakerWithName(name); | ||
| 61 | + if (speaker != null) { | ||
| 62 | + log.debug("Speaker already exists: {}", name); | ||
| 63 | + return; | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + addSpeakerToConf(config); | ||
| 67 | + configService.applyConfig(appId, BgpConfig.class, config.node()); | ||
| 68 | + | ||
| 69 | + print(SPEAKER_ADD_SUCCESS); | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + /** | ||
| 73 | + * Adds the speaker to the BgpConfig service. | ||
| 74 | + * | ||
| 75 | + * @param config the BGP configuration | ||
| 76 | + */ | ||
| 77 | + private void addSpeakerToConf(BgpConfig config) { | ||
| 78 | + log.debug("Adding new speaker to configuration: {}", name); | ||
| 79 | + BgpConfig.BgpSpeakerConfig speaker = getSpeaker(); | ||
| 80 | + | ||
| 81 | + config.addSpeaker(speaker); | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | + private BgpConfig.BgpSpeakerConfig getSpeaker() { | ||
| 85 | + ConnectPoint connectPoint = ConnectPoint. | ||
| 86 | + deviceConnectPoint(connectionPoint); | ||
| 87 | + return new BgpConfig.BgpSpeakerConfig(Optional.ofNullable(name), | ||
| 88 | + connectPoint, new HashSet<IpAddress>()); | ||
| 89 | + } | ||
| 90 | +} |
| 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 | + | ||
| 17 | +package org.onosproject.sdnip.cli; | ||
| 18 | + | ||
| 19 | +import org.apache.karaf.shell.commands.Argument; | ||
| 20 | +import org.apache.karaf.shell.commands.Command; | ||
| 21 | +import org.onlab.packet.IpAddress; | ||
| 22 | +import org.onosproject.cli.AbstractShellCommand; | ||
| 23 | +import org.onosproject.core.ApplicationId; | ||
| 24 | +import org.onosproject.core.CoreService; | ||
| 25 | +import org.onosproject.net.config.NetworkConfigService; | ||
| 26 | +import org.onosproject.routing.RoutingService; | ||
| 27 | +import org.onosproject.routing.config.BgpConfig; | ||
| 28 | + | ||
| 29 | +/** | ||
| 30 | + * Command to remove existing BGP peer. | ||
| 31 | + */ | ||
| 32 | +@Command(scope = "onos", name = "remove-bgp-peer", | ||
| 33 | + description = "Removes a BGP peer") | ||
| 34 | +public class RemovePeerCommand extends AbstractShellCommand { | ||
| 35 | + | ||
| 36 | + @Argument(index = 0, name = "ip", | ||
| 37 | + description = "IP address of the BGP peer", | ||
| 38 | + required = true, multiValued = false) | ||
| 39 | + String ip = null; | ||
| 40 | + | ||
| 41 | + private static final String PEER_REMOVE_SUCCESS = "Peer Successfully Removed."; | ||
| 42 | + private static final String NO_CONFIGURATION = "No speakers configured"; | ||
| 43 | + private static final String PEER_NOT_FOUND = | ||
| 44 | + "Peer with IP \'%s\' not found"; | ||
| 45 | + | ||
| 46 | + private IpAddress peerAddress = null; | ||
| 47 | + | ||
| 48 | + @Override | ||
| 49 | + protected void execute() { | ||
| 50 | + peerAddress = IpAddress.valueOf(ip); | ||
| 51 | + | ||
| 52 | + NetworkConfigService configService = get(NetworkConfigService.class); | ||
| 53 | + CoreService coreService = get(CoreService.class); | ||
| 54 | + ApplicationId appId = coreService.getAppId(RoutingService.ROUTER_APP_ID); | ||
| 55 | + | ||
| 56 | + BgpConfig config = configService.getConfig(appId, BgpConfig.class); | ||
| 57 | + if (config == null || config.bgpSpeakers().isEmpty()) { | ||
| 58 | + print(NO_CONFIGURATION); | ||
| 59 | + return; | ||
| 60 | + } | ||
| 61 | + | ||
| 62 | + peerAddress = IpAddress.valueOf(ip); | ||
| 63 | + | ||
| 64 | + BgpConfig.BgpSpeakerConfig speaker = config.getSpeakerFromPeer(peerAddress); | ||
| 65 | + if (speaker == null) { | ||
| 66 | + print(PEER_NOT_FOUND, ip); | ||
| 67 | + return; | ||
| 68 | + } | ||
| 69 | + | ||
| 70 | + removePeerFromSpeakerConf(speaker, config); | ||
| 71 | + configService.applyConfig(appId, BgpConfig.class, config.node()); | ||
| 72 | + | ||
| 73 | + print(PEER_REMOVE_SUCCESS); | ||
| 74 | + } | ||
| 75 | + | ||
| 76 | + private void removePeerFromSpeakerConf(BgpConfig.BgpSpeakerConfig speaker, | ||
| 77 | + BgpConfig config) { | ||
| 78 | + log.debug("Removing BGP configuration for peer: {}", ip); | ||
| 79 | + config.removePeerFromSpeaker(speaker, peerAddress); | ||
| 80 | + } | ||
| 81 | +} |
| 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 | + | ||
| 17 | +package org.onosproject.sdnip.cli; | ||
| 18 | + | ||
| 19 | +import org.apache.karaf.shell.commands.Argument; | ||
| 20 | +import org.apache.karaf.shell.commands.Command; | ||
| 21 | +import org.onosproject.cli.AbstractShellCommand; | ||
| 22 | +import org.onosproject.core.ApplicationId; | ||
| 23 | +import org.onosproject.core.CoreService; | ||
| 24 | +import org.onosproject.net.config.NetworkConfigService; | ||
| 25 | +import org.onosproject.routing.RoutingService; | ||
| 26 | +import org.onosproject.routing.config.BgpConfig; | ||
| 27 | + | ||
| 28 | +/** | ||
| 29 | + * Command to remove a internal BGP speaker. | ||
| 30 | + */ | ||
| 31 | +@Command(scope = "onos", name = "remove-bgp-speaker", | ||
| 32 | + description = "Removes an internal BGP speaker") | ||
| 33 | +public class RemoveSpeakerCommand extends AbstractShellCommand { | ||
| 34 | + | ||
| 35 | + @Argument(index = 0, name = "name", | ||
| 36 | + description = "Name of the internal BGP speaker", | ||
| 37 | + required = true, multiValued = false) | ||
| 38 | + String name = null; | ||
| 39 | + | ||
| 40 | + private static final String SPEAKER_REMOVE_SUCCESS = "Speaker Successfully Removed."; | ||
| 41 | + private static final String NO_CONFIGURATION = "No speakers configured"; | ||
| 42 | + private static final String PEERS_EXIST = | ||
| 43 | + "Speaker with name \'%s\' has peer connections"; | ||
| 44 | + private static final String SPEAKER_NOT_FOUND = | ||
| 45 | + "Speaker with name \'%s\' not found"; | ||
| 46 | + | ||
| 47 | + @Override | ||
| 48 | + protected void execute() { | ||
| 49 | + NetworkConfigService configService = get(NetworkConfigService.class); | ||
| 50 | + CoreService coreService = get(CoreService.class); | ||
| 51 | + ApplicationId appId = coreService.getAppId(RoutingService.ROUTER_APP_ID); | ||
| 52 | + | ||
| 53 | + BgpConfig config = configService.getConfig(appId, BgpConfig.class); | ||
| 54 | + if (config == null || config.bgpSpeakers().isEmpty()) { | ||
| 55 | + print(NO_CONFIGURATION); | ||
| 56 | + return; | ||
| 57 | + } | ||
| 58 | + | ||
| 59 | + BgpConfig.BgpSpeakerConfig speaker = config.getSpeakerWithName(name); | ||
| 60 | + if (speaker == null) { | ||
| 61 | + print(SPEAKER_NOT_FOUND, name); | ||
| 62 | + return; | ||
| 63 | + } else { | ||
| 64 | + if (!speaker.peers().isEmpty()) { | ||
| 65 | + // Removal not allowed when peer connections exist. | ||
| 66 | + print(PEERS_EXIST, name); | ||
| 67 | + return; | ||
| 68 | + } | ||
| 69 | + } | ||
| 70 | + | ||
| 71 | + removeSpeakerFromConf(config); | ||
| 72 | + configService.applyConfig(appId, BgpConfig.class, config.node()); | ||
| 73 | + | ||
| 74 | + print(SPEAKER_REMOVE_SUCCESS); | ||
| 75 | + } | ||
| 76 | + | ||
| 77 | + /** | ||
| 78 | + * Removes the speaker from the BgpConfig service. | ||
| 79 | + * | ||
| 80 | + * @param bgpConfig the BGP configuration | ||
| 81 | + */ | ||
| 82 | + private void removeSpeakerFromConf(BgpConfig bgpConfig) { | ||
| 83 | + log.debug("Removing speaker from configuration: {}", name); | ||
| 84 | + | ||
| 85 | + bgpConfig.removeSpeaker(name); | ||
| 86 | + } | ||
| 87 | +} |
| ... | @@ -19,5 +19,17 @@ | ... | @@ -19,5 +19,17 @@ |
| 19 | <command> | 19 | <command> |
| 20 | <action class="org.onosproject.sdnip.cli.PrimaryChangeCommand"/> | 20 | <action class="org.onosproject.sdnip.cli.PrimaryChangeCommand"/> |
| 21 | </command> | 21 | </command> |
| 22 | + <command> | ||
| 23 | + <action class="org.onosproject.sdnip.cli.AddSpeakerCommand"/> | ||
| 24 | + </command> | ||
| 25 | + <command> | ||
| 26 | + <action class="org.onosproject.sdnip.cli.RemoveSpeakerCommand"/> | ||
| 27 | + </command> | ||
| 28 | + <command> | ||
| 29 | + <action class="org.onosproject.sdnip.cli.AddPeerCommand"/> | ||
| 30 | + </command> | ||
| 31 | + <command> | ||
| 32 | + <action class="org.onosproject.sdnip.cli.RemovePeerCommand"/> | ||
| 33 | + </command> | ||
| 22 | </command-bundle> | 34 | </command-bundle> |
| 23 | </blueprint> | 35 | </blueprint> | ... | ... |
-
Please register or login to post a comment