Added netbox client to assign/unassign ip 91/60791/14
authorAlexis de Talhouët <adetalhouet89@gmail.com>
Wed, 15 Aug 2018 18:30:43 +0000 (14:30 -0400)
committerDan Timoney <dt5972@att.com>
Thu, 23 Aug 2018 15:07:07 +0000 (15:07 +0000)
Change-Id: Ied317c7f251936ced116b6a3ea81789c82095df6
Issue-ID: CCSDK-462
Signed-off-by: Alexis de Talhouët <adetalhouet89@gmail.com>
16 files changed:
netbox-client/provider/pom.xml
netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/api/IpamException.java [new file with mode: 0644]
netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/api/NetboxClient.java [new file with mode: 0644]
netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/impl/NetboxClientImpl.java [new file with mode: 0644]
netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/impl/NetboxHttpClient.java [new file with mode: 0644]
netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/model/IPAddress.java [new file with mode: 0644]
netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/model/Identifiable.java [new file with mode: 0644]
netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/model/Prefix.java [new file with mode: 0644]
netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/model/Status.java [new file with mode: 0644]
netbox-client/provider/src/main/java/org/onap/ccsdk/sli/adaptors/netbox/property/NetboxProperties.java [new file with mode: 0644]
netbox-client/provider/src/main/resources/netbox.properties [new file with mode: 0755]
netbox-client/provider/src/main/resources/org/opendaylight/blueprint/netbox-client.xml [new file with mode: 0644]
netbox-client/provider/src/test/java/org/onap/ccsdk/sli/adaptors/netbox/impl/NetboxClientImplTest.java [new file with mode: 0644]
netbox-client/provider/src/test/java/org/onap/ccsdk/sli/adaptors/netbox/impl/NetboxHttpClientTest.java [new file with mode: 0644]
netbox-client/provider/src/test/java/org/onap/ccsdk/sli/adaptors/netbox/property/NetboxPropertiesTest.java [new file with mode: 0644]
netbox-client/provider/src/test/resources/nextAvailableIpResponse.json [new file with mode: 0644]

index af4f812..16695f2 100644 (file)
     <name>ccsdk-sli-adaptors :: netbox-client :: ${project.artifactId}</name>
 
     <dependencies>
-        <dependency>
-            <groupId>junit</groupId>
-            <artifactId>junit</artifactId>
-            <version>${junit.version}</version>
-        </dependency>
         <dependency>
             <groupId>com.google.code.gson</groupId>
             <artifactId>gson</artifactId>
             <artifactId>httpcore-osgi</artifactId>
             <version>${apache.httpcomponents.core.version}</version>
         </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.ccsdk.sli.core</groupId>
+            <artifactId>sli-common</artifactId>
+            <scope>compile</scope>
+        </dependency>
+
+        <!--Testing-->
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>${junit.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <version>2.2.11</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.github.tomakehurst</groupId>
+            <artifactId>wiremock</artifactId>
+            <version>2.7.1</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-server</artifactId>
+            <version>9.3.1.v20150714</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+            <version>1.2.3</version>
+            <scope>test</scope>
+        </dependency>
+
     </dependencies>
 
     <build>
                 <configuration>
                     <instructions>
                         <Export-Package>
+                            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
                         </Export-Package>
                     </instructions>
                 </configuration>
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 (file)
index 0000000..869a57c
--- /dev/null
@@ -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 (file)
index 0000000..57d727a
--- /dev/null
@@ -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 (file)
index 0000000..0520ad5
--- /dev/null
@@ -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<Status> 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 (file)
index 0000000..a77b4d3
--- /dev/null
@@ -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<HttpResponse> post(final String uri, final String requestBody) {
+        return sendRequest(uri, requestBody, HttpPost::new);
+    }
+
+    CompletionStage<HttpResponse> 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 <T extends HttpUriRequest> CompletionStage<HttpResponse> sendRequest(final String uri,
+        final Function<String, T> 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 <T extends HttpEntityEnclosingRequest & HttpUriRequest>
+    CompletionStage<HttpResponse> sendRequest(final String uri, final String body,
+        final Function<String, T> 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<HttpResponse> sendRequest(final HttpUriRequest request) {
+        final CompletableFuture<HttpResponse> future = new CompletableFuture<>();
+        client.execute(request, new FutureCallback<HttpResponse>() {
+            @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 (file)
index 0000000..6d62fff
--- /dev/null
@@ -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 (file)
index 0000000..501088d
--- /dev/null
@@ -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 (file)
index 0000000..b20be91
--- /dev/null
@@ -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 (file)
index 0000000..c56828a
--- /dev/null
@@ -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 (file)
index 0000000..ee493ec
--- /dev/null
@@ -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.
+ *
+ * <ol>
+ * <li>A directory identified by the system environment variable <code>SDNC_CONFIG_DIR</code></li>
+ * <li>A directory identified by the JRE argument <code>netbox.properties</code></li>
+ * <li>A <code>netbox.properties</code> file located in the karaf root directory</li>
+ * </ol>
+ *
+ * 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<PropertiesFileResolver> 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<File> 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 (executable)
index 0000000..ee5e67b
--- /dev/null
@@ -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 (file)
index 0000000..cf8a1af
--- /dev/null
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+  -->
+<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0" xmlns:odl="http://opendaylight.org/xmlns/blueprint/v1.0.0"
+  odl:use-default-for-reference-types="true">
+
+    <bean id="netboxProperty" class="org.onap.ccsdk.sli.adaptors.netbox.property.NetboxProperties"/>
+    <bean id="httpClient" class="org.onap.ccsdk.sli.adaptors.netbox.impl.NetboxHttpClient" init-method="init"
+      destroy-method="close">
+        <argument ref="netboxProperty"/>
+    </bean>
+
+    <bean id="netboxClient" class="org.onap.ccsdk.sli.adaptors.netbox.impl.NetboxClientImpl">
+        <argument ref="httpClient"/>
+    </bean>
+
+    <service ref="netboxClient"
+      interface="org.onap.ccsdk.sli.adaptors.netbox.api.NetboxClient"
+      odl:type="default"/>
+
+</blueprint>
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 (file)
index 0000000..19b178c
--- /dev/null
@@ -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 (file)
index 0000000..ee2861c
--- /dev/null
@@ -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 (file)
index 0000000..d991670
--- /dev/null
@@ -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<ILoggingEvent> appender;
+    @Captor
+    private ArgumentCaptor<ILoggingEvent> 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<ILoggingEvent> 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 (file)
index 0000000..dec1245
--- /dev/null
@@ -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