You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jclouds.apache.org by an...@apache.org on 2017/01/11 13:59:58 UTC

jclouds-labs git commit: add ApiMetadata and ProviderMetadata

Repository: jclouds-labs
Updated Branches:
  refs/heads/master a3a6ef299 -> 070fe653b


add ApiMetadata and ProviderMetadata

- add skeleton for PacketApi with ProjectApi only
- add XAuthTokenToRequest filter
- add HttpApiModule and ParserModule
- add ProjectApi feature with Mock and Live Tests
- add pagination to Project API
- add test pagination


Project: http://git-wip-us.apache.org/repos/asf/jclouds-labs/repo
Commit: http://git-wip-us.apache.org/repos/asf/jclouds-labs/commit/070fe653
Tree: http://git-wip-us.apache.org/repos/asf/jclouds-labs/tree/070fe653
Diff: http://git-wip-us.apache.org/repos/asf/jclouds-labs/diff/070fe653

Branch: refs/heads/master
Commit: 070fe653b9ca01854d5dff288ae7902330ccfa5a
Parents: a3a6ef2
Author: Andrea Turli <an...@gmail.com>
Authored: Tue Jan 10 11:11:18 2017 +0100
Committer: Andrea Turli <an...@gmail.com>
Committed: Wed Jan 11 14:59:47 2017 +0100

----------------------------------------------------------------------
 .../main/java/org/jclouds/packet/PacketApi.java |  40 +++
 .../org/jclouds/packet/PacketApiMetadata.java   |  86 +++++
 .../jclouds/packet/PacketProviderMetadata.java  |  78 +++++
 .../config/PacketComputeParserModule.java       |  30 ++
 .../packet/config/PacketHttpApiModule.java      |  55 ++++
 .../domain/internal/PaginatedCollection.java    |  84 +++++
 .../packet/domain/options/ListOptions.java      |  60 ++++
 .../org/jclouds/packet/features/ProjectApi.java |  93 ++++++
 .../packet/filters/AddXAuthTokenToRequest.java  |  47 +++
 .../packet/functions/BaseToPagedIterable.java   |  59 ++++
 .../packet/functions/LinkToListOptions.java     |  63 ++++
 .../packet/handlers/PacketErrorHandler.java     |  64 ++++
 .../packet/PacketProviderMetadataTest.java      |  29 ++
 .../compute/internal/BasePacketApiLiveTest.java |  73 +++++
 .../compute/internal/BasePacketApiMockTest.java | 145 +++++++++
 .../packet/features/ProjectApiLiveTest.java     |  63 ++++
 .../packet/features/ProjectApiMockTest.java     |  79 +++++
 .../packet/functions/LinkToListOptionsTest.java |  57 ++++
 packet/src/test/resources/logback-test.xml      |  42 +++
 packet/src/test/resources/projects-first.json   | 315 +++++++++++++++++++
 packet/src/test/resources/projects-last.json    | 197 ++++++++++++
 packet/src/test/resources/projects.json         |   1 +
 22 files changed, 1760 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/070fe653/packet/src/main/java/org/jclouds/packet/PacketApi.java
