fixed keypair on nova and added test

This commit is contained in:
Adrian Cole 2012-03-29 15:12:19 -04:00
parent ed37df621f
commit 9d2aca696b
9 changed files with 280 additions and 52 deletions

View File

@ -66,6 +66,8 @@ public class NovaTemplateOptions extends TemplateOptions implements Cloneable {
NovaTemplateOptions eTo = NovaTemplateOptions.class.cast(to);
eTo.autoAssignFloatingIp(shouldAutoAssignFloatingIp());
eTo.securityGroupNames(getSecurityGroupNames());
eTo.generateKeyPair(shouldGenerateKeyPair());
eTo.keyPairName(getKeyPairName());
}
}
@ -301,6 +303,15 @@ public class NovaTemplateOptions extends TemplateOptions implements Cloneable {
NovaTemplateOptions options = new NovaTemplateOptions();
return options.overrideLoginCredentials(credentials);
}
/**
* @see TemplateOptions#blockUntilRunning
*/
public static NovaTemplateOptions blockUntilRunning(boolean blockUntilRunning) {
NovaTemplateOptions options = new NovaTemplateOptions();
return options.blockUntilRunning(blockUntilRunning);
}
}
// methods that only facilitate returning the correct object type

View File

@ -101,6 +101,8 @@ public class ApplyNovaTemplateOptionsCreateNodesWithGroupEncodedIntoNameThenAddT
Template mutableTemplate = templateBuilderProvider.get().fromTemplate(template).build();
NovaTemplateOptions templateOptions = NovaTemplateOptions.class.cast(mutableTemplate.getOptions());
assert template.getOptions().equals(templateOptions) : "options didn't clone properly";
String zone = mutableTemplate.getLocation().getId();
if (templateOptions.shouldAutoAssignFloatingIp()) {

View File

@ -19,6 +19,8 @@
package org.jclouds.openstack.nova.v1_1.compute;
import static org.jclouds.compute.util.ComputeServiceUtils.getCores;
import static org.jclouds.openstack.nova.v1_1.compute.options.NovaTemplateOptions.Builder.blockUntilRunning;
import static org.jclouds.openstack.nova.v1_1.compute.options.NovaTemplateOptions.Builder.keyPairName;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
@ -26,6 +28,7 @@ import static org.testng.Assert.assertTrue;
import java.net.URI;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import org.jclouds.compute.ComputeService;
import org.jclouds.compute.domain.NodeMetadata;
@ -36,9 +39,13 @@ import org.jclouds.http.HttpResponse;
import org.jclouds.openstack.nova.v1_1.internal.BaseNovaComputeServiceExpectTest;
import org.testng.annotations.Test;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Iterables;
import com.google.inject.AbstractModule;
import com.google.inject.TypeLiteral;
/**
* Tests the compute service abstraction of the nova client.
@ -50,10 +57,10 @@ public class NovaComputeServiceExpectTest extends BaseNovaComputeServiceExpectTe
public void testListLocationsWhenResponseIs2xx() throws Exception {
Map<HttpRequest, HttpResponse> requestResponseMap = ImmutableMap.<HttpRequest, HttpResponse> builder().put(
keystoneAuthWithUsernameAndPassword, responseWithKeystoneAccess).put(extensionsOfNovaRequest,
extensionsOfNovaResponse).put(listImagesDetail, listImagesDetailResponse).put(listServers,
listServersResponse).put(listFlavorsDetail, listFlavorsDetailResponse).build();
Map<HttpRequest, HttpResponse> requestResponseMap = ImmutableMap.<HttpRequest, HttpResponse> builder()
.put(keystoneAuthWithUsernameAndPassword, responseWithKeystoneAccess)
.put(extensionsOfNovaRequest, extensionsOfNovaResponse).put(listImagesDetail, listImagesDetailResponse)
.put(listServers, listServersResponse).put(listFlavorsDetail, listFlavorsDetailResponse).build();
ComputeService clientWhenServersExist = requestsSendResponses(requestResponseMap);
@ -65,34 +72,39 @@ public class NovaComputeServiceExpectTest extends BaseNovaComputeServiceExpectTe
assertNotNull(clientWhenServersExist.listNodes());
assertEquals(clientWhenServersExist.listNodes().size(), 1);
assertEquals(clientWhenServersExist.listNodes().iterator().next().getId(),
"az-1.region-a.geo-1/52415800-8b69-11e0-9b19-734f000004d2");
"az-1.region-a.geo-1/52415800-8b69-11e0-9b19-734f000004d2");
assertEquals(clientWhenServersExist.listNodes().iterator().next().getName(), "sample-server");
}
Map<HttpRequest, HttpResponse> defaultTemplateTryStack = ImmutableMap
.<HttpRequest, HttpResponse> builder()
.put(keystoneAuthWithUsernameAndPassword,
HttpResponse
.builder()
.statusCode(200)
.message("HTTP/1.1 200")
.payload(
payloadFromResourceWithContentType("/keystoneAuthResponse_trystack.json", "application/json"))
.build())
.put(extensionsOfNovaRequest.toBuilder()
.endpoint(URI.create("https://nova-api.trystack.org:9774/v1.1/3456/extensions")).build(),
HttpResponse.builder().statusCode(200).payload(payloadFromResource("/extension_list_trystack.json"))
.build())
.put(listImagesDetail.toBuilder()
.endpoint(URI.create("https://nova-api.trystack.org:9774/v1.1/3456/images/detail")).build(),
HttpResponse.builder().statusCode(200).payload(payloadFromResource("/image_list_detail_trystack.json"))
.build())
.put(listServers.toBuilder()
.endpoint(URI.create("https://nova-api.trystack.org:9774/v1.1/3456/servers/detail")).build(),
listServersResponse)
.put(listFlavorsDetail.toBuilder()
.endpoint(URI.create("https://nova-api.trystack.org:9774/v1.1/3456/flavors/detail")).build(),
HttpResponse.builder().statusCode(200).payload(payloadFromResource("/flavor_list_detail_trystack.json"))
.build()).build();
public void testDefaultTemplateTryStack() throws Exception {
Map<HttpRequest, HttpResponse> requestResponseMap = ImmutableMap.<HttpRequest, HttpResponse> builder().put(
keystoneAuthWithUsernameAndPassword,
HttpResponse.builder().statusCode(200).message("HTTP/1.1 200").payload(
payloadFromResourceWithContentType("/keystoneAuthResponse_trystack.json", "application/json"))
.build()).put(
extensionsOfNovaRequest.toBuilder().endpoint(
URI.create("https://nova-api.trystack.org:9774/v1.1/3456/extensions")).build(),
HttpResponse.builder().statusCode(200).payload(payloadFromResource("/extension_list_trystack.json"))
.build()).put(
listImagesDetail.toBuilder().endpoint(
URI.create("https://nova-api.trystack.org:9774/v1.1/3456/images/detail")).build(),
HttpResponse.builder().statusCode(200).payload(payloadFromResource("/image_list_detail_trystack.json"))
.build()).put(
listServers.toBuilder().endpoint(
URI.create("https://nova-api.trystack.org:9774/v1.1/3456/servers/detail")).build(),
listServersResponse).put(
listFlavorsDetail.toBuilder().endpoint(
URI.create("https://nova-api.trystack.org:9774/v1.1/3456/flavors/detail")).build(),
HttpResponse.builder().statusCode(200).payload(payloadFromResource("/flavor_list_detail_trystack.json"))
.build()).build();
ComputeService clientForTryStack = requestsSendResponses(requestResponseMap);
ComputeService clientForTryStack = requestsSendResponses(defaultTemplateTryStack);
Template defaultTemplate = clientForTryStack.templateBuilder().imageId("RegionOne/15").build();
checkTemplate(defaultTemplate);
@ -110,30 +122,194 @@ public class NovaComputeServiceExpectTest extends BaseNovaComputeServiceExpectTe
}
public void testListServersWhenReponseIs404IsEmpty() throws Exception {
HttpRequest listServers = HttpRequest.builder().method("GET").endpoint(
URI.create("https://compute.north.host/v1.1/3456/servers/detail")).headers(
ImmutableMultimap.<String, String> builder().put("Accept", "application/json").put("X-Auth-Token",
authToken).build()).build();
HttpRequest listServers = HttpRequest
.builder()
.method("GET")
.endpoint(URI.create("https://compute.north.host/v1.1/3456/servers/detail"))
.headers(
ImmutableMultimap.<String, String> builder().put("Accept", "application/json")
.put("X-Auth-Token", authToken).build()).build();
HttpResponse listServersResponse = HttpResponse.builder().statusCode(404).build();
ComputeService clientWhenNoServersExist = requestsSendResponses(keystoneAuthWithUsernameAndPassword,
responseWithKeystoneAccess, listServers, listServersResponse);
responseWithKeystoneAccess, listServers, listServersResponse);
assertTrue(clientWhenNoServersExist.listNodes().isEmpty());
}
@Test(enabled = false)
public void testCreateNodeSetsCredential() throws Exception {
HttpRequest listSecurityGroups = HttpRequest
.builder()
.method("GET")
.endpoint(URI.create("https://nova-api.trystack.org:9774/v1.1/3456/os-security-groups"))
.headers(
ImmutableMultimap.<String, String> builder().put("Accept", "application/json")
.put("X-Auth-Token", authToken).build()).build();
Map<HttpRequest, HttpResponse> requestResponseMap = ImmutableMap.<HttpRequest, HttpResponse> builder().put(
keystoneAuthWithUsernameAndPassword, responseWithKeystoneAccess).put(extensionsOfNovaRequest,
extensionsOfNovaResponse).put(listImagesDetail, listImagesDetailResponse).put(listServers,
listServersResponse).put(listFlavorsDetail, listFlavorsDetailResponse).build();
HttpResponse notFound = HttpResponse.builder().statusCode(404).build();
ComputeService clientThatCreatesNode = requestsSendResponses(requestResponseMap);
HttpRequest createSecurityGroupWithPrefixOnGroup = HttpRequest
.builder()
.method("POST")
.endpoint(URI.create("https://nova-api.trystack.org:9774/v1.1/3456/os-security-groups"))
.headers(
ImmutableMultimap.<String, String> builder().put("Accept", "application/json")
.put("X-Auth-Token", authToken).build())
.payload(
payloadFromStringWithContentType(
"{\"security_group\":{\"name\":\"jclouds-test\",\"description\":\"jclouds-test\"}}",
"application/json")).build();
NodeMetadata node = Iterables.getOnlyElement(clientThatCreatesNode.createNodesInGroup("test", 1));
assertEquals(node.getCredentials().getPassword(), "foo");
HttpResponse securityGroupCreated = HttpResponse.builder().statusCode(200)
.payload(payloadFromResource("/securitygroup_created.json")).build();
HttpRequest createSecurityGroupRuleForDefaultPort22 = HttpRequest
.builder()
.method("POST")
.endpoint(URI.create("https://nova-api.trystack.org:9774/v1.1/3456/os-security-group-rules"))
.headers(
ImmutableMultimap.<String, String> builder().put("Accept", "application/json")
.put("X-Auth-Token", authToken).build())
.payload(
payloadFromStringWithContentType(
"{\"security_group_rule\":{\"parent_group_id\":\"160\",\"cidr\":\"0.0.0.0/0\",\"ip_protocol\":\"tcp\",\"from_port\":\"22\",\"to_port\":\"22\"}}",
"application/json")).build();
HttpResponse securityGroupRuleCreated = HttpResponse.builder().statusCode(200)
.payload(payloadFromResource("/securitygrouprule_created.json")).build();
HttpRequest getSecurityGroup = HttpRequest
.builder()
.method("GET")
.endpoint(URI.create("https://nova-api.trystack.org:9774/v1.1/3456/os-security-groups/160"))
.headers(
ImmutableMultimap.<String, String> builder().put("Accept", "application/json")
.put("X-Auth-Token", authToken).build()).build();
HttpResponse securityGroupWithPort22 = HttpResponse.builder().statusCode(200)
.payload(payloadFromResource("/securitygroup_details_port22.json")).build();
HttpRequest createKeyPair = HttpRequest
.builder()
.method("POST")
.endpoint(URI.create("https://nova-api.trystack.org:9774/v1.1/3456/os-keypairs"))
.headers(
ImmutableMultimap.<String, String> builder().put("Accept", "application/json")
.put("X-Auth-Token", authToken).build())
.payload(
payloadFromStringWithContentType(
"{\"keypair\":{\"name\":\"jclouds-test-0\"}}",
"application/json")).build();
HttpResponse keyPairWithPrivateKey = HttpResponse.builder().statusCode(200)
.payload(payloadFromResource("/keypair_created_computeservice.json")).build();
@Test
public void testCreateNodeWithGeneratedKeyPair() throws Exception {
Builder<HttpRequest, HttpResponse> requestResponseMap = ImmutableMap.<HttpRequest, HttpResponse> builder()
.putAll(defaultTemplateTryStack);
requestResponseMap.put(listSecurityGroups, notFound);
requestResponseMap.put(createSecurityGroupWithPrefixOnGroup, securityGroupCreated);
requestResponseMap.put(createSecurityGroupRuleForDefaultPort22, securityGroupRuleCreated);
requestResponseMap.put(getSecurityGroup, securityGroupWithPort22);
requestResponseMap.put(createKeyPair, keyPairWithPrivateKey);
HttpRequest createServerWithGeneratedKeyPair = HttpRequest
.builder()
.method("POST")
.endpoint(URI.create("https://nova-api.trystack.org:9774/v1.1/3456/servers"))
.headers(
ImmutableMultimap.<String, String> builder().put("Accept", "application/json")
.put("X-Auth-Token", authToken).build())
.payload(
payloadFromStringWithContentType(
"{\"server\":{\"name\":\"test-1\",\"imageRef\":\"14\",\"flavorRef\":\"1\",\"key_name\":\"jclouds-test-0\",\"security_groups\":[{\"name\":\"jclouds-test\"}]}}",
"application/json")).build();
HttpResponse createdServer = HttpResponse.builder().statusCode(202).message("HTTP/1.1 202 Accepted")
.payload(payloadFromResourceWithContentType("/new_server.json", "application/json; charset=UTF-8")).build();
requestResponseMap.put(createServerWithGeneratedKeyPair, createdServer);
ComputeService clientThatCreatesNode = requestsSendResponses(requestResponseMap.build(), new AbstractModule() {
@Override
protected void configure() {
// predicatable node names
final AtomicInteger suffix = new AtomicInteger();
bind(new TypeLiteral<Supplier<String>>() {
}).toInstance(new Supplier<String>() {
@Override
public String get() {
return suffix.getAndIncrement() + "";
}
});
}
});
NodeMetadata node = Iterables.getOnlyElement(clientThatCreatesNode.createNodesInGroup("test", 1,
blockUntilRunning(false).generateKeyPair(true)));
assertNotNull(node.getCredentials().getPrivateKey());
}
@Test
public void testCreateNodeWhileUserSpecifiesKeyPair() throws Exception {
Builder<HttpRequest, HttpResponse> requestResponseMap = ImmutableMap.<HttpRequest, HttpResponse> builder()
.putAll(defaultTemplateTryStack);
requestResponseMap.put(listSecurityGroups, notFound);
requestResponseMap.put(createSecurityGroupWithPrefixOnGroup, securityGroupCreated);
requestResponseMap.put(createSecurityGroupRuleForDefaultPort22, securityGroupRuleCreated);
requestResponseMap.put(getSecurityGroup, securityGroupWithPort22);
HttpRequest createServerWithSuppliedKeyPair = HttpRequest
.builder()
.method("POST")
.endpoint(URI.create("https://nova-api.trystack.org:9774/v1.1/3456/servers"))
.headers(
ImmutableMultimap.<String, String> builder().put("Accept", "application/json")
.put("X-Auth-Token", authToken).build())
.payload(
payloadFromStringWithContentType(
"{\"server\":{\"name\":\"test-0\",\"imageRef\":\"14\",\"flavorRef\":\"1\",\"key_name\":\"fooPair\",\"security_groups\":[{\"name\":\"jclouds-test\"}]}}",
"application/json")).build();
HttpResponse createdServer = HttpResponse.builder().statusCode(202).message("HTTP/1.1 202 Accepted")
.payload(payloadFromResourceWithContentType("/new_server.json", "application/json; charset=UTF-8")).build();
requestResponseMap.put(createServerWithSuppliedKeyPair, createdServer);
ComputeService clientThatCreatesNode = requestsSendResponses(requestResponseMap.build(), new AbstractModule() {
@Override
protected void configure() {
// predicatable node names
final AtomicInteger suffix = new AtomicInteger();
bind(new TypeLiteral<Supplier<String>>() {
}).toInstance(new Supplier<String>() {
@Override
public String get() {
return suffix.getAndIncrement() + "";
}
});
}
});
NodeMetadata node = Iterables.getOnlyElement(clientThatCreatesNode.createNodesInGroup("test", 1,
keyPairName("fooPair").blockUntilRunning(false)));
// we don't have access to this private key
assertEquals(node.getCredentials().getPrivateKey(), null);
}
}

View File

@ -123,7 +123,7 @@ public class SecurityGroupClientExpectTest extends BaseNovaClientExpectTest {
authToken).build())
.payload(
payloadFromStringWithContentType(
"{\"security_group\":{\"name\":\"name\",\"description\":\"description\"}}",
"{\"security_group\":{\"name\":\"jclouds-test\",\"description\":\"jclouds-test\"}}",
"application/json")).build();
HttpResponse createSecurityGroupResponse = HttpResponse.builder().statusCode(200).payload(
@ -134,7 +134,7 @@ public class SecurityGroupClientExpectTest extends BaseNovaClientExpectTest {
createSecurityGroupResponse);
assertEquals(clientWhenSecurityGroupsExist.getSecurityGroupExtensionForZone("az-1.region-a.geo-1").get()
.createSecurityGroupWithNameAndDescription("name", "description").toString(),
.createSecurityGroupWithNameAndDescription("jclouds-test", "jclouds-test").toString(),
createSecurityGroupExpected().toString());
}
@ -227,7 +227,7 @@ public class SecurityGroupClientExpectTest extends BaseNovaClientExpectTest {
}
private SecurityGroup createSecurityGroupExpected() {
return SecurityGroup.builder().description("description").id("160").name("name").rules(
return SecurityGroup.builder().description("jclouds-test").id("160").name("jclouds-test").rules(
ImmutableSet.<SecurityGroupRule> of()).tenantId("dev_16767499955063").build();
}

View File

@ -18,18 +18,23 @@
*/
package org.jclouds.openstack.nova.v1_1.internal;
import java.io.InputStream;
import java.net.URI;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import org.jclouds.compute.ComputeServiceContext;
import org.jclouds.compute.ComputeServiceContextFactory;
import org.jclouds.http.HttpRequest;
import org.jclouds.http.HttpResponse;
import org.jclouds.io.CopyInputStreamInputSupplierMap;
import org.jclouds.logging.config.NullLoggingModule;
import org.jclouds.rest.config.CredentialStoreModule;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.InputSupplier;
import com.google.inject.Module;
/**
@ -78,8 +83,11 @@ public abstract class BaseNovaComputeServiceContextExpectTest<T> extends BaseNov
}
private ComputeServiceContext createComputeServiceContext(Function<HttpRequest, HttpResponse> fn, Module module,
Properties props) {
Properties props) {
// the following will wipe out credential store between tests
return new ComputeServiceContextFactory(setupRestProperties()).createContext(provider, identity, credential,
ImmutableSet.<Module> of(new ExpectModule(fn), new NullLoggingModule(), module), props);
ImmutableSet.<Module> of(new ExpectModule(fn), new NullLoggingModule(), new CredentialStoreModule(
new CopyInputStreamInputSupplierMap(new ConcurrentHashMap<String, InputSupplier<InputStream>>())),
module), props);
}
}

View File

@ -0,0 +1,9 @@
{
"keypair": {
"public_key": "ssh-rsa AAAXB3NzaC1yc2EAAAADAQABAAAAgQDFNyGjgs6c9akgmZ2ou/fJf7Pdrc23hC95/gM/33OrG4GZABACE4DTioa/PGN+7rHv9YUavUCtXrWayhGniKq/wCuI5fo5TO4AmDNv7/sCGHIHFumADSIoLx0vFhGJIetXEWxL9r0lfFC7//6yZM2W3KcGjbMtlPXqBT9K9PzdyQ== nova@nv-aw2az1-api0001\n",
"private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIICXQIAAAKBgQDFNyGjgs6c9akgmZ2ou/fJf7Pdrc23hC95/gM/33OrG4GZABAC\nE4DTioa/PGN+7rHv9YUavUCtXrWayhGniKq/wCuI5fo5TO4AmDNv7/sCGHIHFumA\nDSIoLx0vFhGJIetXEWxL9r0lfFC7//6yZM2W3KcGjbMtlPXqBT9K9PzdyQIDAQAB\nAoGAW8Ww+KbpQK8smcgCTr/RqcmsSI8VeL2hXjJvDq0L5WbyYuFdkanDvCztUVZn\nsmyfDtwAqZXB4Ct/dN1tY7m8QpdyRaKRW4Q+hghGCAQpsG7rYDdvwdEyvMaW5RA4\ntucQyajMNyQ/tozU3wMx/v8A7RvGcE9tqoG0WK1C3kBu95UCQQDrOd+joYDkvccz\nFIVu5gNPMXEh3fGGzDxk225UlvESquYLzfz4TfmuUjH4Z1BL3wRiwfJsrrjFkm33\njIidDE8PAkEA1qHjxuaIS1yz/rfzErmcOVNlbFHMP4ihjGTTvh1ZctXlNeLwzENQ\nEDaQV3IpUY1KQR6rxcWb5AXgfF9D9PYFpwJBANucAqGAbRgh3lJgPFtXP4u2O0tF\nLPOOxmvbOdybt6KYD4LB5AXmts77SlACFMNhCXUyYaT6UuOSXDyb5gfJsB0CQQC3\nFaGXKU9Z+doQjhlq/6mjvN/nZl80Uvh7Kgb1RVPoAU1kihGeLE0/h0vZTCiyyDNv\nGRqtucMg32J+tUTi0HpBAkAwHiCZMHMeJWHUwIwlRQY/dnR86FWobRl98ViF2rCL\nDHkDVOeIser3Q6zSqU5/m99lX6an5g8pAh/R5LqnOQZC\n-----END RSA PRIVATE KEY-----\n",
"user_id": "65649731189278",
"name": "jclouds-test-0",
"fingerprint": "d2:1f:c9:2b:d8:90:77:5f:15:64:27:e3:9f:77:1d:e4"
}
}

View File

@ -3,7 +3,7 @@
"rules": [ ],
"tenant_id": "dev_16767499955063",
"id": 160,
"name": "name",
"description": "description"
"name": "jclouds-test",
"description": "jclouds-test"
}
}

View File

@ -0,0 +1,19 @@
{
"security_group": {
"rules": [{
"from_port": 22,
"group": {},
"ip_protocol": "tcp",
"to_port": 22,
"parent_group_id": 2769,
"ip_range": {
"cidr": "0.0.0.0/0"
},
"id": 10331
}],
"tenant_id": "37936628937291",
"id": 2769,
"name": "jclouds-test",
"description": "jclouds-test"
}
}

View File

@ -425,6 +425,10 @@ public abstract class BaseRestClientExpectTest<S> {
}
public S requestsSendResponses(final Map<HttpRequest, HttpResponse> requestToResponse, Module module) {
return requestsSendResponses(requestToResponse, module, setupProperties());
}
public S requestsSendResponses(final Map<HttpRequest, HttpResponse> requestToResponse, Module module, Properties props) {
return createClient(new Function<HttpRequest, HttpResponse>() {
ImmutableBiMap<HttpRequest, HttpResponse> bimap = ImmutableBiMap.copyOf(requestToResponse);
@ -458,7 +462,7 @@ public abstract class BaseRestClientExpectTest<S> {
return response;
}
}, module);
}, module, props);
}
public String renderRequest(HttpRequest request) {
@ -489,7 +493,6 @@ public abstract class BaseRestClientExpectTest<S> {
public S createClient(Function<HttpRequest, HttpResponse> fn, Module module) {
return createClient(fn, module, setupProperties());
}
public S createClient(Function<HttpRequest, HttpResponse> fn, Properties props) {