From eae6b96f6529e4a9f350809a5def4d82a904501b Mon Sep 17 00:00:00 2001 From: Alexis de Talhouët Date: Wed, 15 Aug 2018 14:30:43 -0400 Subject: Added netbox client to assign/unassign ip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change-Id: Ied317c7f251936ced116b6a3ea81789c82095df6 Issue-ID: CCSDK-462 Signed-off-by: Alexis de Talhouët --- netbox-client/provider/pom.xml | 51 +++++- .../sli/adaptors/netbox/api/IpamException.java | 27 +++ .../sli/adaptors/netbox/api/NetboxClient.java | 40 +++++ .../sli/adaptors/netbox/impl/NetboxClientImpl.java | 105 ++++++++++++ .../sli/adaptors/netbox/impl/NetboxHttpClient.java | 151 +++++++++++++++++ .../ccsdk/sli/adaptors/netbox/model/IPAddress.java | 58 +++++++ .../sli/adaptors/netbox/model/Identifiable.java | 29 ++++ .../ccsdk/sli/adaptors/netbox/model/Prefix.java | 20 +++ .../ccsdk/sli/adaptors/netbox/model/Status.java | 77 +++++++++ .../adaptors/netbox/property/NetboxProperties.java | 116 +++++++++++++ .../provider/src/main/resources/netbox.properties | 19 +++ .../org/opendaylight/blueprint/netbox-client.xml | 34 ++++ .../adaptors/netbox/impl/NetboxClientImplTest.java | 185 +++++++++++++++++++++ .../adaptors/netbox/impl/NetboxHttpClientTest.java | 127 ++++++++++++++ .../netbox/property/NetboxPropertiesTest.java | 67 ++++++++ .../test/resources/nextAvailableIpResponse.json | 13 ++ 16 files changed, 1112 insertions(+), 7 deletions(-) create mode 100644 netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/api/IpamException.java create mode 100644 netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/api/NetboxClient.java create mode 100644 netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/impl/NetboxClientImpl.java create mode 100644 netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/impl/NetboxHttpClient.java create mode 100644 netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/model/IPAddress.java create mode 100644 netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/model/Identifiable.java create mode 100644 netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/model/Prefix.java create mode 100644 netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/model/Status.java create mode 100644 netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/property/NetboxProperties.java create mode 100755 netbox-client/provider/src/main/resources/netbox.properties create mode 100644 netbox-client/provider/src/main/resources/org/opendaylight/blueprint/netbox-client.xml create mode 100644 netbox-client/provider/src/test/java/org/onap/ccsdk/sli/adaptors/netbox/impl/NetboxClientImplTest.java create mode 100644 netbox-client/provider/src/test/java/org/onap/ccsdk/sli/adaptors/netbox/impl/NetboxHttpClientTest.java create mode 100644 netbox-client/provider/src/test/java/org/onap/ccsdk/sli/adaptors/netbox/property/NetboxPropertiesTest.java create mode 100644 netbox-client/provider/src/test/resources/nextAvailableIpResponse.json (limited to 'netbox-client/provider') diff --git a/netbox-client/provider/pom.xml b/netbox-client/provider/pom.xml index af4f812f..16695f20 100644 --- a/netbox-client/provider/pom.xml +++ b/netbox-client/provider/pom.xml @@ -34,11 +34,6 @@ ccsdk-sli-adaptors :: netbox-client :: ${project.artifactId} - - junit - junit - ${junit.version} - com.google.code.gson gson @@ -59,6 +54,48 @@ httpcore-osgi ${apache.httpcomponents.core.version} + + org.slf4j + slf4j-api + + + org.onap.ccsdk.sli.core + sli-common + compile + + + + + junit + junit + ${junit.version} + test + + + org.mockito + mockito-core + 2.2.11 + test + + + com.github.tomakehurst + wiremock + 2.7.1 + test + + + org.eclipse.jetty + jetty-server + 9.3.1.v20150714 + test + + + ch.qos.logback + logback-classic + 1.2.3 + test + + @@ -70,9 +107,9 @@ + org.onap.ccsdk.sli.adaptors.netbox.api, org.onap.ccsdk.sli.adaptors.netbox.ipam, - org.onap.ccsdk.sli.adaptors.netbox.model, - org.onap.ccsdk.sli.adaptors.netbox.query + org.onap.ccsdk.sli.adaptors.netbox.model diff --git a/netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/api/IpamException.java b/netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/api/IpamException.java new file mode 100644 index 00000000..869a57ce --- /dev/null +++ b/netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/api/IpamException.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2018 Bell Canada. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.onap.ccsdk.sli.adaptors.netbox.api; + +public class IpamException extends Exception { + + public IpamException(final String message) { + super(message); + } + + public IpamException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/api/NetboxClient.java b/netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/api/NetboxClient.java new file mode 100644 index 00000000..57d727ad --- /dev/null +++ b/netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/api/NetboxClient.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2018 Bell Canada. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.onap.ccsdk.sli.adaptors.netbox.api; + +import org.onap.ccsdk.sli.adaptors.netbox.model.IPAddress; +import org.onap.ccsdk.sli.adaptors.netbox.model.Prefix; + +public interface NetboxClient { + + /** + * Assign next available IP in prefix. + * + * @param prefix The prefix from which to get next available IP. + * @return The IPAddress + * @throws IpamException If something goes wrong. + */ + IPAddress assign(Prefix prefix) throws IpamException; + + /** + * Free the IP. + * + * @param ip The IP to release. + * @throws IpamException If something goes wrong. + */ + void unassign(IPAddress ip) throws IpamException; +} + diff --git a/netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/impl/NetboxClientImpl.java b/netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/impl/NetboxClientImpl.java new file mode 100644 index 00000000..0520ad5e --- /dev/null +++ b/netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/impl/NetboxClientImpl.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2018 Bell Canada. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.onap.ccsdk.sli.adaptors.netbox.impl; + +import com.google.common.annotations.VisibleForTesting; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSerializer; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.concurrent.CompletionException; +import org.apache.http.HttpResponse; +import org.onap.ccsdk.sli.adaptors.netbox.api.IpamException; +import org.onap.ccsdk.sli.adaptors.netbox.api.NetboxClient; +import org.onap.ccsdk.sli.adaptors.netbox.model.IPAddress; +import org.onap.ccsdk.sli.adaptors.netbox.model.Prefix; +import org.onap.ccsdk.sli.adaptors.netbox.model.Status; + +public class NetboxClientImpl implements NetboxClient { + + private static final String NEXT_AVAILABLE_IP_IN_PREFIX_PATH = "/api/ipam/prefixes/%s/available-ips/"; + private static final String IP_ADDRESS_PATH = "/api/ipam/ip-addresses/%s/"; + private static final String EMPTY_STRING = ""; + private static final String ID_MISSING_MSG = "Id must be set"; + + private final NetboxHttpClient client; + private final Gson gson; + + public NetboxClientImpl(final NetboxHttpClient client) { + this.client = client; + final JsonSerializer vlanStatusDeserializer = (val, type, context) -> val.toJson(); + gson = new GsonBuilder() + .registerTypeAdapter(Status.class, vlanStatusDeserializer) + .create(); + } + + @Override + public IPAddress assign(final Prefix prefix) throws IpamException { + checkArgument(prefix.getId() != null); + try { + return client.post(String.format(NEXT_AVAILABLE_IP_IN_PREFIX_PATH, prefix.getId()), EMPTY_STRING) + .thenApply(this::getIpAddress) + .toCompletableFuture() + .join(); + } catch (CompletionException e) { + // Unwrap the ComplettionException and wrap in IpamException + throw new IpamException("Fail to assign IP for Prefix(id= " + prefix.getId() + "). " + e.getMessage(), + e.getCause()); + } + } + + @Override + public void unassign(final IPAddress ipAddress) throws IpamException { + checkArgument(ipAddress.getId() != null); + try { + client.delete(String.format(IP_ADDRESS_PATH, ipAddress.getId())) + .thenAccept(this::checkResult) + .toCompletableFuture() + .join(); + } catch (CompletionException e) { + // Unwrap the ComplettionException and wrap in IpamException + throw new IpamException("Fail to unassign IP for IPAddress(id= " + ipAddress.getId() + "). " + e.getMessage(), + e.getCause()); + } + } + + @VisibleForTesting + IPAddress getIpAddress(final HttpResponse response) { + if (response.getStatusLine().getStatusCode() != 201) { + throw new IllegalStateException(NetboxHttpClient.getBodyAsString(response)); + } + try (final Reader reader = new InputStreamReader(response.getEntity().getContent())) { + return gson.fromJson(reader, IPAddress.class); + } catch (final IOException e) { + throw new IllegalStateException(e); + } + } + + private static void checkArgument(final boolean argument) throws IpamException { + if (!argument) { + throw new IpamException(ID_MISSING_MSG); + } + } + + private void checkResult(final HttpResponse response) { + if (response.getStatusLine().getStatusCode() - 200 >= 100) { + throw new IllegalStateException( + "Netbox request failed with status: " + NetboxHttpClient.getBodyAsString(response)); + } + } +} diff --git a/netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/impl/NetboxHttpClient.java b/netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/impl/NetboxHttpClient.java new file mode 100644 index 00000000..a77b4d3e --- /dev/null +++ b/netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/impl/NetboxHttpClient.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2018 Bell Canada. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.onap.ccsdk.sli.adaptors.netbox.impl; + +import static org.apache.http.HttpHeaders.ACCEPT; +import static org.apache.http.HttpHeaders.AUTHORIZATION; +import static org.apache.http.HttpHeaders.CONTENT_TYPE; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.util.Scanner; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.function.Function; +import javax.net.ssl.SSLContext; +import org.apache.http.HttpEntityEnclosingRequest; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.concurrent.FutureCallback; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; +import org.apache.http.impl.nio.client.HttpAsyncClientBuilder; +import org.apache.http.ssl.SSLContexts; +import org.apache.http.ssl.TrustStrategy; +import org.onap.ccsdk.sli.adaptors.netbox.api.IpamException; +import org.onap.ccsdk.sli.adaptors.netbox.property.NetboxProperties; + +public class NetboxHttpClient implements AutoCloseable { + + private static final String APPLICATION_JSON = "application/json"; + + private final CloseableHttpAsyncClient client; + private final String url; + private final String token; + + // Used by the blueprint container + public NetboxHttpClient(NetboxProperties properties) { + this(properties.getHost(), properties.getApiKey()); + } + + NetboxHttpClient(final String url, final String token) { + this.url = url; + this.token = token; + + final TrustStrategy acceptingTrustStrategy = (certificate, authType) -> true; + final SSLContext sslContext; + try { + sslContext = SSLContexts.custom() + .loadTrustMaterial(null, acceptingTrustStrategy).build(); + } catch (final NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) { + throw new IllegalStateException("Can't create http client", e); + } + client = HttpAsyncClientBuilder.create() + .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE) + .setSSLContext(sslContext) + .build(); + + } + + // Has to be public for blueprint container to access it + public void init() { + client.start(); + } + + @Override + public void close() throws IOException { + client.close(); + } + + CompletionStage post(final String uri, final String requestBody) { + return sendRequest(uri, requestBody, HttpPost::new); + } + + CompletionStage delete(final String uri) { + return sendRequest(uri, HttpDelete::new); + } + + static String getBodyAsString(final HttpResponse response) { + final String body; + if (response.getEntity() != null) { + try (final Scanner s = new java.util.Scanner(response.getEntity().getContent()).useDelimiter("\\A")) { + body = s.hasNext() ? s.next() : ""; + } catch (final IOException e) { + throw new IllegalStateException(e); + } + } else { + body = ""; + } + return response.toString() + "\n" + body; + } + + private CompletionStage sendRequest(final String uri, + final Function supplier) { + final T request = supplier.apply(url + uri); + request.addHeader(ACCEPT, APPLICATION_JSON); + request.addHeader(CONTENT_TYPE, APPLICATION_JSON); + request.addHeader(AUTHORIZATION, "Token " + token); + return sendRequest(request); + } + + private + CompletionStage sendRequest(final String uri, final String body, + final Function supplier) { + final T request = supplier.apply(url + uri); + request.addHeader(ACCEPT, APPLICATION_JSON); + request.addHeader(CONTENT_TYPE, APPLICATION_JSON); + request.addHeader(AUTHORIZATION, "Token " + token); + request.setEntity(new StringEntity(body, Charset.forName("UTF-8"))); + return sendRequest(request); + } + + private CompletionStage sendRequest(final HttpUriRequest request) { + final CompletableFuture future = new CompletableFuture<>(); + client.execute(request, new FutureCallback() { + @Override + public void completed(final HttpResponse httpResponse) { + future.complete(httpResponse); + } + + @Override + public void failed(final Exception e) { + future.completeExceptionally(new IpamException("Netbox request failed", e)); + } + + @Override + public void cancelled() { + future.cancel(false); + } + }); + return future; + } +} \ No newline at end of file diff --git a/netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/model/IPAddress.java b/netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/model/IPAddress.java new file mode 100644 index 00000000..6d62fff9 --- /dev/null +++ b/netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/model/IPAddress.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2018 Bell Canada. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.onap.ccsdk.sli.adaptors.netbox.model; + +import java.util.Objects; + +public class IPAddress extends Identifiable { + + private Status.Values status; + private String address; + + public void setStatus(Status.Values status) { + this.status = status; + } + + public void setAddress(String address) { + this.address = address; + } + + public Status.Values getStatus() { + return status; + } + + public String getAddress() { + return address; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + IPAddress ipAddress = (IPAddress) o; + return Objects.equals(status, ipAddress.status) && + Objects.equals(address, ipAddress.address); + } + + @Override + public int hashCode() { + return Objects.hash(status, address); + } +} diff --git a/netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/model/Identifiable.java b/netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/model/Identifiable.java new file mode 100644 index 00000000..501088d7 --- /dev/null +++ b/netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/model/Identifiable.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2018 Bell Canada. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.onap.ccsdk.sli.adaptors.netbox.model; + +public abstract class Identifiable { + + private Integer id; + + public Integer getId() { + return id; + } + + public void setId(final Integer id) { + this.id = id; + } +} diff --git a/netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/model/Prefix.java b/netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/model/Prefix.java new file mode 100644 index 00000000..b20be91d --- /dev/null +++ b/netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/model/Prefix.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2018 Bell Canada. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.onap.ccsdk.sli.adaptors.netbox.model; + +public class Prefix extends Identifiable { + +} diff --git a/netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/model/Status.java b/netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/model/Status.java new file mode 100644 index 00000000..c56828a8 --- /dev/null +++ b/netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/model/Status.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2018 Bell Canada. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.onap.ccsdk.sli.adaptors.netbox.model; + +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import com.google.gson.annotations.SerializedName; + +public class Status { + + private Integer value; + private String label; + + public Integer getValue() { + return value; + } + + public void setValue(final Integer value) { + this.value = value; + } + + public String getLabel() { + return label; + } + + public void setLabel(final String label) { + this.label = label; + } + + public JsonElement toJson() { + return new JsonPrimitive(value); + } + + public enum Values { + @SerializedName("1") + ACTIVE(1, "Active"), + @SerializedName("2") + RESERVED(2, "Reserved"); + + private final int value; + private final String label; + + Values(final int value, final String label) { + this.value = value; + this.label = label; + } + + public int getValue() { + return value; + } + + public String getLabel() { + return label; + } + + public Status getStatus() { + final Status status = new Status(); + status.setValue(value); + status.setLabel(label); + return status; + } + + } +} diff --git a/netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/property/NetboxProperties.java b/netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/property/NetboxProperties.java new file mode 100644 index 00000000..ee493ec1 --- /dev/null +++ b/netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/property/NetboxProperties.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2018 Bell Canada. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.onap.ccsdk.sli.adaptors.netbox.property; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.HashSet; +import java.util.Optional; +import java.util.Properties; +import java.util.Set; + +import org.onap.ccsdk.sli.adaptors.netbox.api.IpamException; +import org.onap.ccsdk.sli.core.utils.JREFileResolver; +import org.onap.ccsdk.sli.core.utils.KarafRootFileResolver; +import org.onap.ccsdk.sli.core.utils.PropertiesFileResolver; +import org.onap.ccsdk.sli.core.utils.common.BundleContextFileResolver; +import org.onap.ccsdk.sli.core.utils.common.SdncConfigEnvVarFileResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Responsible for determining the properties file to use. + * + *
    + *
  1. A directory identified by the system environment variable SDNC_CONFIG_DIR
  2. + *
  3. A directory identified by the JRE argument netbox.properties
  4. + *
  5. A netbox.properties file located in the karaf root directory
  6. + *
+ * + * Partial copy and adaptation of org.onap.ccsdk.sli.adaptors.aai.AAIServiceProvider + */ +public class NetboxProperties { + + private static final Logger LOG = LoggerFactory.getLogger(NetboxProperties.class); + + private static final String NETBOX_PROPERTY_FILE_NAME = "netbox.properties"; + private static final String MISSING_PROPERTY_FILE = + "Missing configuration properties resource for Netbox: " + NETBOX_PROPERTY_FILE_NAME; + private static final String NETBOX_URL_PROP = "org.onap.ccsdk.sli.adaptors.netbox.url"; + private static final String NETBOX_API_KEY_PROP = "org.onap.ccsdk.sli.adaptors.netbox.apikey"; + + private Set fileResolvers = new HashSet<>(); + private Properties properties; + + public NetboxProperties() { + fileResolvers.add(new SdncConfigEnvVarFileResolver("Using property file (1) from environment variable")); + fileResolvers.add(new BundleContextFileResolver("Using property file (2) from BundleContext property", + NetboxProperties.class)); + fileResolvers.add(new JREFileResolver("Using property file (3) from JRE argument", NetboxProperties.class)); + fileResolvers.add(new KarafRootFileResolver("Using property file (4) from karaf root", this)); + + loadProps(); + } + + public String getHost() { + checkArgument(properties != null); + return properties.getProperty(NETBOX_URL_PROP); + } + + public String getApiKey() { + checkArgument(properties != null); + return properties.getProperty(NETBOX_API_KEY_PROP); + } + + private void checkArgument(final boolean argument) { + if (!argument) { + LOG.info("Propety file {} was missing, trying to reload it", NETBOX_PROPERTY_FILE_NAME); + loadProps(); + if (properties == null) { + throw new IllegalArgumentException(MISSING_PROPERTY_FILE); + } + } + } + + private void loadProps() { + // determines properties file as according to the priority described in the class header comment + final File propertiesFile = determinePropertiesFile(); + if (propertiesFile != null) { + try (FileInputStream fileInputStream = new FileInputStream(propertiesFile)) { + properties = new Properties(); + properties.load(fileInputStream); + } catch (final IOException e) { + String errorMsg = "Failed to load properties for file: " + propertiesFile.toString(); + LOG.error(errorMsg, new IpamException(errorMsg)); + } + } + } + + private File determinePropertiesFile() { + for (final PropertiesFileResolver resolver : fileResolvers) { + final Optional fileOptional = resolver.resolveFile(NETBOX_PROPERTY_FILE_NAME); + if (fileOptional.isPresent()) { + final File file = fileOptional.get(); + LOG.info("{} {}", resolver.getSuccessfulResolutionMessage(), file.getPath()); + return file; + } + } + + LOG.error(MISSING_PROPERTY_FILE, new IpamException(MISSING_PROPERTY_FILE)); + return null; + } +} diff --git a/netbox-client/provider/src/main/resources/netbox.properties b/netbox-client/provider/src/main/resources/netbox.properties new file mode 100755 index 00000000..ee5e67b4 --- /dev/null +++ b/netbox-client/provider/src/main/resources/netbox.properties @@ -0,0 +1,19 @@ +# +# Copyright (C) 2018 AT&T, Bell Canada. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Configuration file for Netbox client +org.onap.ccsdk.sli.adaptors.netbox.url=http://localhost:9998 +org.onap.ccsdk.sli.adaptors.netbox.apikey=onceuponatimeiplayedwithnetbox20180814 \ No newline at end of file diff --git a/netbox-client/provider/src/main/resources/org/opendaylight/blueprint/netbox-client.xml b/netbox-client/provider/src/main/resources/org/opendaylight/blueprint/netbox-client.xml new file mode 100644 index 00000000..cf8a1af4 --- /dev/null +++ b/netbox-client/provider/src/main/resources/org/opendaylight/blueprint/netbox-client.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + diff --git a/netbox-client/provider/src/test/java/org/onap/ccsdk/sli/adaptors/netbox/impl/NetboxClientImplTest.java b/netbox-client/provider/src/test/java/org/onap/ccsdk/sli/adaptors/netbox/impl/NetboxClientImplTest.java new file mode 100644 index 00000000..19b178c9 --- /dev/null +++ b/netbox-client/provider/src/test/java/org/onap/ccsdk/sli/adaptors/netbox/impl/NetboxClientImplTest.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2018 Bell Canada. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.onap.ccsdk.sli.adaptors.netbox.impl; + +import static com.github.tomakehurst.wiremock.client.WireMock.created; +import static com.github.tomakehurst.wiremock.client.WireMock.delete; +import static com.github.tomakehurst.wiremock.client.WireMock.deleteRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.givenThat; +import static com.github.tomakehurst.wiremock.client.WireMock.ok; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.serverError; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.apache.http.HttpHeaders.ACCEPT; +import static org.apache.http.HttpHeaders.AUTHORIZATION; +import static org.apache.http.HttpHeaders.CONTENT_TYPE; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import com.google.common.base.Charsets; +import com.google.common.io.Resources; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.StatusLine; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; +import org.onap.ccsdk.sli.adaptors.netbox.api.IpamException; +import org.onap.ccsdk.sli.adaptors.netbox.model.IPAddress; +import org.onap.ccsdk.sli.adaptors.netbox.model.Prefix; +import org.onap.ccsdk.sli.adaptors.netbox.model.Status.Values; + +@RunWith(MockitoJUnitRunner.class) +public class NetboxClientImplTest { + + private static final String APPLICATION_JSON = "application/json"; + + @Rule + public WireMockRule wm = new WireMockRule(wireMockConfig().dynamicPort()); + + private String token = "token"; + + private NetboxHttpClient httpClient; + private NetboxClientImpl netboxClient; + + @Before + public void setup() { + String baseUrl = "http://localhost:" + wm.port(); + + httpClient = new NetboxHttpClient(baseUrl, token); + httpClient.init(); + + netboxClient = new NetboxClientImpl(httpClient); + + wm.addMockServiceRequestListener( + (request, response) -> { + System.out.println("Request URL :" + request.getAbsoluteUrl()); + System.out.println("Request body :" + request.getBodyAsString()); + System.out.println("Response status :" + response.getStatus()); + System.out.println("Response body :" + response.getBodyAsString()); + }); + } + + @After + public void tearDown() throws IOException { + httpClient.close(); + } + + @Test + public void nextAvailableIpInPrefixTestNoId() { + Prefix prefix = mock(Prefix.class); + doReturn(null).when(prefix).getId(); + try { + netboxClient.assign(prefix); + } catch (IpamException e) { + Assert.assertEquals("Id must be set", e.getMessage()); + return; + } + Assert.fail(); + } + + @Test + public void nextAvailableIpInPrefixTest() throws IOException, IpamException { + Integer id = 3; + Prefix prefix = mock(Prefix.class); + doReturn(id).when(prefix).getId(); + + URL url = Resources.getResource("nextAvailableIpResponse.json"); + String response = Resources.toString(url, Charsets.UTF_8); + + String expectedUrl = "/api/ipam/prefixes/" + id + "/available-ips/"; + givenThat(post(urlEqualTo(expectedUrl)).willReturn(created().withBody(response))); + + netboxClient.assign(prefix); + + verify(postRequestedFor(urlEqualTo(expectedUrl)) + .withHeader(ACCEPT, equalTo(APPLICATION_JSON)) + .withHeader(CONTENT_TYPE, equalTo(APPLICATION_JSON)) + .withHeader(AUTHORIZATION, equalTo("Token " + token))); + } + + @Test + public void deleteIpTestError500() { + Integer id = 3; + IPAddress ipAddress = mock(IPAddress.class); + doReturn(id).when(ipAddress).getId(); + + String expectedUrl = "/api/ipam/ip-addresses/" + id + "/"; + givenThat(delete(urlEqualTo(expectedUrl)).willReturn(serverError())); + try { + netboxClient.unassign(ipAddress); + } catch (IpamException e) { + Assert.assertEquals(IllegalStateException.class, e.getCause().getClass()); + Assert.assertTrue(e.getMessage().contains( + "Fail to unassign IP for IPAddress(id= 3). java.lang.IllegalStateException: Netbox request failed with status: HTTP/1.1 500 Server Error")); + return; + } + Assert.fail(); + } + + @Test + public void deleteIpTest() throws IpamException { + Integer id = 3; + IPAddress ipAddress = mock(IPAddress.class); + doReturn(id).when(ipAddress).getId(); + + String expectedUrl = "/api/ipam/ip-addresses/" + id + "/"; + givenThat(delete(urlEqualTo(expectedUrl)).willReturn(ok())); + netboxClient.unassign(ipAddress); + verify(deleteRequestedFor(urlEqualTo(expectedUrl)) + .withHeader(ACCEPT, equalTo(APPLICATION_JSON)) + .withHeader(CONTENT_TYPE, equalTo(APPLICATION_JSON)) + .withHeader(AUTHORIZATION, equalTo("Token " + token))); + } + + + @Test + public void getIpAddressTest() throws IOException { + StatusLine statusLine = mock(StatusLine.class); + doReturn(201).when(statusLine).getStatusCode(); + + URL url = Resources.getResource("nextAvailableIpResponse.json"); + String response = Resources.toString(url, Charsets.UTF_8); + InputStream stream = new ByteArrayInputStream(response.getBytes(StandardCharsets.UTF_8)); + + HttpEntity entity = mock(HttpEntity.class); + doReturn(stream).when(entity).getContent(); + + HttpResponse httpResponse = mock(HttpResponse.class); + doReturn(statusLine).when(httpResponse).getStatusLine(); + doReturn(entity).when(httpResponse).getEntity(); + + IPAddress ipAddress = netboxClient.getIpAddress(httpResponse); + + Assert.assertEquals("192.168.20.7/32", ipAddress.getAddress()); + Assert.assertEquals(Integer.valueOf(8), ipAddress.getId()); + Assert.assertEquals(Values.ACTIVE, ipAddress.getStatus()); + } +} \ No newline at end of file diff --git a/netbox-client/provider/src/test/java/org/onap/ccsdk/sli/adaptors/netbox/impl/NetboxHttpClientTest.java b/netbox-client/provider/src/test/java/org/onap/ccsdk/sli/adaptors/netbox/impl/NetboxHttpClientTest.java new file mode 100644 index 00000000..ee2861c0 --- /dev/null +++ b/netbox-client/provider/src/test/java/org/onap/ccsdk/sli/adaptors/netbox/impl/NetboxHttpClientTest.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2018 Bell Canada. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.onap.ccsdk.sli.adaptors.netbox.impl; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.delete; +import static com.github.tomakehurst.wiremock.client.WireMock.deleteRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.givenThat; +import static com.github.tomakehurst.wiremock.client.WireMock.ok; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.apache.http.HttpHeaders.ACCEPT; +import static org.apache.http.HttpHeaders.CONTENT_TYPE; + +import com.github.tomakehurst.wiremock.http.Fault; +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import java.io.IOException; +import java.util.concurrent.CompletionException; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.onap.ccsdk.sli.adaptors.netbox.api.IpamException; + +public class NetboxHttpClientTest { + + private static final String APPLICATION_JSON = "application/json"; + + @Rule + public WireMockRule wm = new WireMockRule(wireMockConfig().dynamicPort()); + + private NetboxHttpClient httpClient; + + @Before + public void setup() { + String baseUrl = "http://localhost:" + wm.port(); + String token = "token"; + + httpClient = new NetboxHttpClient(baseUrl, token); + httpClient.init(); + + wm.addMockServiceRequestListener( + (request, response) -> { + System.out.println("Request URL :" + request.getAbsoluteUrl()); + System.out.println("Request body :" + request.getBodyAsString()); + System.out.println("Response status :" + response.getStatus()); + System.out.println("Response body :" + response.getBodyAsString()); + }); + } + + @After + public void tearDown() throws IOException { + httpClient.close(); + } + + @Test + public void postTest() { + String expectedUrl = "/testPost"; + givenThat(post(urlEqualTo(expectedUrl)).willReturn(ok())); + + httpClient.post(expectedUrl, "").toCompletableFuture().join(); + + verify(postRequestedFor(urlEqualTo(expectedUrl)) + .withHeader(ACCEPT, equalTo(APPLICATION_JSON)) + .withHeader(CONTENT_TYPE, equalTo(APPLICATION_JSON))); + } + + @Test + public void postTestException() { + String expectedUrl = "/testPost"; + givenThat(post(urlEqualTo(expectedUrl)).willReturn(aResponse().withFault(Fault.MALFORMED_RESPONSE_CHUNK))); + + try { + httpClient.post(expectedUrl, "").toCompletableFuture().join(); + } catch (CompletionException e) { + Assert.assertEquals(IpamException.class, e.getCause().getClass()); + Assert.assertEquals("Netbox request failed", e.getCause().getMessage()); + return; + } + Assert.fail(); + } + + @Test + public void deleteTest() { + String expectedUrl = "/testDelete"; + givenThat(delete(urlEqualTo(expectedUrl)).willReturn(ok())); + + httpClient.delete(expectedUrl).toCompletableFuture().join(); + + verify(deleteRequestedFor(urlEqualTo(expectedUrl)) + .withHeader(ACCEPT, equalTo(APPLICATION_JSON)) + .withHeader(CONTENT_TYPE, equalTo(APPLICATION_JSON))); + } + + @Test + public void deleteTestException() { + String expectedUrl = "/testDelete"; + givenThat(delete(urlEqualTo(expectedUrl)).willReturn(aResponse().withFault(Fault.MALFORMED_RESPONSE_CHUNK))); + + try { + httpClient.delete(expectedUrl).toCompletableFuture().join(); + } catch (CompletionException e) { + Assert.assertEquals(IpamException.class, e.getCause().getClass()); + Assert.assertEquals("Netbox request failed", e.getCause().getMessage()); + return; + } + Assert.fail(); + } +} diff --git a/netbox-client/provider/src/test/java/org/onap/ccsdk/sli/adaptors/netbox/property/NetboxPropertiesTest.java b/netbox-client/provider/src/test/java/org/onap/ccsdk/sli/adaptors/netbox/property/NetboxPropertiesTest.java new file mode 100644 index 00000000..d9916707 --- /dev/null +++ b/netbox-client/provider/src/test/java/org/onap/ccsdk/sli/adaptors/netbox/property/NetboxPropertiesTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2018 Bell Canada. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.onap.ccsdk.sli.adaptors.netbox.property; + +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.Appender; +import java.util.List; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.slf4j.LoggerFactory; + +@RunWith(MockitoJUnitRunner.class) +public class NetboxPropertiesTest { + + private NetboxProperties props; + + @Mock + private Appender appender; + @Captor + private ArgumentCaptor captor; + + @Before + public void setup() { + ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) LoggerFactory + .getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); + logger.addAppender(appender); + } + + @Test + public void testMissingFile() { + props = new NetboxProperties(); + + verifyLogEntry( + "Missing configuration properties resource for Netbox: netbox.properties"); + } + + + private void verifyLogEntry(String message) { + verify(appender, times(1)).doAppend(captor.capture()); + List allValues = captor.getAllValues(); + for (ILoggingEvent loggingEvent : allValues) { + Assert.assertTrue(loggingEvent.getFormattedMessage().contains(message)); + } + } +} \ No newline at end of file diff --git a/netbox-client/provider/src/test/resources/nextAvailableIpResponse.json b/netbox-client/provider/src/test/resources/nextAvailableIpResponse.json new file mode 100644 index 00000000..dec1245e --- /dev/null +++ b/netbox-client/provider/src/test/resources/nextAvailableIpResponse.json @@ -0,0 +1,13 @@ +{ + "id": 8, + "address": "192.168.20.7/32", + "vrf": null, + "tenant": 1, + "status": 1, + "role": null, + "interface": null, + "description": "", + "nat_inside": null, + "created": "2018-08-15", + "last_updated": "2018-08-15T15:51:26.634903Z" +} \ No newline at end of file -- cgit 1.2.3-korg