----------------------------------------------------------------------
diff --git a/packet/src/main/java/org/jclouds/packet/PacketApi.java b/packet/src/main/java/org/jclouds/packet/PacketApi.java
new file mode 100644
index 0000000..1cb8e9b
--- /dev/null
+++ b/packet/src/main/java/org/jclouds/packet/PacketApi.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.jclouds.packet;
+
+import java.io.Closeable;
+
+import org.jclouds.packet.features.ProjectApi;
+import org.jclouds.rest.annotations.Delegate;
+
+/**
+ * The Packet API is a REST API for managing your services and deployments.
+ * <p>
+ *
+ * @see <a href="https://www.packet.net/help/api/" >doc</a>
+ */
+public interface PacketApi extends Closeable {
+
+   /**
+    * The Packet API includes operations for managing project.
+    *
+    * @see <a href="https://www.packet.net/help/api/#page:projects,header:projects-projects">docs</a>
+    */
+   @Delegate
+   ProjectApi projectApi();
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/070fe653/packet/src/main/java/org/jclouds/packet/PacketApiMetadata.java
----------------------------------------------------------------------
diff --git a/packet/src/main/java/org/jclouds/packet/PacketApiMetadata.java b/packet/src/main/java/org/jclouds/packet/PacketApiMetadata.java
new file mode 100644
index 0000000..75ba0e6
--- /dev/null
+++ b/packet/src/main/java/org/jclouds/packet/PacketApiMetadata.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.jclouds.packet;
+
+import java.net.URI;
+import java.util.Properties;
+
+import org.jclouds.apis.ApiMetadata;
+import org.jclouds.packet.config.PacketComputeParserModule;
+import org.jclouds.packet.config.PacketHttpApiModule;
+import org.jclouds.rest.internal.BaseHttpApiMetadata;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.inject.Module;
+
+import static org.jclouds.compute.config.ComputeServiceProperties.TEMPLATE;
+import static org.jclouds.compute.config.ComputeServiceProperties.TIMEOUT_NODE_RUNNING;
+
+/**
+ * Implementation of {@link ApiMetadata} for Packet API
+ */
+public class PacketApiMetadata extends BaseHttpApiMetadata<PacketApi> {
+
+   @Override
+   public Builder toBuilder() {
+      return new Builder().fromApiMetadata(this);
+   }
+
+   public PacketApiMetadata() {
+      this(new Builder());
+   }
+
+   protected PacketApiMetadata(Builder builder) {
+      super(builder);
+   }
+
+   public static Properties defaultProperties() {
+      Properties properties = BaseHttpApiMetadata.defaultProperties();
+      properties.put(TEMPLATE, "osFamily=UBUNTU,os64Bit=true,osVersionMatches=16.*");
+      properties.put(TIMEOUT_NODE_RUNNING, 300000); // 5 mins
+      return properties;
+   }
+
+   public static class Builder extends BaseHttpApiMetadata.Builder<PacketApi, Builder> {
+
+      protected Builder() {
+         id("packet")
+                 .name("Packet API")
+                 .identityName("Packet Project Id")
+                 .credentialName("Must be Packet Token")
+                 .documentation(URI.create("https://www.packet.net/help/api/#"))
+                 .defaultEndpoint("https://api.packet.net")
+                 .defaultProperties(PacketApiMetadata.defaultProperties())
+                 //.view(typeToken(ComputeServiceContext.class))
+                 .defaultModules(ImmutableSet.<Class<? extends Module>>builder()
+                         .add(PacketHttpApiModule.class)
+                         .add(PacketComputeParserModule.class)
+                         //.add(PacketComputeServiceContextModule.class)
+                         .build());
+      }
+
+      @Override
+      public PacketApiMetadata build() {
+         return new PacketApiMetadata(this);
+      }
+
+      @Override
+      protected Builder self() {
+         return this;
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/070fe653/packet/src/main/java/org/jclouds/packet/PacketProviderMetadata.java
----------------------------------------------------------------------
diff --git a/packet/src/main/java/org/jclouds/packet/PacketProviderMetadata.java b/packet/src/main/java/org/jclouds/packet/PacketProviderMetadata.java
new file mode 100644
index 0000000..69e0918
--- /dev/null
+++ b/packet/src/main/java/org/jclouds/packet/PacketProviderMetadata.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.jclouds.packet;
+
+import java.net.URI;
+import java.util.Properties;
+
+import org.jclouds.providers.ProviderMetadata;
+import org.jclouds.providers.internal.BaseProviderMetadata;
+
+import com.google.auto.service.AutoService;
+
+@AutoService(ProviderMetadata.class)
+public class PacketProviderMetadata extends BaseProviderMetadata {
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    @Override
+    public Builder toBuilder() {
+        return builder().fromProviderMetadata(this);
+    }
+
+    public PacketProviderMetadata() {
+        super(builder());
+    }
+
+    public PacketProviderMetadata(Builder builder) {
+        super(builder);
+    }
+
+    public static Properties defaultProperties() {
+        final Properties properties = PacketApiMetadata.defaultProperties();
+        return properties;
+    }
+
+    public static class Builder extends BaseProviderMetadata.Builder {
+
+        protected Builder() {
+            id("packet")
+                    .name("Packet Compute Services")
+                    .apiMetadata(new PacketApiMetadata())
+                    .homepage(URI.create("https://www.packet.net/"))
+                    .console(URI.create("https://app.packet.net/portal"))
+                    .endpoint("https://api.packet.net")
+                    .iso3166Codes("US-CA", "US-NJ", "NL", "JP")
+                    .defaultProperties(PacketProviderMetadata.defaultProperties());
+        }
+
+        @Override
+        public PacketProviderMetadata build() {
+            return new PacketProviderMetadata(this);
+        }
+
+        @Override
+        public Builder fromProviderMetadata(ProviderMetadata in) {
+            super.fromProviderMetadata(in);
+            return this;
+        }
+    }
+}
+
+

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/070fe653/packet/src/main/java/org/jclouds/packet/config/PacketComputeParserModule.java
----------------------------------------------------------------------
diff --git a/packet/src/main/java/org/jclouds/packet/config/PacketComputeParserModule.java b/packet/src/main/java/org/jclouds/packet/config/PacketComputeParserModule.java
new file mode 100644
index 0000000..8471fc4
--- /dev/null
+++ b/packet/src/main/java/org/jclouds/packet/config/PacketComputeParserModule.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.jclouds.packet.config;
+
+import org.jclouds.json.config.GsonModule;
+
+import com.google.inject.AbstractModule;
+
+public class PacketComputeParserModule extends AbstractModule {
+
+   @Override
+   protected void configure() {
+      bind(GsonModule.DateAdapter.class).to(GsonModule.Iso8601DateAdapter.class);
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/070fe653/packet/src/main/java/org/jclouds/packet/config/PacketHttpApiModule.java
----------------------------------------------------------------------
diff --git a/packet/src/main/java/org/jclouds/packet/config/PacketHttpApiModule.java b/packet/src/main/java/org/jclouds/packet/config/PacketHttpApiModule.java
new file mode 100644
index 0000000..e74bb19
--- /dev/null
+++ b/packet/src/main/java/org/jclouds/packet/config/PacketHttpApiModule.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.jclouds.packet.config;
+
+import org.jclouds.http.HttpErrorHandler;
+import org.jclouds.http.annotation.ClientError;
+import org.jclouds.http.annotation.Redirection;
+import org.jclouds.http.annotation.ServerError;
+import org.jclouds.location.suppliers.ImplicitLocationSupplier;
+import org.jclouds.location.suppliers.implicit.FirstRegion;
+import org.jclouds.packet.PacketApi;
+import org.jclouds.packet.domain.Href;
+import org.jclouds.packet.domain.options.ListOptions;
+import org.jclouds.packet.functions.LinkToListOptions;
+import org.jclouds.packet.handlers.PacketErrorHandler;
+import org.jclouds.rest.ConfiguresHttpApi;
+import org.jclouds.rest.config.HttpApiModule;
+
+import com.google.common.base.Function;
+import com.google.inject.Scopes;
+import com.google.inject.TypeLiteral;
+
+@ConfiguresHttpApi
+public class PacketHttpApiModule extends HttpApiModule<PacketApi> {
+
+   @Override
+   protected void configure() {
+      super.configure();
+      bind(ImplicitLocationSupplier.class).to(FirstRegion.class).in(Scopes.SINGLETON);
+      bind(new TypeLiteral<Function<Href, ListOptions>>() {
+      }).to(LinkToListOptions.class);
+   }
+
+   @Override
+   protected void bindErrorHandlers() {
+      bind(HttpErrorHandler.class).annotatedWith(Redirection.class).to(PacketErrorHandler.class);
+      bind(HttpErrorHandler.class).annotatedWith(ClientError.class).to(PacketErrorHandler.class);
+      bind(HttpErrorHandler.class).annotatedWith(ServerError.class).to(PacketErrorHandler.class);
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/070fe653/packet/src/main/java/org/jclouds/packet/domain/internal/PaginatedCollection.java
----------------------------------------------------------------------
diff --git a/packet/src/main/java/org/jclouds/packet/domain/internal/PaginatedCollection.java b/packet/src/main/java/org/jclouds/packet/domain/internal/PaginatedCollection.java
new file mode 100644
index 0000000..047151e
--- /dev/null
+++ b/packet/src/main/java/org/jclouds/packet/domain/internal/PaginatedCollection.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.jclouds.packet.domain.internal;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Iterator;
+import java.util.List;
+
+import org.jclouds.collect.IterableWithMarker;
+import org.jclouds.javax.annotation.Nullable;
+import org.jclouds.json.SerializedNames;
+import org.jclouds.packet.domain.Href;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Base class for all collections that return paginated results.
+ */
+public abstract class PaginatedCollection<T> extends IterableWithMarker<T> {
+
+   @AutoValue
+   public abstract static class Meta {
+      public abstract long total();
+      @Nullable public abstract Href first();
+      @Nullable public abstract Href previous();
+      @Nullable public abstract Href self();
+      @Nullable public abstract Href next();
+      @Nullable public abstract Href last();
+
+      @SerializedNames({ "total", "first", "previous", "self", "next", "last" })
+      public static Meta create(long total, Href first, Href previous, Href self, Href next, Href last) {
+         return new AutoValue_PaginatedCollection_Meta(total, first, previous, self, next, last);
+      }
+
+      Meta() { }
+   }
+
+   private final List<T> items;
+   private final Meta meta;
+
+   protected PaginatedCollection(List<T> items, Meta meta) {
+      this.items = ImmutableList.copyOf(checkNotNull(items, "items cannot be null"));
+      this.meta = checkNotNull(meta, "meta cannot be null");
+   }
+
+   public List<T> items() {
+      return items;
+   }
+
+   public Meta meta() {
+      return meta;
+   }
+
+   @Override
+   public Iterator<T> iterator() {
+      return items.iterator();
+   }
+
+   @Override
+   public Optional<Object> nextMarker() {
+      if (meta.next() == null) {
+         return Optional.absent();
+      }
+      return Optional.fromNullable((Object) meta.next());
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/070fe653/packet/src/main/java/org/jclouds/packet/domain/options/ListOptions.java
----------------------------------------------------------------------
diff --git a/packet/src/main/java/org/jclouds/packet/domain/options/ListOptions.java b/packet/src/main/java/org/jclouds/packet/domain/options/ListOptions.java
new file mode 100644
index 0000000..c858a7f
--- /dev/null
+++ b/packet/src/main/java/org/jclouds/packet/domain/options/ListOptions.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.jclouds.packet.domain.options;
+
+import org.jclouds.http.options.BaseHttpRequestOptions;
+
+/**
+ * Options to customize how paginated lists are returned.
+ */
+public class ListOptions extends BaseHttpRequestOptions {
+   public static final String PAGE_PARAM = "page";
+   public static final String PER_PAGE_PARAM = "per_page";
+   
+   /**
+    * Configures the number of entries to return in each page.
+    */
+   public ListOptions perPage(int perPage) {
+      queryParameters.put(PER_PAGE_PARAM, String.valueOf(perPage));
+      return this;
+   }
+   
+   /**
+    * Configures the number of the page to be returned.
+    */
+   public ListOptions page(int page) {
+      queryParameters.put(PAGE_PARAM, String.valueOf(page));
+      return this;
+   }
+   
+   public static final class Builder {
+      
+      /**
+       * @see {@link ListOptions#perPage(int)}
+       */
+      public static ListOptions perPage(int perPage) {
+         return new ListOptions().perPage(perPage);
+      }
+      
+      /**
+       * @see {@link ListOptions#page(int)}
+       */
+      public static ListOptions page(int page) {
+         return new ListOptions().page(page);
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/070fe653/packet/src/main/java/org/jclouds/packet/features/ProjectApi.java
----------------------------------------------------------------------
diff --git a/packet/src/main/java/org/jclouds/packet/features/ProjectApi.java b/packet/src/main/java/org/jclouds/packet/features/ProjectApi.java
new file mode 100644
index 0000000..e6bf0ca
--- /dev/null
+++ b/packet/src/main/java/org/jclouds/packet/features/ProjectApi.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.jclouds.packet.features;
+
+import java.beans.ConstructorProperties;
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.core.MediaType;
+
+import org.jclouds.Fallbacks;
+import org.jclouds.collect.IterableWithMarker;
+import org.jclouds.collect.PagedIterable;
+import org.jclouds.http.functions.ParseJson;
+import org.jclouds.json.Json;
+import org.jclouds.packet.PacketApi;
+import org.jclouds.packet.domain.Href;
+import org.jclouds.packet.domain.Project;
+import org.jclouds.packet.domain.internal.PaginatedCollection;
+import org.jclouds.packet.domain.options.ListOptions;
+import org.jclouds.packet.filters.AddXAuthTokenToRequest;
+import org.jclouds.packet.functions.BaseToPagedIterable;
+import org.jclouds.rest.annotations.Fallback;
+import org.jclouds.rest.annotations.RequestFilters;
+import org.jclouds.rest.annotations.ResponseParser;
+import org.jclouds.rest.annotations.Transform;
+
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.inject.TypeLiteral;
+
+@Path("/projects")
+@Consumes(MediaType.APPLICATION_JSON)
+@RequestFilters(AddXAuthTokenToRequest.class)
+public interface ProjectApi {
+
+
+    @Named("project:list")
+    @GET
+    @ResponseParser(ParseProjects.class)
+    @Transform(ParseProjects.ToPagedIterable.class)
+    @Fallback(Fallbacks.EmptyPagedIterableOnNotFoundOr404.class)
+    PagedIterable<Project> list();
+
+    @Named("project:list")
+    @GET
+    @ResponseParser(ParseProjects.class)
+    @Fallback(Fallbacks.EmptyIterableWithMarkerOnNotFoundOr404.class)
+    IterableWithMarker<Project> list(ListOptions options);
+
+    final class ParseProjects extends ParseJson<ParseProjects.Projects> {
+        @Inject
+        ParseProjects(Json json) {
+            super(json, TypeLiteral.get(Projects.class));
+        }
+
+        private static class Projects extends PaginatedCollection<Project> {
+            @ConstructorProperties({ "projects", "meta" })
+            public Projects(List<Project> items, Meta meta) {
+                super(items, meta);
+            }
+        }
+
+        private static class ToPagedIterable extends BaseToPagedIterable<Project, ListOptions> {
+            @Inject ToPagedIterable(PacketApi api, Function<Href, ListOptions> linkToOptions) {
+                super(api, linkToOptions);
+            }
+
+            @Override
+            protected IterableWithMarker<Project> fetchPageUsingOptions(ListOptions options, Optional<Object> arg0) {
+                return api.projectApi().list(options);
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/070fe653/packet/src/main/java/org/jclouds/packet/filters/AddXAuthTokenToRequest.java
----------------------------------------------------------------------
diff --git a/packet/src/main/java/org/jclouds/packet/filters/AddXAuthTokenToRequest.java b/packet/src/main/java/org/jclouds/packet/filters/AddXAuthTokenToRequest.java
new file mode 100644
index 0000000..e9d6bdd
--- /dev/null
+++ b/packet/src/main/java/org/jclouds/packet/filters/AddXAuthTokenToRequest.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.jclouds.packet.filters;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.jclouds.domain.Credentials;
+import org.jclouds.http.HttpException;
+import org.jclouds.http.HttpRequest;
+import org.jclouds.http.HttpRequestFilter;
+import org.jclouds.location.Provider;
+
+import com.google.common.base.Supplier;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+@Singleton
+public class AddXAuthTokenToRequest implements HttpRequestFilter {
+
+    private final Supplier<Credentials> creds;
+
+    @Inject
+    AddXAuthTokenToRequest(@Provider Supplier<Credentials> creds) {
+        this.creds = creds;
+    }
+
+    @Override
+    public HttpRequest filter(HttpRequest request) throws HttpException {
+        Credentials currentCreds = checkNotNull(creds.get(), "credential supplier returned null");
+        return request.toBuilder().replaceHeader("X-Auth-Token", currentCreds.credential).build();
+    }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/070fe653/packet/src/main/java/org/jclouds/packet/functions/BaseToPagedIterable.java
----------------------------------------------------------------------
diff --git a/packet/src/main/java/org/jclouds/packet/functions/BaseToPagedIterable.java b/packet/src/main/java/org/jclouds/packet/functions/BaseToPagedIterable.java
new file mode 100644
index 0000000..c5c275b
--- /dev/null
+++ b/packet/src/main/java/org/jclouds/packet/functions/BaseToPagedIterable.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.jclouds.packet.functions;
+
+import javax.inject.Inject;
+
+import org.jclouds.collect.IterableWithMarker;
+import org.jclouds.collect.internal.Arg0ToPagedIterable;
+import org.jclouds.packet.PacketApi;
+import org.jclouds.packet.domain.Href;
+import org.jclouds.packet.domain.options.ListOptions;
+
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+
+/**
+ * Base class to implement the functions that build the
+ * <code>PagedIterable</code>. Subclasses just need to override the
+ * {@link #fetchPageUsingOptions(ListOptions, Optional)} to invoke the right API
+ * method with the given options parameter to get the next page.
+ */
+public abstract class BaseToPagedIterable<T, O extends ListOptions> extends
+        Arg0ToPagedIterable<T, BaseToPagedIterable<T, O>> {
+   private final Function<Href, O> linkToOptions;
+   protected final PacketApi api;
+
+   @Inject protected BaseToPagedIterable(PacketApi api, Function<Href, O> linkToOptions) {
+      this.api = api;
+      this.linkToOptions = linkToOptions;
+   }
+
+   protected abstract IterableWithMarker<T> fetchPageUsingOptions(O options, Optional<Object> arg0);
+
+   @Override
+   protected Function<Object, IterableWithMarker<T>> markerToNextForArg0(final Optional<Object> arg0) {
+      return new Function<Object, IterableWithMarker<T>>() {
+         @Override
+         public IterableWithMarker<T> apply(Object input) {
+            O nextOptions = linkToOptions.apply(Href.class.cast(input));
+            return fetchPageUsingOptions(nextOptions, arg0);
+         }
+      };
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/070fe653/packet/src/main/java/org/jclouds/packet/functions/LinkToListOptions.java
----------------------------------------------------------------------
diff --git a/packet/src/main/java/org/jclouds/packet/functions/LinkToListOptions.java b/packet/src/main/java/org/jclouds/packet/functions/LinkToListOptions.java
new file mode 100644
index 0000000..4aef811
--- /dev/null
+++ b/packet/src/main/java/org/jclouds/packet/functions/LinkToListOptions.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.jclouds.packet.functions;
+
+import java.net.URI;
+
+import org.jclouds.packet.domain.Href;
+import org.jclouds.packet.domain.options.ListOptions;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Multimap;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Strings.emptyToNull;
+import static com.google.common.collect.Iterables.getFirst;
+import static org.jclouds.http.utils.Queries.queryParser;
+import static org.jclouds.packet.domain.options.ListOptions.PAGE_PARAM;
+import static org.jclouds.packet.domain.options.ListOptions.PER_PAGE_PARAM;
+
+/**
+ * Transforms an href returned by the API into a {@link ListOptions} that can be
+ * used to perform a request to get another page of a paginated list.
+ */
+public class LinkToListOptions implements Function<Href, ListOptions> {
+
+   @Override
+   public ListOptions apply(Href input) {
+      checkNotNull(input, "input cannot be null");
+
+      Multimap<String, String> queryParams = queryParser().apply(URI.create(input.href()).getQuery());
+      String nextPage = getFirstOrNull(PAGE_PARAM, queryParams);
+      String nextPerPage = getFirstOrNull(PER_PAGE_PARAM, queryParams);
+
+      ListOptions options = new ListOptions();
+      if (nextPage != null) {
+         options.page(Integer.parseInt(nextPage));
+      }
+      if (nextPerPage != null) {
+         options.perPage(Integer.parseInt(nextPerPage));
+      }
+
+      return options;
+   }
+
+   public static String getFirstOrNull(String key, Multimap<String, String> params) {
+      return params.containsKey(key) ? emptyToNull(getFirst(params.get(key), null)) : null;
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/070fe653/packet/src/main/java/org/jclouds/packet/handlers/PacketErrorHandler.java
----------------------------------------------------------------------
diff --git a/packet/src/main/java/org/jclouds/packet/handlers/PacketErrorHandler.java b/packet/src/main/java/org/jclouds/packet/handlers/PacketErrorHandler.java
new file mode 100644
index 0000000..01d870b
--- /dev/null
+++ b/packet/src/main/java/org/jclouds/packet/handlers/PacketErrorHandler.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.jclouds.packet.handlers;
+
+import javax.inject.Singleton;
+
+import org.jclouds.http.HttpCommand;
+import org.jclouds.http.HttpErrorHandler;
+import org.jclouds.http.HttpResponse;
+import org.jclouds.http.HttpResponseException;
+import org.jclouds.rest.AuthorizationException;
+import org.jclouds.rest.ResourceNotFoundException;
+
+import static org.jclouds.http.HttpUtils.closeClientButKeepContentStream;
+
+/**
+ * This will parse and set an appropriate exception on the command object.
+ */
+@Singleton
+public class PacketErrorHandler implements HttpErrorHandler {
+
+   public void handleError(HttpCommand command, HttpResponse response) {
+      // it is important to always read fully and close streams
+      byte[] data = closeClientButKeepContentStream(response);
+      String message = data != null ? new String(data) : null;
+
+      Exception exception = message != null ? new HttpResponseException(command, response, message)
+              : new HttpResponseException(command, response);
+      message = message != null ? message : String.format("%s -> %s", command.getCurrentRequest().getRequestLine(),
+              response.getStatusLine());
+      switch (response.getStatusCode()) {
+         case 400:
+            exception = new IllegalArgumentException(message, exception);
+            break;
+         case 401:
+         case 403:
+               exception = new AuthorizationException(message, exception);
+            break;
+         case 404:
+            if (!command.getCurrentRequest().getMethod().equals("DELETE")) {
+               exception = new ResourceNotFoundException(message, exception);
+            }
+            break;
+         case 409:
+            exception = new IllegalStateException(message, exception);
+            break;
+      }
+      command.setException(exception);
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/070fe653/packet/src/test/java/org/jclouds/packet/PacketProviderMetadataTest.java
----------------------------------------------------------------------
diff --git a/packet/src/test/java/org/jclouds/packet/PacketProviderMetadataTest.java b/packet/src/test/java/org/jclouds/packet/PacketProviderMetadataTest.java
new file mode 100644
index 0000000..cc0c8c5
--- /dev/null
+++ b/packet/src/test/java/org/jclouds/packet/PacketProviderMetadataTest.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.jclouds.packet;
+
+import org.jclouds.providers.internal.BaseProviderMetadataTest;
+import org.testng.annotations.Test;
+
+@Test(groups = "unit", testName = "PacketProviderMetadataTest")
+public class PacketProviderMetadataTest extends BaseProviderMetadataTest {
+
+   public PacketProviderMetadataTest() {
+      super(new PacketProviderMetadata(), new PacketApiMetadata());
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/070fe653/packet/src/test/java/org/jclouds/packet/compute/internal/BasePacketApiLiveTest.java
----------------------------------------------------------------------
diff --git a/packet/src/test/java/org/jclouds/packet/compute/internal/BasePacketApiLiveTest.java b/packet/src/test/java/org/jclouds/packet/compute/internal/BasePacketApiLiveTest.java
new file mode 100644
index 0000000..6c8cf63
--- /dev/null
+++ b/packet/src/test/java/org/jclouds/packet/compute/internal/BasePacketApiLiveTest.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.jclouds.packet.compute.internal;
+
+import java.util.Properties;
+import java.util.concurrent.TimeUnit;
+
+import org.jclouds.apis.BaseApiLiveTest;
+import org.jclouds.compute.config.ComputeServiceProperties;
+import org.jclouds.packet.PacketApi;
+
+import com.google.common.base.Predicate;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.Module;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Names;
+
+import static org.jclouds.compute.config.ComputeServiceProperties.TIMEOUT_NODE_RUNNING;
+import static org.jclouds.compute.config.ComputeServiceProperties.TIMEOUT_NODE_TERMINATED;
+import static org.testng.Assert.assertTrue;
+
+public class BasePacketApiLiveTest extends BaseApiLiveTest<PacketApi> {
+
+   private Predicate<String> deviceRunning;
+   private Predicate<String> deviceTerminated;
+
+   public BasePacketApiLiveTest() {
+      provider = "packet";
+   }
+
+   @Override
+   protected Properties setupProperties() {
+      Properties props = super.setupProperties();
+      props.put(ComputeServiceProperties.POLL_INITIAL_PERIOD, 1000);
+      props.put(ComputeServiceProperties.POLL_MAX_PERIOD, 10000);
+      props.put(ComputeServiceProperties.TIMEOUT_IMAGE_AVAILABLE, TimeUnit.MINUTES.toMillis(45));
+      return props;
+   }
+
+   @Override
+   protected PacketApi create(Properties props, Iterable<Module> modules) {
+      Injector injector = newBuilder().modules(modules).overrides(props).buildInjector();
+      deviceRunning = injector.getInstance(Key.get(new TypeLiteral<Predicate<String>>(){},
+            Names.named(TIMEOUT_NODE_RUNNING)));
+      deviceTerminated = injector.getInstance(Key.get(new TypeLiteral<Predicate<String>>(){},
+              Names.named(TIMEOUT_NODE_TERMINATED)));
+      return injector.getInstance(PacketApi.class);
+   }
+
+   protected void assertNodeRunning(String deviceId) {
+      assertTrue(deviceRunning.apply(deviceId), String.format("Device %s did not start in the configured timeout", deviceId));
+   }
+
+   protected void assertNodeTerminated(String deviceId) {
+      assertTrue(deviceTerminated.apply(deviceId), String.format("Device %s was not terminated in the configured timeout", deviceId));
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/070fe653/packet/src/test/java/org/jclouds/packet/compute/internal/BasePacketApiMockTest.java
----------------------------------------------------------------------
diff --git a/packet/src/test/java/org/jclouds/packet/compute/internal/BasePacketApiMockTest.java b/packet/src/test/java/org/jclouds/packet/compute/internal/BasePacketApiMockTest.java
new file mode 100644
index 0000000..5b8d6ae
--- /dev/null
+++ b/packet/src/test/java/org/jclouds/packet/compute/internal/BasePacketApiMockTest.java
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.jclouds.packet.compute.internal;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+
+import org.jclouds.ContextBuilder;
+import org.jclouds.concurrent.config.ExecutorServiceModule;
+import org.jclouds.json.Json;
+import org.jclouds.packet.PacketApi;
+import org.jclouds.packet.PacketProviderMetadata;
+import org.jclouds.rest.ApiContext;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.io.Resources;
+import com.google.common.reflect.TypeToken;
+import com.google.gson.JsonParser;
+import com.google.inject.Module;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+import com.squareup.okhttp.mockwebserver.RecordedRequest;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.Iterables.getOnlyElement;
+import static com.google.common.util.concurrent.MoreExecutors.sameThreadExecutor;
+import static org.jclouds.Constants.PROPERTY_MAX_RETRIES;
+import static org.testng.Assert.assertEquals;
+
+public class BasePacketApiMockTest {
+   
+   private static final String X_AUTHORIZATION_TOKEN = "c5401990f0c24135e8d6b5d260603fc71696d4738da9aa04a720229a01a2521d";
+   private static final String DEFAULT_ENDPOINT = new PacketProviderMetadata().getEndpoint();
+   
+   private final Set<Module> modules = ImmutableSet.<Module> of(new ExecutorServiceModule(sameThreadExecutor()));
+   
+   protected MockWebServer server;
+   protected PacketApi api;
+   private Json json;
+   
+   // So that we can ignore formatting.
+   private final JsonParser parser = new JsonParser();
+   
+   @BeforeMethod
+   public void start() throws IOException {
+      server = new MockWebServer();
+      server.play();
+      ApiContext<PacketApi> ctx = ContextBuilder.newBuilder("packet")
+            .credentials("", X_AUTHORIZATION_TOKEN)
+            .endpoint(url(""))
+            .modules(modules)
+            .overrides(overrides())
+            .build();
+      json = ctx.utils().injector().getInstance(Json.class);
+      api = ctx.getApi();
+   }
+
+   @AfterMethod(alwaysRun = true)
+   public void stop() throws IOException {
+      server.shutdown();
+      api.close();
+   }
+   
+   protected Properties overrides() {
+      Properties properties = new Properties();
+      properties.put(PROPERTY_MAX_RETRIES, "0"); // Do not retry
+      return properties;
+   }
+
+   protected String url(String path) {
+      return server.getUrl(path).toString();
+   }
+
+   protected MockResponse jsonResponse(String resource) {
+      return new MockResponse().addHeader("Content-Type", "application/json").setBody(stringFromResource(resource));
+   }
+
+   protected MockResponse response404() {
+      return new MockResponse().setStatus("HTTP/1.1 404 Not Found");
+   }
+   
+   protected MockResponse response204() {
+      return new MockResponse().setStatus("HTTP/1.1 204 No Content");
+   }
+
+   protected String stringFromResource(String resourceName) {
+      try {
+         return Resources.toString(getClass().getResource(resourceName), Charsets.UTF_8)
+               .replace(DEFAULT_ENDPOINT, url(""));
+      } catch (IOException e) {
+         throw Throwables.propagate(e);
+      }
+   }
+   
+   protected <T> T onlyObjectFromResource(String resourceName, TypeToken<Map<String, T>> type) {
+      // Assume JSON objects passed here will be in the form: { "entity": { ... } }
+      String text = stringFromResource(resourceName);
+      Map<String, T> object = json.fromJson(text, type.getType());
+      checkArgument(!object.isEmpty(), "The given json does not contain any object: %s", text);
+      checkArgument(object.keySet().size() == 1, "The given json does not contain more than one object: %s", text);
+      return object.get(getOnlyElement(object.keySet()));
+   }
+   
+   protected <T> T objectFromResource(String resourceName, Class<T> type) {
+      String text = stringFromResource(resourceName);
+      return json.fromJson(text, type);
+   }
+
+   protected RecordedRequest assertSent(MockWebServer server, String method, String path) throws InterruptedException {
+      RecordedRequest request = server.takeRequest();
+      assertEquals(request.getMethod(), method);
+      assertEquals(request.getPath(), path);
+      assertEquals(request.getHeader("Accept"), "application/json");
+      assertEquals(request.getHeader("X-Auth-Token"), X_AUTHORIZATION_TOKEN);
+      return request;
+   }
+
+   protected RecordedRequest assertSent(MockWebServer server, String method, String path, String json)
+         throws InterruptedException {
+      RecordedRequest request = assertSent(server, method, path);
+      assertEquals(request.getHeader("Content-Type"), "application/json");
+      assertEquals(parser.parse(new String(request.getBody(), Charsets.UTF_8)), parser.parse(json));
+      return request;
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/070fe653/packet/src/test/java/org/jclouds/packet/features/ProjectApiLiveTest.java
----------------------------------------------------------------------
diff --git a/packet/src/test/java/org/jclouds/packet/features/ProjectApiLiveTest.java b/packet/src/test/java/org/jclouds/packet/features/ProjectApiLiveTest.java
new file mode 100644
index 0000000..133e5ef
--- /dev/null
+++ b/packet/src/test/java/org/jclouds/packet/features/ProjectApiLiveTest.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.jclouds.packet.features;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.jclouds.packet.compute.internal.BasePacketApiLiveTest;
+import org.jclouds.packet.domain.Project;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+
+import static org.jclouds.packet.domain.options.ListOptions.Builder.page;
+import static org.testng.Assert.assertTrue;
+import static org.testng.util.Strings.isNullOrEmpty;
+
+@Test(groups = "live", testName = "ProjectApiLiveTest")
+public class ProjectApiLiveTest extends BasePacketApiLiveTest {
+
+   public void testListProjects() {
+      final AtomicInteger found = new AtomicInteger(0);
+      assertTrue(Iterables.all(api().list().concat(), new Predicate<Project>() {
+         @Override
+         public boolean apply(Project input) {
+            found.incrementAndGet();
+            return !isNullOrEmpty(input.id());
+         }
+      }), "All projects must have the 'id' field populated");
+      assertTrue(found.get() > 0, "Expected some projects to be returned");
+   }
+   
+   public void testListActionsOnePage() {
+      final AtomicInteger found = new AtomicInteger(0);
+      assertTrue(api().list(page(1).perPage(5)).allMatch(new Predicate<Project>() {
+         @Override
+         public boolean apply(Project input) {
+            found.incrementAndGet();
+            return !isNullOrEmpty(input.id());
+         }
+      }), "All projects must have the 'id' field populated");
+      assertTrue(found.get() > 0, "Expected some projects to be returned");
+   }
+   
+
+   private ProjectApi api() {
+      return api.projectApi();
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/070fe653/packet/src/test/java/org/jclouds/packet/features/ProjectApiMockTest.java
----------------------------------------------------------------------
diff --git a/packet/src/test/java/org/jclouds/packet/features/ProjectApiMockTest.java b/packet/src/test/java/org/jclouds/packet/features/ProjectApiMockTest.java
new file mode 100644
index 0000000..2899020
--- /dev/null
+++ b/packet/src/test/java/org/jclouds/packet/features/ProjectApiMockTest.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.jclouds.packet.features;
+
+import org.jclouds.packet.compute.internal.BasePacketApiMockTest;
+import org.jclouds.packet.domain.Project;
+import org.testng.annotations.Test;
+
+import static com.google.common.collect.Iterables.isEmpty;
+import static com.google.common.collect.Iterables.size;
+import static org.jclouds.packet.domain.options.ListOptions.Builder.page;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+@Test(groups = "unit", testName = "ProjectApiMockTest", singleThreaded = true)
+public class ProjectApiMockTest extends BasePacketApiMockTest {
+
+   public void testListProjects() throws InterruptedException {
+      server.enqueue(jsonResponse("/projects-first.json"));
+      server.enqueue(jsonResponse("/projects-last.json"));
+
+      Iterable<Project> projects = api.projectApi().list().concat();
+
+      assertEquals(size(projects), 8); // Force the PagedIterable to advance
+      assertEquals(server.getRequestCount(), 2);
+
+      assertSent(server, "GET", "/projects");
+      assertSent(server, "GET", "/projects?page=2");
+   }
+
+   public void testListProjectsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Project> projects = api.projectApi().list().concat();
+
+      assertTrue(isEmpty(projects));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/projects");
+   }
+
+   public void testListProjectsWithOptions() throws InterruptedException {
+      server.enqueue(jsonResponse("/projects-first.json"));
+
+      Iterable<Project> actions = api.projectApi().list(page(1).perPage(5));
+
+      assertEquals(size(actions), 5);
+      assertEquals(server.getRequestCount(), 1);
+
+      assertSent(server, "GET", "/projects?page=1&per_page=5");
+   }
+
+   public void testListProjectsWithOptionsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Project> actions = api.projectApi().list(page(1).perPage(5));
+
+      assertTrue(isEmpty(actions));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/projects?page=1&per_page=5");
+   }
+   
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/070fe653/packet/src/test/java/org/jclouds/packet/functions/LinkToListOptionsTest.java
----------------------------------------------------------------------
diff --git a/packet/src/test/java/org/jclouds/packet/functions/LinkToListOptionsTest.java b/packet/src/test/java/org/jclouds/packet/functions/LinkToListOptionsTest.java
new file mode 100644
index 0000000..15262e8
--- /dev/null
+++ b/packet/src/test/java/org/jclouds/packet/functions/LinkToListOptionsTest.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.jclouds.packet.functions;
+
+import org.jclouds.packet.domain.Href;
+import org.jclouds.packet.domain.options.ListOptions;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.Multimap;
+
+import static com.google.common.collect.Iterables.getOnlyElement;
+import static org.jclouds.packet.domain.options.ListOptions.PAGE_PARAM;
+import static org.jclouds.packet.domain.options.ListOptions.PER_PAGE_PARAM;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+
+@Test(groups = "unit", testName = "LinkToListOptionsTest")
+public class LinkToListOptionsTest {
+
+   public void testNoOptions() {
+      LinkToListOptions function = new LinkToListOptions();
+
+      ListOptions options = function.apply(Href.create("https://api.packet.net/projects"));
+      assertNotNull(options);
+
+      Multimap<String, String> params = options.buildQueryParameters();
+      assertFalse(params.containsKey(PAGE_PARAM));
+      assertFalse(params.containsKey(PER_PAGE_PARAM));
+   }
+
+   public void testWithOptions() {
+      LinkToListOptions function = new LinkToListOptions();
+
+      ListOptions options = function.apply(Href.create("https://api.packet.net/projects?page=2&per_page=5"));
+      assertNotNull(options);
+
+      Multimap<String, String> params = options.buildQueryParameters();
+      assertEquals(getOnlyElement(params.get(PAGE_PARAM)), "2");
+      assertEquals(getOnlyElement(params.get(PER_PAGE_PARAM)), "5");
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/070fe653/packet/src/test/resources/logback-test.xml
----------------------------------------------------------------------
diff --git a/packet/src/test/resources/logback-test.xml b/packet/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..cb55d49
--- /dev/null
+++ b/packet/src/test/resources/logback-test.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<configuration scan="false">
+    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
+        <file>target/test-data/jclouds.log</file>
+        <encoder>
+            <Pattern>%d %-5p [%c] [%thread] %m%n</Pattern>
+        </encoder>
+    </appender>
+    <appender name="WIREFILE" class="ch.qos.logback.core.FileAppender">
+        <file>target/test-data/jclouds-wire.log</file>
+        <encoder>
+            <Pattern>%d %-5p [%c] [%thread] %m%n</Pattern>
+        </encoder>
+    </appender>
+    <appender name="COMPUTEFILE" class="ch.qos.logback.core.FileAppender">
+        <file>target/jclouds-compute.log</file>
+        <encoder>
+            <Pattern>%d %-5p [%c] [%thread] %m%n</Pattern>
+        </encoder>
+    </appender>
+
+    <logger name="org.jclouds">
+        <level value="DEBUG" />
+        <appender-ref ref="FILE" />
+    </logger>
+    <logger name="jclouds.compute">
+        <level value="DEBUG" />
+        <appender-ref ref="COMPUTEFILE" />
+    </logger>
+    <logger name="jclouds.wire">
+        <level value="DEBUG" />
+        <appender-ref ref="WIREFILE" />
+    </logger>
+    <logger name="jclouds.headers">
+        <level value="DEBUG" />
+        <appender-ref ref="WIREFILE" />
+    </logger>
+
+    <root>
+        <level value="INFO" />
+    </root>
+</configuration>

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/070fe653/packet/src/test/resources/projects-first.json
----------------------------------------------------------------------
diff --git a/packet/src/test/resources/projects-first.json b/packet/src/test/resources/projects-first.json
new file mode 100644
index 0000000..f65d8e3
--- /dev/null
+++ b/packet/src/test/resources/projects-first.json
@@ -0,0 +1,315 @@
+{
+  "projects": [
+    {
+      "id": "93907f48-adfe-43ed-ad89-0e6e83721a54",
+      "name": "Cloudsoft CCS Testing",
+      "created_at": "2016-09-15T08:50:58Z",
+      "updated_at": "2017-01-05T09:36:53Z",
+      "max_devices": {
+        "baremetal_0": null,
+        "baremetal_1": null,
+        "baremetal_2": null,
+        "baremetal_3": null,
+        "baremetal_2a": null,
+        "storage_1": null,
+        "storage_2": null
+      },
+      "members": [
+        {
+          "href": "/users/1140617d-262d-4502-a3d6-771d83c930da"
+        },
+        {
+          "href": "/users/343345fe-18b3-46a3-9b9c-e4a2fe88ccbd"
+        },
+        {
+          "href": "/users/73b0442e-cc4b-42a0-8d3a-c8dfb8a4ff2e"
+        },
+        {
+          "href": "/users/ad711bc3-6333-449a-a405-23ca81f38c00"
+        }
+      ],
+      "memberships": [
+        {
+          "href": "/memberships/914facae-547f-46fc-93e8-860eb53d9bf6"
+        },
+        {
+          "href": "/memberships/1ce7c9d9-d11f-47f2-b5a6-d1221338ad69"
+        },
+        {
+          "href": "/memberships/e25c4478-9e44-465a-a9d7-bffc7a83300d"
+        },
+        {
+          "href": "/memberships/8a00c05b-3ddc-41c3-8dd7-4a3d5984ecec"
+        }
+      ],
+      "invitations": [],
+      "payment_method": {
+        "href": "/payment-methods/b2bb4ee0-506b-4c01-b17d-ba41d5c430c5"
+      },
+      "devices": [
+        {
+          "href": "/devices/4942a6c7-55c6-42cc-a06b-cccd6f2fa5df"
+        }
+      ],
+      "ssh_keys": [
+        {
+          "href": "/ssh-keys/070e3282-5b6a-4f75-8f18-a4e7488eafaa"
+        }
+      ],
+      "volumes": [],
+      "href": "/projects/93907f48-adfe-43ed-ad89-0e6e83721a54"
+    },
+    {
+      "id": "93907f48-adfe-43ed-ad89-0e6e83721a53",
+      "name": "Cloudsoft CCS Testing",
+      "created_at": "2016-09-15T08:50:58Z",
+      "updated_at": "2017-01-05T09:36:53Z",
+      "max_devices": {
+        "baremetal_0": null,
+        "baremetal_1": null,
+        "baremetal_2": null,
+        "baremetal_3": null,
+        "baremetal_2a": null,
+        "storage_1": null,
+        "storage_2": null
+      },
+      "members": [
+        {
+          "href": "/users/1140617d-262d-4502-a3d6-771d83c930da"
+        },
+        {
+          "href": "/users/343345fe-18b3-46a3-9b9c-e4a2fe88ccbd"
+        },
+        {
+          "href": "/users/73b0442e-cc4b-42a0-8d3a-c8dfb8a4ff2e"
+        },
+        {
+          "href": "/users/ad711bc3-6333-449a-a405-23ca81f38c00"
+        }
+      ],
+      "memberships": [
+        {
+          "href": "/memberships/914facae-547f-46fc-93e8-860eb53d9bf6"
+        },
+        {
+          "href": "/memberships/1ce7c9d9-d11f-47f2-b5a6-d1221338ad69"
+        },
+        {
+          "href": "/memberships/e25c4478-9e44-465a-a9d7-bffc7a83300d"
+        },
+        {
+          "href": "/memberships/8a00c05b-3ddc-41c3-8dd7-4a3d5984ecec"
+        }
+      ],
+      "invitations": [],
+      "payment_method": {
+        "href": "/payment-methods/b2bb4ee0-506b-4c01-b17d-ba41d5c430c5"
+      },
+      "devices": [
+        {
+          "href": "/devices/4942a6c7-55c6-42cc-a06b-cccd6f2fa5df"
+        }
+      ],
+      "ssh_keys": [
+        {
+          "href": "/ssh-keys/070e3282-5b6a-4f75-8f18-a4e7488eafaa"
+        }
+      ],
+      "volumes": [],
+      "href": "/projects/93907f48-adfe-43ed-ad89-0e6e83721a54"
+    },
+    {
+      "id": "93907f48-adfe-43ed-ad89-0e6e83721a52",
+      "name": "Cloudsoft CCS Testing",
+      "created_at": "2016-09-15T08:50:58Z",
+      "updated_at": "2017-01-05T09:36:53Z",
+      "max_devices": {
+        "baremetal_0": null,
+        "baremetal_1": null,
+        "baremetal_2": null,
+        "baremetal_3": null,
+        "baremetal_2a": null,
+        "storage_1": null,
+        "storage_2": null
+      },
+      "members": [
+        {
+          "href": "/users/1140617d-262d-4502-a3d6-771d83c930da"
+        },
+        {
+          "href": "/users/343345fe-18b3-46a3-9b9c-e4a2fe88ccbd"
+        },
+        {
+          "href": "/users/73b0442e-cc4b-42a0-8d3a-c8dfb8a4ff2e"
+        },
+        {
+          "href": "/users/ad711bc3-6333-449a-a405-23ca81f38c00"
+        }
+      ],
+      "memberships": [
+        {
+          "href": "/memberships/914facae-547f-46fc-93e8-860eb53d9bf6"
+        },
+        {
+          "href": "/memberships/1ce7c9d9-d11f-47f2-b5a6-d1221338ad69"
+        },
+        {
+          "href": "/memberships/e25c4478-9e44-465a-a9d7-bffc7a83300d"
+        },
+        {
+          "href": "/memberships/8a00c05b-3ddc-41c3-8dd7-4a3d5984ecec"
+        }
+      ],
+      "invitations": [],
+      "payment_method": {
+        "href": "/payment-methods/b2bb4ee0-506b-4c01-b17d-ba41d5c430c5"
+      },
+      "devices": [
+        {
+          "href": "/devices/4942a6c7-55c6-42cc-a06b-cccd6f2fa5df"
+        }
+      ],
+      "ssh_keys": [
+        {
+          "href": "/ssh-keys/070e3282-5b6a-4f75-8f18-a4e7488eafaa"
+        }
+      ],
+      "volumes": [],
+      "href": "/projects/93907f48-adfe-43ed-ad89-0e6e83721a54"
+    },
+    {
+      "id": "93907f48-adfe-43ed-ad89-0e6e83721a51",
+      "name": "Cloudsoft CCS Testing",
+      "created_at": "2016-09-15T08:50:58Z",
+      "updated_at": "2017-01-05T09:36:53Z",
+      "max_devices": {
+        "baremetal_0": null,
+        "baremetal_1": null,
+        "baremetal_2": null,
+        "baremetal_3": null,
+        "baremetal_2a": null,
+        "storage_1": null,
+        "storage_2": null
+      },
+      "members": [
+        {
+          "href": "/users/1140617d-262d-4502-a3d6-771d83c930da"
+        },
+        {
+          "href": "/users/343345fe-18b3-46a3-9b9c-e4a2fe88ccbd"
+        },
+        {
+          "href": "/users/73b0442e-cc4b-42a0-8d3a-c8dfb8a4ff2e"
+        },
+        {
+          "href": "/users/ad711bc3-6333-449a-a405-23ca81f38c00"
+        }
+      ],
+      "memberships": [
+        {
+          "href": "/memberships/914facae-547f-46fc-93e8-860eb53d9bf6"
+        },
+        {
+          "href": "/memberships/1ce7c9d9-d11f-47f2-b5a6-d1221338ad69"
+        },
+        {
+          "href": "/memberships/e25c4478-9e44-465a-a9d7-bffc7a83300d"
+        },
+        {
+          "href": "/memberships/8a00c05b-3ddc-41c3-8dd7-4a3d5984ecec"
+        }
+      ],
+      "invitations": [],
+      "payment_method": {
+        "href": "/payment-methods/b2bb4ee0-506b-4c01-b17d-ba41d5c430c5"
+      },
+      "devices": [
+        {
+          "href": "/devices/4942a6c7-55c6-42cc-a06b-cccd6f2fa5df"
+        }
+      ],
+      "ssh_keys": [
+        {
+          "href": "/ssh-keys/070e3282-5b6a-4f75-8f18-a4e7488eafaa"
+        }
+      ],
+      "volumes": [],
+      "href": "/projects/93907f48-adfe-43ed-ad89-0e6e83721a54"
+    },
+    {
+      "id": "93907f48-adfe-43ed-ad89-0e6e83721a50",
+      "name": "Cloudsoft CCS Testing",
+      "created_at": "2016-09-15T08:50:58Z",
+      "updated_at": "2017-01-05T09:36:53Z",
+      "max_devices": {
+        "baremetal_0": null,
+        "baremetal_1": null,
+        "baremetal_2": null,
+        "baremetal_3": null,
+        "baremetal_2a": null,
+        "storage_1": null,
+        "storage_2": null
+      },
+      "members": [
+        {
+          "href": "/users/1140617d-262d-4502-a3d6-771d83c930da"
+        },
+        {
+          "href": "/users/343345fe-18b3-46a3-9b9c-e4a2fe88ccbd"
+        },
+        {
+          "href": "/users/73b0442e-cc4b-42a0-8d3a-c8dfb8a4ff2e"
+        },
+        {
+          "href": "/users/ad711bc3-6333-449a-a405-23ca81f38c00"
+        }
+      ],
+      "memberships": [
+        {
+          "href": "/memberships/914facae-547f-46fc-93e8-860eb53d9bf6"
+        },
+        {
+          "href": "/memberships/1ce7c9d9-d11f-47f2-b5a6-d1221338ad69"
+        },
+        {
+          "href": "/memberships/e25c4478-9e44-465a-a9d7-bffc7a83300d"
+        },
+        {
+          "href": "/memberships/8a00c05b-3ddc-41c3-8dd7-4a3d5984ecec"
+        }
+      ],
+      "invitations": [],
+      "payment_method": {
+        "href": "/payment-methods/b2bb4ee0-506b-4c01-b17d-ba41d5c430c5"
+      },
+      "devices": [
+        {
+          "href": "/devices/4942a6c7-55c6-42cc-a06b-cccd6f2fa5df"
+        }
+      ],
+      "ssh_keys": [
+        {
+          "href": "/ssh-keys/070e3282-5b6a-4f75-8f18-a4e7488eafaa"
+        }
+      ],
+      "volumes": [],
+      "href": "/projects/93907f48-adfe-43ed-ad89-0e6e83721a54"
+    }
+  ],
+  "meta": {
+    "first": {
+      "href": "/projects?page=1"
+    },
+    "previous": null,
+    "self": {
+      "href": "/projects?page=1"
+    },
+    "next": {
+      "href": "/projects?page=2"
+    },
+    "last": {
+      "href": "/projects?page=2"
+    },
+    "total": 8
+  }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/070fe653/packet/src/test/resources/projects-last.json
----------------------------------------------------------------------
diff --git a/packet/src/test/resources/projects-last.json b/packet/src/test/resources/projects-last.json
new file mode 100644
index 0000000..3d44e67
--- /dev/null
+++ b/packet/src/test/resources/projects-last.json
@@ -0,0 +1,197 @@
+{
+  "projects": [
+    {
+      "id": "93907f48-adfe-43ed-ad89-0e6e83721a55",
+      "name": "Cloudsoft CCS Testing",
+      "created_at": "2016-09-15T08:50:58Z",
+      "updated_at": "2017-01-05T09:36:53Z",
+      "max_devices": {
+        "baremetal_0": null,
+        "baremetal_1": null,
+        "baremetal_2": null,
+        "baremetal_3": null,
+        "baremetal_2a": null,
+        "storage_1": null,
+        "storage_2": null
+      },
+      "members": [
+        {
+          "href": "/users/1140617d-262d-4502-a3d6-771d83c930da"
+        },
+        {
+          "href": "/users/343345fe-18b3-46a3-9b9c-e4a2fe88ccbd"
+        },
+        {
+          "href": "/users/73b0442e-cc4b-42a0-8d3a-c8dfb8a4ff2e"
+        },
+        {
+          "href": "/users/ad711bc3-6333-449a-a405-23ca81f38c00"
+        }
+      ],
+      "memberships": [
+        {
+          "href": "/memberships/914facae-547f-46fc-93e8-860eb53d9bf6"
+        },
+        {
+          "href": "/memberships/1ce7c9d9-d11f-47f2-b5a6-d1221338ad69"
+        },
+        {
+          "href": "/memberships/e25c4478-9e44-465a-a9d7-bffc7a83300d"
+        },
+        {
+          "href": "/memberships/8a00c05b-3ddc-41c3-8dd7-4a3d5984ecec"
+        }
+      ],
+      "invitations": [],
+      "payment_method": {
+        "href": "/payment-methods/b2bb4ee0-506b-4c01-b17d-ba41d5c430c5"
+      },
+      "devices": [
+        {
+          "href": "/devices/4942a6c7-55c6-42cc-a06b-cccd6f2fa5df"
+        }
+      ],
+      "ssh_keys": [
+        {
+          "href": "/ssh-keys/070e3282-5b6a-4f75-8f18-a4e7488eafaa"
+        }
+      ],
+      "volumes": [],
+      "href": "/projects/93907f48-adfe-43ed-ad89-0e6e83721a54"
+    },
+    {
+      "id": "93907f48-adfe-43ed-ad89-0e6e83721a56",
+      "name": "Cloudsoft CCS Testing",
+      "created_at": "2016-09-15T08:50:58Z",
+      "updated_at": "2017-01-05T09:36:53Z",
+      "max_devices": {
+        "baremetal_0": null,
+        "baremetal_1": null,
+        "baremetal_2": null,
+        "baremetal_3": null,
+        "baremetal_2a": null,
+        "storage_1": null,
+        "storage_2": null
+      },
+      "members": [
+        {
+          "href": "/users/1140617d-262d-4502-a3d6-771d83c930da"
+        },
+        {
+          "href": "/users/343345fe-18b3-46a3-9b9c-e4a2fe88ccbd"
+        },
+        {
+          "href": "/users/73b0442e-cc4b-42a0-8d3a-c8dfb8a4ff2e"
+        },
+        {
+          "href": "/users/ad711bc3-6333-449a-a405-23ca81f38c00"
+        }
+      ],
+      "memberships": [
+        {
+          "href": "/memberships/914facae-547f-46fc-93e8-860eb53d9bf6"
+        },
+        {
+          "href": "/memberships/1ce7c9d9-d11f-47f2-b5a6-d1221338ad69"
+        },
+        {
+          "href": "/memberships/e25c4478-9e44-465a-a9d7-bffc7a83300d"
+        },
+        {
+          "href": "/memberships/8a00c05b-3ddc-41c3-8dd7-4a3d5984ecec"
+        }
+      ],
+      "invitations": [],
+      "payment_method": {
+        "href": "/payment-methods/b2bb4ee0-506b-4c01-b17d-ba41d5c430c5"
+      },
+      "devices": [
+        {
+          "href": "/devices/4942a6c7-55c6-42cc-a06b-cccd6f2fa5df"
+        }
+      ],
+      "ssh_keys": [
+        {
+          "href": "/ssh-keys/070e3282-5b6a-4f75-8f18-a4e7488eafaa"
+        }
+      ],
+      "volumes": [],
+      "href": "/projects/93907f48-adfe-43ed-ad89-0e6e83721a54"
+    },
+    {
+      "id": "93907f48-adfe-43ed-ad89-0e6e83721a57",
+      "name": "Cloudsoft CCS Testing",
+      "created_at": "2016-09-15T08:50:58Z",
+      "updated_at": "2017-01-05T09:36:53Z",
+      "max_devices": {
+        "baremetal_0": null,
+        "baremetal_1": null,
+        "baremetal_2": null,
+        "baremetal_3": null,
+        "baremetal_2a": null,
+        "storage_1": null,
+        "storage_2": null
+      },
+      "members": [
+        {
+          "href": "/users/1140617d-262d-4502-a3d6-771d83c930da"
+        },
+        {
+          "href": "/users/343345fe-18b3-46a3-9b9c-e4a2fe88ccbd"
+        },
+        {
+          "href": "/users/73b0442e-cc4b-42a0-8d3a-c8dfb8a4ff2e"
+        },
+        {
+          "href": "/users/ad711bc3-6333-449a-a405-23ca81f38c00"
+        }
+      ],
+      "memberships": [
+        {
+          "href": "/memberships/914facae-547f-46fc-93e8-860eb53d9bf6"
+        },
+        {
+          "href": "/memberships/1ce7c9d9-d11f-47f2-b5a6-d1221338ad69"
+        },
+        {
+          "href": "/memberships/e25c4478-9e44-465a-a9d7-bffc7a83300d"
+        },
+        {
+          "href": "/memberships/8a00c05b-3ddc-41c3-8dd7-4a3d5984ecec"
+        }
+      ],
+      "invitations": [],
+      "payment_method": {
+        "href": "/payment-methods/b2bb4ee0-506b-4c01-b17d-ba41d5c430c5"
+      },
+      "devices": [
+        {
+          "href": "/devices/4942a6c7-55c6-42cc-a06b-cccd6f2fa5df"
+        }
+      ],
+      "ssh_keys": [
+        {
+          "href": "/ssh-keys/070e3282-5b6a-4f75-8f18-a4e7488eafaa"
+        }
+      ],
+      "volumes": [],
+      "href": "/projects/93907f48-adfe-43ed-ad89-0e6e83721a54"
+    }
+  ],
+  "meta": {
+    "first": {
+      "href": "/projects?page=1"
+    },
+    "previous": {
+      "href": "/projects?page=1"
+    },
+    "self": {
+      "href": "/projects?page=2"
+    },
+    "next": null,
+    "last": {
+      "href": "/projects?page=2"
+    },
+    "total": 8
+  }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/070fe653/packet/src/test/resources/projects.json
----------------------------------------------------------------------
diff --git a/packet/src/test/resources/projects.json b/packet/src/test/resources/projects.json
new file mode 100644
index 0000000..f7f1710
--- /dev/null
+++ b/packet/src/test/resources/projects.json
@@ -0,0 +1 @@
+{"projects":[{"id":"93907f48-adfe-43ed-ad89-0e6e83721a54","name":"Cloudsoft CCS Testing","created_at":"2016-09-15T08:50:58Z","updated_at":"2017-01-05T09:36:53Z","max_devices":{"baremetal_0":null,"baremetal_1":null,"baremetal_2":null,"baremetal_3":null,"baremetal_2a":null,"storage_1":null,"storage_2":null},"members":[{"href":"/users/1140617d-262d-4502-a3d6-771d83c930da"},{"href":"/users/343345fe-18b3-46a3-9b9c-e4a2fe88ccbd"},{"href":"/users/73b0442e-cc4b-42a0-8d3a-c8dfb8a4ff2e"},{"href":"/users/ad711bc3-6333-449a-a405-23ca81f38c00"}],"memberships":[{"href":"/memberships/914facae-547f-46fc-93e8-860eb53d9bf6"},{"href":"/memberships/1ce7c9d9-d11f-47f2-b5a6-d1221338ad69"},{"href":"/memberships/e25c4478-9e44-465a-a9d7-bffc7a83300d"},{"href":"/memberships/8a00c05b-3ddc-41c3-8dd7-4a3d5984ecec"}],"invitations":[],"payment_method":{"href":"/payment-methods/b2bb4ee0-506b-4c01-b17d-ba41d5c430c5"},"devices":[{"href":"/devices/4942a6c7-55c6-42cc-a06b-cccd6f2fa5df"}],"ssh_keys":[{"href":"/ssh-keys
 /070e3282-5b6a-4f75-8f18-a4e7488eafaa"}],"volumes":[],"href":"/projects/93907f48-adfe-43ed-ad89-0e6e83721a54"}],"meta":{"first":{"href":"/projects?page=1"},"previous":null,"self":{"href":"/projects?page=1"},"next":null,"last":{"href":"/projects?page=1"},"total":1}}