diff --git a/providers/packet/README.md b/providers/packet/README.md
new file mode 100644
index 0000000000..49e755386b
--- /dev/null
+++ b/providers/packet/README.md
@@ -0,0 +1,10 @@
+# Apache jclouds Packet (packet.net) provider
+
+
+## Pre-requisites
+
+If you are using Oracle JDK, you may get this exception
+```
+Caused by: javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure
+```
+when calling Packet endpoints. To solve this, you want to install JCE following the [official documentation](http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html).
diff --git a/providers/packet/pom.xml b/providers/packet/pom.xml
new file mode 100644
index 0000000000..8864894ba5
--- /dev/null
+++ b/providers/packet/pom.xml
@@ -0,0 +1,145 @@
+
+
+
+
+ 4.0.0
+
+ org.apache.jclouds.labs
+ jclouds-labs
+ 2.1.0-SNAPSHOT
+
+
+
+ packet
+ jclouds Packet compute API
+ jclouds components to access an implementation of Packet's Compute Service
+ bundle
+
+
+ https://api.packet.net/
+ projectId
+ api key
+ org.jclouds.packet*;version="${project.version}"
+ org.jclouds*;version="${project.version}",*
+
+
+
+
+ org.apache.jclouds
+ jclouds-compute
+ ${project.parent.version}
+
+
+ com.google.auto.service
+ auto-service
+ provided
+
+
+ com.google.auto.value
+ auto-value
+ provided
+
+
+
+ org.apache.jclouds
+ jclouds-core
+ ${project.parent.version}
+ test-jar
+ test
+
+
+ org.apache.jclouds
+ jclouds-compute
+ ${project.parent.version}
+ test-jar
+ test
+
+
+ org.apache.jclouds.driver
+ jclouds-slf4j
+ ${project.parent.version}
+ test-jar
+ test
+
+
+ org.apache.jclouds.driver
+ jclouds-slf4j
+ ${project.parent.version}
+ test
+
+
+ ch.qos.logback
+ logback-classic
+ test
+
+
+ org.apache.jclouds.driver
+ jclouds-sshj
+ ${project.parent.version}
+ test
+
+
+ com.squareup.okhttp
+ mockwebserver
+ test
+
+
+
+ org.bouncycastle
+ bcprov-jdk15on
+
+
+
+
+
+
+
+ live
+
+
+ clean verify
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ integration
+ integration-test
+
+ test
+
+
+
+ ${test.packet.endpoint}
+ ${test.packet.identity}
+ ${test.packet.credential}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/providers/packet/src/main/java/org/jclouds/packet/PacketApi.java b/providers/packet/src/main/java/org/jclouds/packet/PacketApi.java
new file mode 100644
index 0000000000..e5f84e33f2
--- /dev/null
+++ b/providers/packet/src/main/java/org/jclouds/packet/PacketApi.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.io.Closeable;
+
+import javax.ws.rs.PathParam;
+
+import org.jclouds.packet.features.DeviceApi;
+import org.jclouds.packet.features.FacilityApi;
+import org.jclouds.packet.features.OperatingSystemApi;
+import org.jclouds.packet.features.PlanApi;
+import org.jclouds.packet.features.ProjectApi;
+import org.jclouds.packet.features.SshKeyApi;
+import org.jclouds.rest.annotations.Delegate;
+
+/**
+ * The Packet API is a REST API for managing your services and deployments.
+ *
+ *
+ * @see doc
+ */
+public interface PacketApi extends Closeable {
+
+ /**
+ * The Packet API includes operations for managing project.
+ *
+ * @see docs
+ */
+ @Delegate
+ ProjectApi projectApi();
+
+ /**
+ * This Packet API provides all of the devices
+ *
+ * @see docs
+ */
+ @Delegate
+ DeviceApi deviceApi(@PathParam("projectId") String projectId);
+
+ /**
+ * This Packet API provides all of the facilities
+ *
+ * @see docs
+ */
+ @Delegate
+ FacilityApi facilityApi();
+
+ /**
+ * This Packet API provides all of the plans
+ *
+ * @see docs
+ */
+ @Delegate
+ PlanApi planApi();
+
+ /**
+ * This Packet API provides all of the operating systems
+ *
+ * @see docs
+ */
+ @Delegate
+ OperatingSystemApi operatingSystemApi();
+
+ /**
+ * This Packet API provides all of the operating systems
+ *
+ * @see docs
+ */
+ @Delegate
+ SshKeyApi sshKeyApi();
+}
diff --git a/providers/packet/src/main/java/org/jclouds/packet/PacketApiMetadata.java b/providers/packet/src/main/java/org/jclouds/packet/PacketApiMetadata.java
new file mode 100644
index 0000000000..3d27de8608
--- /dev/null
+++ b/providers/packet/src/main/java/org/jclouds/packet/PacketApiMetadata.java
@@ -0,0 +1,92 @@
+/*
+ * 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.compute.ComputeServiceContext;
+import org.jclouds.packet.compute.config.PacketComputeServiceContextModule;
+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;
+import static org.jclouds.compute.config.ComputeServiceProperties.TIMEOUT_NODE_SUSPENDED;
+import static org.jclouds.reflect.Reflection2.typeToken;
+
+/**
+ * Implementation of {@link ApiMetadata} for Packet API
+ */
+public class PacketApiMetadata extends BaseHttpApiMetadata {
+
+ @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, 900000); // 15 mins
+ properties.put(TIMEOUT_NODE_SUSPENDED, 900000); // 15 mins
+ return properties;
+ }
+
+ public static class Builder extends BaseHttpApiMetadata.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())
+ .version("1")
+ .view(typeToken(ComputeServiceContext.class))
+ .defaultModules(ImmutableSet.>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;
+ }
+ }
+}
diff --git a/providers/packet/src/main/java/org/jclouds/packet/PacketProviderMetadata.java b/providers/packet/src/main/java/org/jclouds/packet/PacketProviderMetadata.java
new file mode 100644
index 0000000000..69e0918182
--- /dev/null
+++ b/providers/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;
+ }
+ }
+}
+
+
diff --git a/providers/packet/src/main/java/org/jclouds/packet/compute/PacketComputeServiceAdapter.java b/providers/packet/src/main/java/org/jclouds/packet/compute/PacketComputeServiceAdapter.java
new file mode 100644
index 0000000000..3672b14125
--- /dev/null
+++ b/providers/packet/src/main/java/org/jclouds/packet/compute/PacketComputeServiceAdapter.java
@@ -0,0 +1,184 @@
+/*
+ * 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;
+
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Resource;
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.jclouds.compute.ComputeServiceAdapter;
+import org.jclouds.compute.domain.Template;
+import org.jclouds.compute.reference.ComputeServiceConstants;
+import org.jclouds.domain.Credentials;
+import org.jclouds.location.Provider;
+import org.jclouds.logging.Logger;
+import org.jclouds.packet.PacketApi;
+import org.jclouds.packet.compute.options.PacketTemplateOptions;
+import org.jclouds.packet.domain.BillingCycle;
+import org.jclouds.packet.domain.Device;
+import org.jclouds.packet.domain.Facility;
+import org.jclouds.packet.domain.OperatingSystem;
+import org.jclouds.packet.domain.Plan;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
+import com.google.common.base.Supplier;
+import com.google.common.collect.Iterables;
+
+import static com.google.common.collect.Iterables.contains;
+import static com.google.common.collect.Iterables.filter;
+
+/**
+ * defines the connection between the {@link org.jclouds.packet.PacketApi} implementation and
+ * the jclouds {@link org.jclouds.compute.ComputeService}
+ */
+@Singleton
+public class PacketComputeServiceAdapter implements ComputeServiceAdapter {
+
+ @Resource
+ @Named(ComputeServiceConstants.COMPUTE_LOGGER)
+ protected Logger logger = Logger.NULL;
+
+ private final PacketApi api;
+ private final String projectId;
+
+ @Inject
+ PacketComputeServiceAdapter(PacketApi api, @Provider final Supplier creds) {
+ this.api = api;
+ this.projectId = creds.get().identity;
+ }
+
+ @Override
+ public NodeAndInitialCredentials createNodeWithGroupEncodedIntoName(String group, String name, Template template) {
+
+ PacketTemplateOptions templateOptions = template.getOptions().as(PacketTemplateOptions.class);
+ Map features = templateOptions.getFeatures();
+ BillingCycle billingCycle = BillingCycle.fromValue(templateOptions.getBillingCycle());
+ boolean locked = templateOptions.isLocked();
+ String userdata = templateOptions.getUserData();
+ Set tags = templateOptions.getTags();
+
+ String plan = template.getHardware().getId();
+ String facility = template.getLocation().getId();
+ String operatingSystem = template.getImage().getId();
+
+ Device device = api.deviceApi(projectId).create(
+ Device.CreateDevice.builder()
+ .hostname(name)
+ .plan(plan)
+ .billingCycle(billingCycle.value())
+ .facility(facility)
+ .features(features)
+ .operatingSystem(operatingSystem)
+ .locked(locked)
+ .userdata(userdata)
+ .tags(tags)
+ .build()
+ );
+
+ // Any new servers you deploy to projects you are a collaborator on will have your project and personal SSH keys, if defined.
+ // If no SSH keys are defined in your account, jclouds will generate one usiing CreateSshKeysThenCreateNodes
+ // so that it will add it to the device with the default mechanism.
+
+ // Safe to pass null credentials here, as jclouds will default populate
+ // the node with the default credentials from the image, or the ones in
+ // the options, if provided.
+ return new NodeAndInitialCredentials(device, device.id(), null);
+ }
+
+ @Override
+ public Iterable listHardwareProfiles() {
+ return Iterables.filter(api.planApi().list().concat(), new Predicate() {
+ @Override
+ public boolean apply(Plan input) {
+ return input.line().equals("baremetal");
+ }
+ });
+ }
+
+ @Override
+ public Iterable listImages() {
+ return api.operatingSystemApi().list().concat();
+ }
+
+ @Override
+ public OperatingSystem getImage(final String id) {
+ Optional firstInterestingOperatingSystem = api
+ .operatingSystemApi().list()
+ .concat()
+ .firstMatch(new Predicate() {
+ @Override
+ public boolean apply(OperatingSystem input) {
+ return input.slug().equals(id);
+ }
+ });
+ if (!firstInterestingOperatingSystem.isPresent()) {
+ throw new IllegalStateException("Cannot find image with the required slug " + id);
+ }
+ return firstInterestingOperatingSystem.get();
+ }
+
+ @Override
+ public Iterable listLocations() {
+ return api.facilityApi().list().concat();
+ }
+
+ @Override
+ public Device getNode(String id) {
+ return api.deviceApi(projectId).get(id);
+ }
+
+ @Override
+ public void destroyNode(String id) {
+ api.deviceApi(projectId).delete(id);
+ }
+
+ @Override
+ public void rebootNode(String id) {
+ api.deviceApi(projectId).reboot(id);
+ }
+
+ @Override
+ public void resumeNode(String id) {
+ api.deviceApi(projectId).powerOn(id);
+ }
+
+ @Override
+ public void suspendNode(String id) {
+ api.deviceApi(projectId).powerOff(id);
+ }
+
+ @Override
+ public Iterable listNodes() {
+ return api.deviceApi(projectId).list().concat();
+ }
+
+ @Override
+ public Iterable listNodesByIds(final Iterable ids) {
+ return filter(listNodes(), new Predicate() {
+ @Override
+ public boolean apply(Device device) {
+ return contains(ids, String.valueOf(device.id()));
+ }
+ });
+ }
+
+}
diff --git a/providers/packet/src/main/java/org/jclouds/packet/compute/config/PacketComputeServiceContextModule.java b/providers/packet/src/main/java/org/jclouds/packet/compute/config/PacketComputeServiceContextModule.java
new file mode 100644
index 0000000000..0a64f43ee8
--- /dev/null
+++ b/providers/packet/src/main/java/org/jclouds/packet/compute/config/PacketComputeServiceContextModule.java
@@ -0,0 +1,151 @@
+/*
+ * 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.config;
+
+import org.jclouds.compute.ComputeServiceAdapter;
+import org.jclouds.compute.config.ComputeServiceAdapterContextModule;
+import org.jclouds.compute.domain.Hardware;
+import org.jclouds.compute.domain.Image;
+import org.jclouds.compute.domain.NodeMetadata;
+import org.jclouds.compute.options.TemplateOptions;
+import org.jclouds.compute.reference.ComputeServiceConstants;
+import org.jclouds.compute.strategy.CreateNodesInGroupThenAddToSet;
+import org.jclouds.domain.Credentials;
+import org.jclouds.domain.Location;
+import org.jclouds.location.Provider;
+import org.jclouds.packet.PacketApi;
+import org.jclouds.packet.compute.PacketComputeServiceAdapter;
+import org.jclouds.packet.compute.functions.DeviceStateToStatus;
+import org.jclouds.packet.compute.functions.DeviceToNodeMetadata;
+import org.jclouds.packet.compute.functions.FacilityToLocation;
+import org.jclouds.packet.compute.functions.OperatingSystemToImage;
+import org.jclouds.packet.compute.functions.PlanToHardware;
+import org.jclouds.packet.compute.options.PacketTemplateOptions;
+import org.jclouds.packet.compute.strategy.CreateSshKeysThenCreateNodes;
+import org.jclouds.packet.domain.Device;
+import org.jclouds.packet.domain.Facility;
+import org.jclouds.packet.domain.OperatingSystem;
+import org.jclouds.packet.domain.Plan;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.base.Supplier;
+import com.google.inject.Provides;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Named;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.jclouds.compute.config.ComputeServiceProperties.TIMEOUT_NODE_RUNNING;
+import static org.jclouds.compute.config.ComputeServiceProperties.TIMEOUT_NODE_SUSPENDED;
+import static org.jclouds.compute.config.ComputeServiceProperties.TIMEOUT_NODE_TERMINATED;
+import static org.jclouds.util.Predicates2.retry;
+
+public class PacketComputeServiceContextModule extends
+ ComputeServiceAdapterContextModule {
+
+ @SuppressWarnings("unchecked")
+ @Override
+ protected void configure() {
+ super.configure();
+
+ bind(new TypeLiteral>() {
+ }).to(PacketComputeServiceAdapter.class);
+
+ bind(new TypeLiteral>() {
+ }).to(DeviceToNodeMetadata.class);
+ bind(new TypeLiteral>() {
+ }).to(PlanToHardware.class);
+ bind(new TypeLiteral>() {
+ }).to(OperatingSystemToImage.class);
+ bind(new TypeLiteral>() {
+ }).to(FacilityToLocation.class);
+ bind(new TypeLiteral>() {
+ }).to(DeviceStateToStatus.class);
+ install(new LocationsFromComputeServiceAdapterModule() {
+ });
+ bind(TemplateOptions.class).to(PacketTemplateOptions.class);
+ bind(CreateNodesInGroupThenAddToSet.class).to(CreateSshKeysThenCreateNodes.class);
+ }
+
+ @Provides
+ @Named(TIMEOUT_NODE_RUNNING)
+ protected Predicate provideDeviceRunningPredicate(final PacketApi api,
+ @Provider final Supplier creds,
+ ComputeServiceConstants.Timeouts timeouts,
+ ComputeServiceConstants.PollPeriod pollPeriod) {
+ return retry(new DeviceInStatusPredicate(api, creds.get().identity, Device.State.ACTIVE), timeouts.nodeRunning,
+ pollPeriod.pollInitialPeriod, pollPeriod.pollMaxPeriod);
+ }
+
+ @Provides
+ @Named(TIMEOUT_NODE_SUSPENDED)
+ protected Predicate provideDeviceSuspendedPredicate(final PacketApi api, @Provider final Supplier creds, ComputeServiceConstants.Timeouts timeouts,
+ ComputeServiceConstants.PollPeriod pollPeriod) {
+ return retry(new DeviceInStatusPredicate(api, creds.get().identity, Device.State.INACTIVE), timeouts.nodeSuspended,
+ pollPeriod.pollInitialPeriod, pollPeriod.pollMaxPeriod);
+ }
+
+ @Provides
+ @Named(TIMEOUT_NODE_TERMINATED)
+ protected Predicate provideDeviceTerminatedPredicate(final PacketApi api, @Provider final Supplier creds, ComputeServiceConstants.Timeouts timeouts,
+ ComputeServiceConstants.PollPeriod pollPeriod) {
+ return retry(new DeviceTerminatedPredicate(api, creds.get().identity), timeouts.nodeTerminated, pollPeriod.pollInitialPeriod,
+ pollPeriod.pollMaxPeriod);
+ }
+
+ @VisibleForTesting
+ static class DeviceInStatusPredicate implements Predicate {
+
+ private final PacketApi api;
+ private final String projectId;
+ private final Device.State state;
+
+ public DeviceInStatusPredicate(PacketApi api, String projectId, Device.State state) {
+ this.api = checkNotNull(api, "api must not be null");
+ this.projectId = checkNotNull(projectId, "projectId must not be null");
+ this.state = checkNotNull(state, "state must not be null");
+ }
+
+ @Override
+ public boolean apply(String input) {
+ checkNotNull(input, "device id");
+ Device device = api.deviceApi(projectId).get(input);
+ return device != null && state == device.state();
+ }
+ }
+
+ @VisibleForTesting
+ static class DeviceTerminatedPredicate implements Predicate {
+
+ private final PacketApi api;
+ private final String projectId;
+
+ public DeviceTerminatedPredicate(PacketApi api, String projectId) {
+ this.api = checkNotNull(api, "api must not be null");
+ this.projectId = checkNotNull(projectId, "projectId must not be null");
+ }
+
+ @Override
+ public boolean apply(String input) {
+ checkNotNull(input, "device id");
+ Device device = api.deviceApi(projectId).get(input);
+ return device == null;
+ }
+ }
+
+}
diff --git a/providers/packet/src/main/java/org/jclouds/packet/compute/functions/DeviceStateToStatus.java b/providers/packet/src/main/java/org/jclouds/packet/compute/functions/DeviceStateToStatus.java
new file mode 100644
index 0000000000..7606bf5aca
--- /dev/null
+++ b/providers/packet/src/main/java/org/jclouds/packet/compute/functions/DeviceStateToStatus.java
@@ -0,0 +1,50 @@
+/*
+ * 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.functions;
+
+import javax.inject.Singleton;
+
+import org.jclouds.compute.domain.NodeMetadata.Status;
+import org.jclouds.packet.domain.Device;
+
+import com.google.common.base.Function;
+import com.google.common.base.Functions;
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * Transforms an {@link Device.State} to the jclouds portable model.
+ */
+@Singleton
+public class DeviceStateToStatus implements Function {
+
+ private static final Function toPortableStatus = Functions.forMap(
+ ImmutableMap. builder()
+ .put(Device.State.PROVISIONING, Status.PENDING)
+ .put(Device.State.POWERING_ON, Status.PENDING)
+ .put(Device.State.POWERING_OFF, Status.PENDING)
+ .put(Device.State.REBOOTING, Status.PENDING)
+ .put(Device.State.QUEUED, Status.PENDING)
+ .put(Device.State.INACTIVE, Status.SUSPENDED)
+ .put(Device.State.ACTIVE, Status.RUNNING)
+ .build(),
+ Status.UNRECOGNIZED);
+
+ @Override
+ public Status apply(final Device.State input) {
+ return toPortableStatus.apply(input);
+ }
+}
diff --git a/providers/packet/src/main/java/org/jclouds/packet/compute/functions/DeviceToNodeMetadata.java b/providers/packet/src/main/java/org/jclouds/packet/compute/functions/DeviceToNodeMetadata.java
new file mode 100644
index 0000000000..ec222fc40b
--- /dev/null
+++ b/providers/packet/src/main/java/org/jclouds/packet/compute/functions/DeviceToNodeMetadata.java
@@ -0,0 +1,110 @@
+/*
+ * 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.functions;
+
+import java.util.List;
+
+import javax.annotation.Resource;
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.jclouds.compute.domain.NodeMetadata;
+import org.jclouds.compute.domain.NodeMetadataBuilder;
+import org.jclouds.compute.functions.GroupNamingConvention;
+import org.jclouds.compute.reference.ComputeServiceConstants;
+import org.jclouds.logging.Logger;
+import org.jclouds.packet.domain.Device;
+import org.jclouds.packet.domain.IpAddress;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+
+import static com.google.common.collect.FluentIterable.from;
+
+/**
+ * Transforms an {@link Device} to the jclouds portable model.
+ */
+@Singleton
+public class DeviceToNodeMetadata implements Function {
+
+ @Resource
+ @Named(ComputeServiceConstants.COMPUTE_LOGGER)
+ protected Logger logger = Logger.NULL;
+
+ private final PlanToHardware planToHardware;
+ private final OperatingSystemToImage operatingSystemToImage;
+ private final FacilityToLocation facilityToLocation;
+ private final Function toPortableStatus;
+ private final GroupNamingConvention groupNamingConvention;
+
+ @Inject
+ DeviceToNodeMetadata(PlanToHardware planToHardware, OperatingSystemToImage operatingSystemToImage, FacilityToLocation facilityToLocation,
+ Function toPortableStatus,
+ GroupNamingConvention.Factory groupNamingConvention) {
+ this.planToHardware = planToHardware;
+ this.operatingSystemToImage = operatingSystemToImage;
+ this.facilityToLocation = facilityToLocation;
+ this.toPortableStatus = toPortableStatus;
+ this.groupNamingConvention = groupNamingConvention.createWithoutPrefix();
+ }
+
+ @Override
+ public NodeMetadata apply(Device input) {
+ return new NodeMetadataBuilder()
+ .ids(input.id())
+ .name(input.hostname())
+ .hostname(String.format("%s", input.hostname()))
+ .group(groupNamingConvention.extractGroup(input.hostname()))
+ .location(facilityToLocation.apply(input.facility()))
+ .hardware(planToHardware.apply(input.plan()))
+ .imageId(input.operatingSystem().slug())
+ .operatingSystem(operatingSystemToImage.apply(input.operatingSystem()).getOperatingSystem())
+ .status(toPortableStatus.apply(input.state()))
+ .publicAddresses(getPublicIpAddresses(input.ipAddresses()))
+ .privateAddresses(getPrivateIpAddresses(input.ipAddresses()))
+ .tags(input.tags())
+ .build();
+ }
+
+ private Iterable getPublicIpAddresses(List input) {
+ return filterAndTransformIpAddresses(input, new IsPublicIpAddress());
+ }
+
+ private Iterable getPrivateIpAddresses(List input) {
+ return filterAndTransformIpAddresses(input, Predicates.not(new IsPublicIpAddress()));
+ }
+
+ private Iterable filterAndTransformIpAddresses(List input, Predicate filter) {
+ return from(input).filter(filter).transform(new IpAddressToIp());
+ }
+
+ private static class IpAddressToIp implements Function {
+ @Override
+ public String apply(final IpAddress input) {
+ return input.address();
+ }
+ }
+
+ private static class IsPublicIpAddress implements Predicate {
+ @Override
+ public boolean apply(IpAddress input) {
+ return input.publicAddress();
+ }
+ }
+}
diff --git a/providers/packet/src/main/java/org/jclouds/packet/compute/functions/FacilityToLocation.java b/providers/packet/src/main/java/org/jclouds/packet/compute/functions/FacilityToLocation.java
new file mode 100644
index 0000000000..c8df1bc74d
--- /dev/null
+++ b/providers/packet/src/main/java/org/jclouds/packet/compute/functions/FacilityToLocation.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.compute.functions;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.jclouds.domain.Location;
+import org.jclouds.domain.LocationBuilder;
+import org.jclouds.domain.LocationScope;
+import org.jclouds.location.suppliers.all.JustProvider;
+import org.jclouds.packet.domain.Facility;
+
+import com.google.common.base.Function;
+
+import static com.google.common.collect.Iterables.getOnlyElement;
+
+/**
+ * Transforms an {@link Facility} to the jclouds portable model.
+ */
+@Singleton
+public class FacilityToLocation implements Function {
+
+ private final JustProvider justProvider;
+
+ // allow us to lazy discover the provider of a resource
+ @Inject
+ FacilityToLocation(JustProvider justProvider) {
+ this.justProvider = justProvider;
+ }
+
+ @Override
+ public Location apply(final Facility facility) {
+ final LocationBuilder builder = new LocationBuilder();
+ builder.id(facility.code());
+ builder.description(facility.name());
+ builder.parent(getOnlyElement(justProvider.get()));
+ builder.scope(LocationScope.REGION);
+ return builder.build();
+ }
+}
diff --git a/providers/packet/src/main/java/org/jclouds/packet/compute/functions/OperatingSystemToImage.java b/providers/packet/src/main/java/org/jclouds/packet/compute/functions/OperatingSystemToImage.java
new file mode 100644
index 0000000000..d28b579dfe
--- /dev/null
+++ b/providers/packet/src/main/java/org/jclouds/packet/compute/functions/OperatingSystemToImage.java
@@ -0,0 +1,54 @@
+/*
+ * 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.functions;
+
+import javax.inject.Singleton;
+
+import org.jclouds.compute.domain.Image;
+import org.jclouds.compute.domain.ImageBuilder;
+import org.jclouds.packet.domain.Distribution;
+import org.jclouds.packet.domain.OperatingSystem;
+
+import com.google.common.base.Function;
+
+import static org.jclouds.compute.domain.OperatingSystem.builder;
+
+/**
+ * Transforms an {@link OperatingSystem} to the jclouds portable model.
+ */
+@Singleton
+public class OperatingSystemToImage implements Function {
+
+ @Override
+ public Image apply(final OperatingSystem input) {
+ ImageBuilder builder = new ImageBuilder();
+ builder.ids(input.slug());
+ builder.name(input.name());
+ builder.description(input.name());
+ builder.status(Image.Status.AVAILABLE);
+
+ builder.operatingSystem(builder()
+ .name(input.name())
+ .family(Distribution.fromValue(input.distribution()).osFamily())
+ .description(input.name())
+ .version(input.version())
+ .is64Bit(true)
+ .build());
+
+ return builder.build();
+ }
+}
diff --git a/providers/packet/src/main/java/org/jclouds/packet/compute/functions/PlanToHardware.java b/providers/packet/src/main/java/org/jclouds/packet/compute/functions/PlanToHardware.java
new file mode 100644
index 0000000000..e6ac2efcb1
--- /dev/null
+++ b/providers/packet/src/main/java/org/jclouds/packet/compute/functions/PlanToHardware.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.compute.functions;
+
+import javax.inject.Singleton;
+
+import org.jclouds.compute.domain.Hardware;
+import org.jclouds.compute.domain.HardwareBuilder;
+import org.jclouds.compute.domain.Processor;
+import org.jclouds.compute.domain.Volume;
+import org.jclouds.compute.domain.internal.VolumeImpl;
+import org.jclouds.packet.domain.Plan;
+import org.jclouds.packet.domain.Specs;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+
+/**
+ * Transforms an {@link Plan} to the jclouds portable model.
+ */
+@Singleton
+public class PlanToHardware implements Function {
+
+ @Override
+ public Hardware apply(Plan plan) {
+ HardwareBuilder builder = new HardwareBuilder()
+ .ids(plan.slug())
+ .name(plan.name())
+ .hypervisor("none")
+ .processors(getProcessors(plan))
+ .ram(getMemory(plan))
+ .volumes(getVolumes(plan));
+ return builder.build();
+ }
+
+ private Integer getMemory(Plan plan) {
+ if (plan.specs() == null || plan.specs().drives() == null) return 0;
+ String total = plan.specs().memory().total();
+ if (total.endsWith("GB")) {
+ return Integer.valueOf(total.substring(0, total.length() - 2)) * 1024;
+ } else {
+ throw new IllegalArgumentException("Cannot parse memory: " + plan.specs().memory());
+ }
+ }
+
+ private Iterable getVolumes(Plan plan) {
+ if (plan.specs() == null || plan.specs().drives() == null) return Lists.newArrayList();
+
+ return Iterables.transform(plan.specs().drives(), new Function() {
+ @Override
+ public Volume apply(Specs.Drive drive) {
+ return new VolumeImpl(
+ drive.type(),
+ Volume.Type.LOCAL,
+ Float.parseFloat(drive.size().substring(0, drive.size().length() - 2)), null, true, false);
+ }
+ });
+ }
+
+
+ private Iterable getProcessors(Plan plan) {
+ if (plan.specs() == null || plan.specs().cpus() == null) return Lists.newArrayList();
+ return Iterables.transform(plan.specs().cpus(), new Function() {
+ @Override
+ public Processor apply(Specs.CPU input) {
+ // No cpu speed from Packet API, so assume more cores == faster
+ return new Processor(input.count(), input.count());
+ }
+ });
+ }
+}
diff --git a/providers/packet/src/main/java/org/jclouds/packet/compute/options/PacketTemplateOptions.java b/providers/packet/src/main/java/org/jclouds/packet/compute/options/PacketTemplateOptions.java
new file mode 100644
index 0000000000..30cef4522e
--- /dev/null
+++ b/providers/packet/src/main/java/org/jclouds/packet/compute/options/PacketTemplateOptions.java
@@ -0,0 +1,158 @@
+/*
+ * 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.options;
+
+import java.util.Map;
+
+import org.jclouds.compute.options.TemplateOptions;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Objects.ToStringHelper;
+import com.google.common.collect.ImmutableMap;
+
+import static com.google.common.base.Objects.equal;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Custom options for the Packet API.
+ */
+public class PacketTemplateOptions extends TemplateOptions implements Cloneable {
+
+ private Map features = ImmutableMap.of();
+ private boolean locked = false;
+ private String billingCycle = "hourly";
+ private String userData = "";
+
+ public PacketTemplateOptions features(Map features) {
+ this.features = ImmutableMap.copyOf(checkNotNull(features, "features cannot be null"));
+ return this;
+ }
+
+ public PacketTemplateOptions locked(boolean locked) {
+ this.locked = locked;
+ return this;
+ }
+
+ public PacketTemplateOptions billingCycle(String billingCycle) {
+ this.billingCycle = billingCycle;
+ return this;
+ }
+
+ public PacketTemplateOptions userData(String userData) {
+ this.userData = userData;
+ return this;
+ }
+
+ public Map getFeatures() {
+ return features;
+ }
+ public boolean isLocked() {
+ return locked;
+ }
+ public String getBillingCycle() {
+ return billingCycle;
+ }
+ public String getUserData() {
+ return userData;
+ }
+
+ @Override
+ public PacketTemplateOptions clone() {
+ PacketTemplateOptions options = new PacketTemplateOptions();
+ copyTo(options);
+ return options;
+ }
+
+ @Override
+ public void copyTo(TemplateOptions to) {
+ super.copyTo(to);
+ if (to instanceof PacketTemplateOptions) {
+ PacketTemplateOptions eTo = PacketTemplateOptions.class.cast(to);
+ eTo.features(features);
+ eTo.locked(locked);
+ eTo.billingCycle(billingCycle);
+ eTo.userData(userData);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(super.hashCode(), features, locked, billingCycle, userData);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!super.equals(obj)) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ PacketTemplateOptions other = (PacketTemplateOptions) obj;
+ return super.equals(other) && equal(this.locked, other.locked) && equal(this.billingCycle, other.billingCycle) && equal(this.userData, other.userData) && equal(this.features, other.features);
+ }
+
+ @Override
+ public ToStringHelper string() {
+ ToStringHelper toString = super.string().omitNullValues();
+ if (!features.isEmpty()) {
+ toString.add("features", features);
+ } toString.add("locked", locked);
+ toString.add("billingCycle", billingCycle);
+ toString.add("userData", userData);
+ return toString;
+ }
+
+ public static class Builder {
+
+ /**
+ * @see PacketTemplateOptions#features
+ */
+ public static PacketTemplateOptions features(Map features) {
+ PacketTemplateOptions options = new PacketTemplateOptions();
+ return options.features(features);
+ }
+
+ /**
+ * @see PacketTemplateOptions#locked
+ */
+ public static PacketTemplateOptions locked(boolean locked) {
+ PacketTemplateOptions options = new PacketTemplateOptions();
+ return options.locked(locked);
+ }
+
+ /**
+ * @see PacketTemplateOptions#billingCycle
+ */
+ public static PacketTemplateOptions billingCycle(String billingCycle) {
+ PacketTemplateOptions options = new PacketTemplateOptions();
+ return options.billingCycle(billingCycle);
+ }
+
+ /**
+ * @see PacketTemplateOptions#userData
+ */
+ public static PacketTemplateOptions userData(String userData) {
+ PacketTemplateOptions options = new PacketTemplateOptions();
+ return options.userData(userData);
+ }
+
+ }
+}
diff --git a/providers/packet/src/main/java/org/jclouds/packet/compute/strategy/CreateSshKeysThenCreateNodes.java b/providers/packet/src/main/java/org/jclouds/packet/compute/strategy/CreateSshKeysThenCreateNodes.java
new file mode 100644
index 0000000000..3a5a02a8d9
--- /dev/null
+++ b/providers/packet/src/main/java/org/jclouds/packet/compute/strategy/CreateSshKeysThenCreateNodes.java
@@ -0,0 +1,215 @@
+/*
+ * 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.strategy;
+
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.RSAPublicKeySpec;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Resource;
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.jclouds.Constants;
+import org.jclouds.compute.config.CustomizationResponse;
+import org.jclouds.compute.domain.NodeMetadata;
+import org.jclouds.compute.domain.Template;
+import org.jclouds.compute.functions.GroupNamingConvention;
+import org.jclouds.compute.reference.ComputeServiceConstants;
+import org.jclouds.compute.strategy.CreateNodeWithGroupEncodedIntoName;
+import org.jclouds.compute.strategy.CustomizeNodeAndAddToGoodMapOrPutExceptionIntoBadMap;
+import org.jclouds.compute.strategy.ListNodesStrategy;
+import org.jclouds.compute.strategy.impl.CreateNodesWithGroupEncodedIntoNameThenAddToSet;
+import org.jclouds.logging.Logger;
+import org.jclouds.packet.PacketApi;
+import org.jclouds.packet.compute.options.PacketTemplateOptions;
+import org.jclouds.packet.domain.SshKey;
+import org.jclouds.ssh.SshKeyPairGenerator;
+import org.jclouds.ssh.SshKeys;
+
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Throwables.propagate;
+import static com.google.common.collect.Iterables.get;
+import static com.google.common.collect.Iterables.size;
+
+@Singleton
+public class CreateSshKeysThenCreateNodes extends CreateNodesWithGroupEncodedIntoNameThenAddToSet {
+
+ @Resource
+ @Named(ComputeServiceConstants.COMPUTE_LOGGER)
+ protected Logger logger = Logger.NULL;
+
+ private final PacketApi api;
+ private final SshKeyPairGenerator keyGenerator;
+
+ @Inject
+ protected CreateSshKeysThenCreateNodes(
+ CreateNodeWithGroupEncodedIntoName addNodeWithGroupStrategy,
+ ListNodesStrategy listNodesStrategy,
+ GroupNamingConvention.Factory namingConvention,
+ @Named(Constants.PROPERTY_USER_THREADS) ListeningExecutorService userExecutor,
+ CustomizeNodeAndAddToGoodMapOrPutExceptionIntoBadMap.Factory customizeNodeAndAddToGoodMapOrPutExceptionIntoBadMapFactory,
+ PacketApi api, SshKeyPairGenerator keyGenerator) {
+ super(addNodeWithGroupStrategy, listNodesStrategy, namingConvention, userExecutor,
+ customizeNodeAndAddToGoodMapOrPutExceptionIntoBadMapFactory);
+ this.api = api;
+ this.keyGenerator = keyGenerator;
+ }
+
+ @Override
+ public Map, ListenableFuture> execute(String group, int count, Template template,
+ Set goodNodes, Map badNodes,
+ Multimap customizationResponses) {
+
+ PacketTemplateOptions options = template.getOptions().as(PacketTemplateOptions.class);
+ Set generatedSshKeyIds = Sets.newHashSet();
+
+ // If no key has been configured, generate a key pair
+ if (Strings.isNullOrEmpty(options.getPublicKey())) {
+ generateKeyPairAndAddKeyToSet(options, generatedSshKeyIds, group);
+ }
+
+ // If there is a script to run in the node, make sure a private key has
+ // been configured so jclouds will be able to access the node
+ if (options.getRunScript() != null && Strings.isNullOrEmpty(options.getLoginPrivateKey())) {
+ logger.warn(">> A runScript has been configured but no SSH key has been provided."
+ + " Authentication will delegate to the ssh-agent");
+ }
+
+ // If there is a key configured, then make sure there is a key pair for it
+ if (!Strings.isNullOrEmpty(options.getPublicKey())) {
+ createKeyPairForPublicKeyInOptionsAndAddToSet(options, generatedSshKeyIds);
+ }
+
+ Map, ListenableFuture> responses = super.execute(group, count, template, goodNodes, badNodes,
+ customizationResponses);
+
+ // Key pairs in Packet are only required to create the devices. They aren't used anymore so it is better
+ // to delete the auto-generated key pairs at this point where we know exactly which ones have been
+ // auto-generated by jclouds.
+ registerAutoGeneratedKeyPairCleanupCallbacks(responses, generatedSshKeyIds);
+
+ return responses;
+ }
+
+ private void createKeyPairForPublicKeyInOptionsAndAddToSet(PacketTemplateOptions options,
+ Set generatedSshKeyIds) {
+ logger.debug(">> checking if the key pair already exists...");
+
+ PublicKey userKey;
+ Iterable parts = Splitter.on(' ').split(options.getPublicKey());
+ checkArgument(size(parts) >= 2, "bad format, should be: ssh-rsa AAAAB3...");
+ String type = get(parts, 0);
+
+ try {
+ if ("ssh-rsa".equals(type)) {
+ RSAPublicKeySpec spec = SshKeys.publicKeySpecFromOpenSSH(options.getPublicKey());
+ userKey = KeyFactory.getInstance("RSA").generatePublic(spec);
+ } else {
+ throw new IllegalArgumentException("bad format, ssh-rsa is only supported");
+ }
+ } catch (InvalidKeySpecException ex) {
+ throw propagate(ex);
+ } catch (NoSuchAlgorithmException ex) {
+ throw propagate(ex);
+ }
+ String label = computeFingerprint(userKey);
+ SshKey key = api.sshKeyApi().get(label);
+
+ if (key == null) {
+ logger.debug(">> key pair not found. creating a new key pair %s ...", label);
+ SshKey newKey = api.sshKeyApi().create(label, options.getPublicKey());
+ logger.debug(">> key pair created! %s", newKey);
+ } else {
+ logger.debug(">> key pair found! %s", key);
+ generatedSshKeyIds.add(key.id());
+ }
+ }
+
+ private void generateKeyPairAndAddKeyToSet(PacketTemplateOptions options, Set generatedSshKeyIds, String prefix) {
+ logger.debug(">> creating default keypair for node...");
+
+ Map defaultKeys = keyGenerator.get();
+
+ SshKey sshKey = api.sshKeyApi().create(prefix + System.getProperty("user.name"), defaultKeys.get("public"));
+ generatedSshKeyIds.add(sshKey.id());
+ logger.debug(">> keypair created! %s", sshKey);
+
+ // If a private key has not been explicitly set, configure the generated one
+ if (Strings.isNullOrEmpty(options.getLoginPrivateKey())) {
+ options.overrideLoginPrivateKey(defaultKeys.get("private"));
+ }
+ }
+
+ private void registerAutoGeneratedKeyPairCleanupCallbacks(Map, ListenableFuture> responses,
+ final Set generatedSshKeyIds) {
+ // The Futures.allAsList fails immediately if some of the futures fail. The Futures.successfulAsList, however,
+ // returns a list containing the results or 'null' for those futures that failed. We want to wait for all them
+ // (even if they fail), so better use the latter form.
+ ListenableFuture> aggregatedResponses = Futures.successfulAsList(responses.values());
+
+ // Key pairs must be cleaned up after all futures completed (even if some failed).
+ Futures.addCallback(aggregatedResponses, new FutureCallback>() {
+ @Override
+ public void onSuccess(List result) {
+ cleanupAutoGeneratedKeyPairs(generatedSshKeyIds);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ cleanupAutoGeneratedKeyPairs(generatedSshKeyIds);
+ }
+
+ private void cleanupAutoGeneratedKeyPairs(Set generatedSshKeyIds) {
+ logger.debug(">> cleaning up auto-generated key pairs...");
+ for (String sshKeyId : generatedSshKeyIds) {
+ try {
+ api.sshKeyApi().delete(sshKeyId);
+ } catch (Exception ex) {
+ logger.warn(">> could not delete key pair %s: %s", sshKeyId, ex.getMessage());
+ }
+ }
+ }
+ }, userExecutor);
+ }
+
+ private static String computeFingerprint(PublicKey key) {
+ if (key instanceof RSAPublicKey) {
+ RSAPublicKey rsaKey = (RSAPublicKey) key;
+ return SshKeys.fingerprint(rsaKey.getPublicExponent(), rsaKey.getModulus());
+ } else {
+ throw new IllegalArgumentException("Only RSA keys are supported");
+ }
+ }
+
+}
diff --git a/providers/packet/src/main/java/org/jclouds/packet/config/PacketComputeParserModule.java b/providers/packet/src/main/java/org/jclouds/packet/config/PacketComputeParserModule.java
new file mode 100644
index 0000000000..8471fc48d3
--- /dev/null
+++ b/providers/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);
+ }
+
+}
diff --git a/providers/packet/src/main/java/org/jclouds/packet/config/PacketHttpApiModule.java b/providers/packet/src/main/java/org/jclouds/packet/config/PacketHttpApiModule.java
new file mode 100644
index 0000000000..6edeebd81a
--- /dev/null
+++ b/providers/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.HrefToListOptions;
+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 {
+
+ @Override
+ protected void configure() {
+ super.configure();
+ bind(ImplicitLocationSupplier.class).to(FirstRegion.class).in(Scopes.SINGLETON);
+ bind(new TypeLiteral>() {
+ }).to(HrefToListOptions.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);
+ }
+
+}
diff --git a/providers/packet/src/main/java/org/jclouds/packet/domain/ActionType.java b/providers/packet/src/main/java/org/jclouds/packet/domain/ActionType.java
new file mode 100644
index 0000000000..9177ec5835
--- /dev/null
+++ b/providers/packet/src/main/java/org/jclouds/packet/domain/ActionType.java
@@ -0,0 +1,39 @@
+/*
+ * 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;
+
+/**
+ * Performs an action for the given device. Possible actions include:
+
+ power_on
+ power_off
+ reboot
+ rescue: reboot the device into rescue OS.
+ */
+public enum ActionType {
+
+ POWER_ON ("power_on"),
+ POWER_OFF ("power_off"),
+ REBOOT ("reboot"),
+ RESCUE ("rescue");
+
+ private final String type;
+
+ ActionType(String type) {
+ this.type = type;
+ }
+}
diff --git a/providers/packet/src/main/java/org/jclouds/packet/domain/BillingCycle.java b/providers/packet/src/main/java/org/jclouds/packet/domain/BillingCycle.java
new file mode 100644
index 0000000000..ef88946d0d
--- /dev/null
+++ b/providers/packet/src/main/java/org/jclouds/packet/domain/BillingCycle.java
@@ -0,0 +1,56 @@
+/*
+ * 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;
+
+import java.util.List;
+
+import com.google.common.base.Predicate;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.collect.Iterables.tryFind;
+import static java.util.Arrays.asList;
+
+public enum BillingCycle {
+ HOURLY("hourly"),
+ MONTHLY("monthly"),
+ UNRECOGNIZED("");
+
+ private static final List values = asList(BillingCycle.values());
+
+ private final String value;
+
+ private BillingCycle(String value) {
+ this.value = checkNotNull(value, "value cannot be null");
+ }
+
+ public String value() {
+ return this.value;
+ }
+
+ public static BillingCycle fromValue(String value) {
+ return tryFind(values, hasValue(value)).or(UNRECOGNIZED);
+ }
+
+ private static Predicate hasValue(final String value) {
+ return new Predicate() {
+ @Override
+ public boolean apply(BillingCycle input) {
+ return input.value.equalsIgnoreCase(value);
+ }
+ };
+ }
+}
diff --git a/providers/packet/src/main/java/org/jclouds/packet/domain/Device.java b/providers/packet/src/main/java/org/jclouds/packet/domain/Device.java
new file mode 100644
index 0000000000..18f5d955e4
--- /dev/null
+++ b/providers/packet/src/main/java/org/jclouds/packet/domain/Device.java
@@ -0,0 +1,184 @@
+/*
+ * 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;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.jclouds.javax.annotation.Nullable;
+import org.jclouds.json.SerializedNames;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Enums;
+import com.google.common.base.Joiner;
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+@AutoValue
+public abstract class Device {
+
+ public enum State {
+ PROVISIONING, QUEUED, ACTIVE, REBOOTING, POWERING_OFF, POWERING_ON, INACTIVE;
+
+ public static State fromValue(String value) {
+ Optional state = Enums.getIfPresent(State.class, value.toUpperCase());
+ checkArgument(state.isPresent(), "Expected one of %s but was %s", Joiner.on(',').join(State.values()), value);
+ return state.get();
+ }
+ }
+
+ public abstract String id();
+ public abstract String shortId();
+ public abstract String hostname();
+ @Nullable
+ public abstract String description();
+ public abstract State state();
+ public abstract List tags();
+ public abstract String billingCycle();
+ public abstract String user();
+ public abstract String iqn();
+ public abstract Boolean locked();
+ public abstract String bondingMode();
+ public abstract Date createdAt();
+ public abstract Date updatedAt();
+ public abstract OperatingSystem operatingSystem();
+ public abstract Facility facility();
+ public abstract Href project();
+ public abstract List sshKeys();
+ public abstract Href projectLite();
+ public abstract List