/*
* Copyright 2013-2017 EMC Corporation. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0.txt
*
* or in the "license" file accompanying this file. This file 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 com.emc.ecs.sync;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.ClientConfiguration;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3Client;
import com.emc.ecs.nfsclient.nfs.io.Nfs3File;
import com.emc.ecs.nfsclient.nfs.nfs3.Nfs3;
import com.emc.ecs.nfsclient.rpc.CredentialUnix;
import com.emc.ecs.sync.config.Protocol;
import com.emc.ecs.sync.config.SyncConfig;
import com.emc.ecs.sync.config.SyncOptions;
import com.emc.ecs.sync.config.storage.*;
import com.emc.ecs.sync.filter.SyncFilter;
import com.emc.ecs.sync.model.*;
import com.emc.ecs.sync.rest.LogLevel;
import com.emc.ecs.sync.service.AbstractDbService;
import com.emc.ecs.sync.service.SqliteDbService;
import com.emc.ecs.sync.service.SyncJobService;
import com.emc.ecs.sync.storage.SyncStorage;
import com.emc.ecs.sync.storage.TestStorage;
import com.emc.ecs.sync.util.PluginUtil;
import com.emc.object.s3.S3Client;
import com.emc.object.s3.S3Config;
import com.emc.object.s3.S3Exception;
import com.emc.object.s3.jersey.S3JerseyClient;
import org.junit.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.rowset.SqlRowSet;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URI;
import java.util.*;
import java.util.concurrent.*;
import static org.junit.Assert.assertFalse;
public class EndToEndTest {
private static final Logger log = LoggerFactory.getLogger(EndToEndTest.class);
private static final int SM_OBJ_COUNT = 200;
private static final int SM_OBJ_MAX_SIZE = 10240; // 10K
private static final int LG_OBJ_COUNT = 10;
private static final int LG_OBJ_MAX_SIZE = 1024 * 1024; // 1M
private static final int SYNC_THREAD_COUNT = 32;
private ExecutorService service;
private class TestDbService extends SqliteDbService {
TestDbService() {
super(":memory:");
initCheck();
}
@Override
public JdbcTemplate getJdbcTemplate() {
return super.getJdbcTemplate();
}
}
private TestDbService dbService = new TestDbService();
@Before
public void before() {
service = Executors.newFixedThreadPool(SYNC_THREAD_COUNT);
}
@After
public void after() {
if (service != null) service.shutdownNow();
}
@Test
public void testTestPlugins() throws Exception {
TestConfig config = new TestConfig().withObjectCount(SM_OBJ_COUNT).withMaxSize(SM_OBJ_MAX_SIZE)
.withReadData(true).withDiscardData(false);
EcsSync sync = new EcsSync();
sync.setSyncConfig(new SyncConfig().withSource(config).withTarget(config));
sync.run();
TestStorage source = (TestStorage) sync.getSource();
TestStorage target = (TestStorage) sync.getTarget();
VerifyTest.verifyObjects(source, source.getRootObjects(), target, target.getRootObjects(), true);
}
@Test
public void testFilesystem() throws Exception {
final File tempDir = new File("/tmp/ecs-sync-filesystem-test"); // File.createTempFile("ecs-sync-filesystem-test", "dir");
tempDir.mkdir();
tempDir.deleteOnExit();
if (!tempDir.exists() || !tempDir.isDirectory())
throw new RuntimeException("unable to make temp dir");
FilesystemConfig filesystemConfig = new FilesystemConfig();
filesystemConfig.setPath(tempDir.getPath());
filesystemConfig.setStoreMetadata(true);
multiEndToEndTest(filesystemConfig, new TestConfig(), false);
new File(tempDir, ObjectMetadata.METADATA_DIR).delete(); // delete this so the temp dir can go away
}
@Test
public void testNfs() throws Exception {
Properties syncProperties = com.emc.ecs.sync.test.TestConfig.getProperties();
String export = syncProperties.getProperty(com.emc.ecs.sync.test.TestConfig.PROP_NFS_EXPORT);
Assume.assumeNotNull(export);
if (!export.contains(":")) throw new RuntimeException("invalid export: " + export);
String server = export.split(":")[0];
String mountPath = export.split(":")[1];
final Nfs3 nfs = new Nfs3(server, mountPath, new CredentialUnix(0, 0, null), 3);
final Nfs3File tempDir = new Nfs3File(nfs, "/ecs-sync-nfs-test");
tempDir.mkdir();
if (!tempDir.exists() || !tempDir.isDirectory()) {
throw new RuntimeException("unable to make temp dir");
}
try {
NfsConfig config = new NfsConfig();
config.setServer(server);
config.setMountPath(mountPath);
config.setPath(tempDir.getPath());
config.setStoreMetadata(true);
multiEndToEndTest(config, new TestConfig(), false);
} finally {
tempDir.getChildFile(ObjectMetadata.METADATA_DIR).delete();
tempDir.delete();
assertFalse(tempDir.exists());
}
}
@Test
public void testArchive() throws Exception {
final File archive = new File("/tmp/ecs-sync-archive-test.zip");
if (archive.exists()) archive.delete();
archive.deleteOnExit();
ArchiveConfig archiveConfig = new ArchiveConfig();
archiveConfig.setPath(archive.getPath());
archiveConfig.setStoreMetadata(true);
TestConfig testConfig = new TestConfig().withReadData(true).withDiscardData(false);
testConfig.withObjectCount(LG_OBJ_COUNT).withMaxSize(LG_OBJ_MAX_SIZE);
endToEndTest(archiveConfig, testConfig, null, false);
}
@Test
public void testAtmos() throws Exception {
Properties syncProperties = com.emc.ecs.sync.test.TestConfig.getProperties();
final String rootPath = "/ecs-sync-atmos-test/";
String endpoints = syncProperties.getProperty(com.emc.ecs.sync.test.TestConfig.PROP_ATMOS_ENDPOINTS);
String uid = syncProperties.getProperty(com.emc.ecs.sync.test.TestConfig.PROP_ATMOS_UID);
String secretKey = syncProperties.getProperty(com.emc.ecs.sync.test.TestConfig.PROP_ATMOS_SECRET);
Assume.assumeNotNull(endpoints, uid, secretKey);
Protocol protocol = Protocol.http;
List<String> hosts = new ArrayList<>();
int port = -1;
for (String endpoint : endpoints.split(",")) {
URI uri = new URI(endpoint);
protocol = Protocol.valueOf(uri.getScheme().toLowerCase());
port = uri.getPort();
hosts.add(uri.getHost());
}
AtmosConfig atmosConfig = new AtmosConfig();
atmosConfig.setProtocol(protocol);
atmosConfig.setHosts(hosts.toArray(new String[hosts.size()]));
atmosConfig.setPort(port);
atmosConfig.setUid(uid);
atmosConfig.setSecret(secretKey);
atmosConfig.setPath(rootPath);
atmosConfig.setAccessType(AtmosConfig.AccessType.namespace);
String[] validGroups = new String[]{"other"};
String[] validPermissions = new String[]{"READ", "WRITE", "FULL_CONTROL"};
TestConfig testConfig = new TestConfig();
testConfig.setObjectOwner(uid.substring(uid.lastIndexOf('/') + 1));
testConfig.setValidGroups(validGroups);
testConfig.setValidPermissions(validPermissions);
ObjectAcl template = new ObjectAcl();
template.addGroupGrant("other", "NONE");
multiEndToEndTest(atmosConfig, testConfig, template, true);
}
@Test
public void testEcsS3() throws Exception {
Properties syncProperties = com.emc.ecs.sync.test.TestConfig.getProperties();
final String bucket = "ecs-sync-s3-test-bucket";
final String endpoint = syncProperties.getProperty(com.emc.ecs.sync.test.TestConfig.PROP_S3_ENDPOINT);
final String accessKey = syncProperties.getProperty(com.emc.ecs.sync.test.TestConfig.PROP_S3_ACCESS_KEY_ID);
final String secretKey = syncProperties.getProperty(com.emc.ecs.sync.test.TestConfig.PROP_S3_SECRET_KEY);
final boolean useVHost = Boolean.valueOf(syncProperties.getProperty(com.emc.ecs.sync.test.TestConfig.PROP_S3_VHOST));
Assume.assumeNotNull(endpoint, accessKey, secretKey);
URI endpointUri = new URI(endpoint);
S3Config s3Config;
if (useVHost) {
s3Config = new S3Config(endpointUri);
} else {
s3Config = new S3Config(com.emc.object.Protocol.valueOf(endpointUri.getScheme().toUpperCase()), endpointUri.getHost());
}
s3Config.withPort(endpointUri.getPort()).withUseVHost(useVHost).withIdentity(accessKey).withSecretKey(secretKey);
S3Client s3 = new S3JerseyClient(s3Config);
try {
s3.createBucket(bucket);
} catch (S3Exception e) {
if (!e.getErrorCode().equals("BucketAlreadyExists")) throw e;
}
// for testing ACLs
String authUsers = "http://acs.amazonaws.com/groups/global/AuthenticatedUsers";
String everyone = "http://acs.amazonaws.com/groups/global/AllUsers";
String[] validGroups = {authUsers, everyone};
String[] validPermissions = {"READ", "WRITE", "FULL_CONTROL"};
EcsS3Config ecsS3Config = new EcsS3Config();
if (endpointUri.getScheme() != null)
ecsS3Config.setProtocol(Protocol.valueOf(endpointUri.getScheme().toLowerCase()));
ecsS3Config.setHost(endpointUri.getHost());
ecsS3Config.setPort(endpointUri.getPort());
ecsS3Config.setAccessKey(accessKey);
ecsS3Config.setSecretKey(secretKey);
ecsS3Config.setEnableVHosts(useVHost);
ecsS3Config.setBucketName(bucket);
ecsS3Config.setPreserveDirectories(true);
TestConfig testConfig = new TestConfig();
testConfig.setObjectOwner(accessKey);
testConfig.setValidGroups(validGroups);
testConfig.setValidPermissions(validPermissions);
try {
multiEndToEndTest(ecsS3Config, testConfig, true);
} finally {
try {
s3.deleteBucket(bucket);
} catch (Throwable t) {
log.warn("could not delete bucket", t);
}
}
}
@Test
public void testS3() throws Exception {
Properties syncProperties = com.emc.ecs.sync.test.TestConfig.getProperties();
final String bucket = "ecs-sync-s3-test-bucket";
final String endpoint = syncProperties.getProperty(com.emc.ecs.sync.test.TestConfig.PROP_S3_ENDPOINT);
final String accessKey = syncProperties.getProperty(com.emc.ecs.sync.test.TestConfig.PROP_S3_ACCESS_KEY_ID);
final String secretKey = syncProperties.getProperty(com.emc.ecs.sync.test.TestConfig.PROP_S3_SECRET_KEY);
Assume.assumeNotNull(endpoint, accessKey, secretKey);
URI endpointUri = new URI(endpoint);
ClientConfiguration config = new ClientConfiguration().withSignerOverride("S3SignerType");
AmazonS3Client s3 = new AmazonS3Client(new BasicAWSCredentials(accessKey, secretKey), config);
s3.setEndpoint(endpoint);
try {
s3.createBucket(bucket);
} catch (AmazonServiceException e) {
if (!e.getErrorCode().equals("BucketAlreadyExists")) throw e;
}
// for testing ACLs
String authUsers = "http://acs.amazonaws.com/groups/global/AuthenticatedUsers";
String everyone = "http://acs.amazonaws.com/groups/global/AllUsers";
String[] validGroups = {authUsers, everyone};
String[] validPermissions = {"READ", "WRITE", "FULL_CONTROL"};
AwsS3Config awsS3Config = new AwsS3Config();
if (endpointUri.getScheme() != null)
awsS3Config.setProtocol(Protocol.valueOf(endpointUri.getScheme().toLowerCase()));
awsS3Config.setHost(endpointUri.getHost());
awsS3Config.setPort(endpointUri.getPort());
awsS3Config.setAccessKey(accessKey);
awsS3Config.setSecretKey(secretKey);
awsS3Config.setLegacySignatures(true);
awsS3Config.setDisableVHosts(true);
awsS3Config.setBucketName(bucket);
awsS3Config.setPreserveDirectories(true);
TestConfig testConfig = new TestConfig();
testConfig.setObjectOwner(accessKey);
testConfig.setValidGroups(validGroups);
testConfig.setValidPermissions(validPermissions);
try {
multiEndToEndTest(awsS3Config, testConfig, true);
} finally {
try {
s3.deleteBucket(bucket);
} catch (Throwable t) {
log.warn("could not delete bucket", t);
}
}
}
private void multiEndToEndTest(Object storageConfig, TestConfig testConfig, boolean syncAcl) {
multiEndToEndTest(storageConfig, testConfig, null, syncAcl);
}
private void multiEndToEndTest(Object storageConfig, TestConfig testConfig, ObjectAcl aclTemplate, boolean syncAcl) {
if (testConfig == null) testConfig = new TestConfig();
testConfig.withReadData(true).withDiscardData(false);
// large objects
testConfig.withObjectCount(LG_OBJ_COUNT).withMaxSize(LG_OBJ_MAX_SIZE);
endToEndTest(storageConfig, testConfig, aclTemplate, syncAcl);
// small objects
testConfig.withObjectCount(SM_OBJ_COUNT).withMaxSize(SM_OBJ_MAX_SIZE);
endToEndTest(storageConfig, testConfig, aclTemplate, syncAcl);
// zero-byte objects (always important!)
testConfig.withObjectCount(SM_OBJ_COUNT).withMaxSize(0);
endToEndTest(storageConfig, testConfig, aclTemplate, syncAcl);
}
private void endToEndTest(Object storageConfig, TestConfig testConfig, ObjectAcl aclTemplate, boolean syncAcl) {
SyncJobService.getInstance().setLogLevel(LogLevel.verbose);
SyncOptions options = new SyncOptions().withThreadCount(SYNC_THREAD_COUNT);
options.withSyncAcl(syncAcl).withTimingsEnabled(true).withTimingWindow(100);
// create test source
TestStorage testSource = new TestStorage();
testSource.withAclTemplate(aclTemplate).withConfig(testConfig).withOptions(options);
try {
// send test data to test system
options.setVerify(true);
EcsSync sync = new EcsSync();
sync.setSource(testSource); // must use the same source for consistency
sync.setSyncConfig(new SyncConfig().withTarget(storageConfig).withOptions(options));
sync.setPerfReportSeconds(2);
sync.run();
options.setVerify(false); // revert options
Assert.assertEquals(0, sync.getStats().getObjectsFailed());
// test verify-only in target
options.setVerifyOnly(true);
sync = new EcsSync();
sync.setSource(testSource); // must use the same source for consistency
sync.setSyncConfig(new SyncConfig().withTarget(storageConfig).withOptions(options));
sync.setPerfReportSeconds(2);
sync.run();
options.setVerifyOnly(false); // revert options
Assert.assertEquals(0, sync.getStats().getObjectsFailed());
// read data from same system
options.setVerify(true);
sync = new EcsSync();
sync.setSyncConfig(new SyncConfig().withSource(storageConfig).withTarget(testConfig).withOptions(options));
sync.setPerfReportSeconds(2);
sync.setDbService(dbService);
sync.run();
options.setVerify(false); // revert options
// save test target for verify-only
TestStorage testTarget = (TestStorage) sync.getTarget();
Assert.assertEquals(0, sync.getStats().getObjectsFailed());
verifyDb(testSource);
Assert.assertEquals(sync.getStats().getObjectsComplete(), sync.getEstimatedTotalObjects());
Assert.assertEquals(sync.getStats().getBytesComplete(), sync.getEstimatedTotalBytes());
// test verify-only in source
options.setVerifyOnly(true);
sync = new EcsSync();
sync.setSyncConfig(new SyncConfig().withSource(storageConfig).withOptions(options));
sync.setTarget(testTarget);
sync.setPerfReportSeconds(2);
sync.setDbService(dbService);
sync.run();
options.setVerifyOnly(false); // revert options
Assert.assertEquals(0, sync.getStats().getObjectsFailed());
verifyDb(testSource);
VerifyTest.verifyObjects(testSource, testSource.getRootObjects(), testTarget, testTarget.getRootObjects(), syncAcl);
// test list-file operation
File listFile = createListFile(sync.getSource()); // should be the real storage plugin (not test)
options.setSourceListFile(listFile.getPath());
sync = new EcsSync();
sync.setSyncConfig(new SyncConfig().withSource(storageConfig).withTarget(testConfig).withOptions(options));
sync.run();
options.setSourceListFile(null); // revert options
testTarget = (TestStorage) sync.getTarget();
VerifyTest.verifyObjects(testSource, testSource.getRootObjects(), testTarget, testTarget.getRootObjects(), syncAcl);
} finally {
try {
dbService.getJdbcTemplate().execute("delete from objects");
} catch (Throwable t) {
log.warn("could not drop database", t);
}
try {
// delete the objects from the test system
SyncStorage<?> storage = PluginUtil.newStorageFromConfig(storageConfig, options);
storage.configure(storage, Collections.<SyncFilter>emptyIterator(), null);
List<Future> futures = new ArrayList<>();
for (ObjectSummary summary : storage.allObjects()) {
futures.add(recursiveDelete(storage, summary));
}
for (Future future : futures) {
try {
future.get();
} catch (Throwable t) {
log.warn("error deleting object", t);
}
}
} catch (Throwable t) {
log.warn("could not delete objects after sync: " + t.getMessage());
}
}
}
private File createListFile(SyncStorage<?> storage) {
try {
File listFile = File.createTempFile("list-file", null);
listFile.deleteOnExit();
try (BufferedWriter writer = new BufferedWriter(new FileWriter(listFile))) {
listIdentifiers(writer, storage, storage.allObjects());
writer.flush();
}
return listFile;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void listIdentifiers(BufferedWriter writer, SyncStorage<?> storage, Iterable<ObjectSummary> summaries) throws IOException {
for (ObjectSummary summary : summaries) {
writer.append(summary.getIdentifier()).append("\n");
if (summary.isDirectory()) listIdentifiers(writer, storage, storage.children(summary));
}
}
private Future recursiveDelete(final SyncStorage<?> storage, final ObjectSummary object) throws ExecutionException, InterruptedException {
final List<Future> futures = new ArrayList<>();
if (object.isDirectory()) {
for (ObjectSummary child : storage.children(object)) {
futures.add(recursiveDelete(storage, child));
}
}
return service.submit(new Callable() {
@Override
public Object call() throws Exception {
for (Future future : futures) {
future.get();
}
try {
log.info("deleting {}", object.getIdentifier());
storage.delete(object.getIdentifier());
} catch (Throwable t) {
log.warn("could not delete " + object.getIdentifier(), t);
}
return null;
}
});
}
private void verifyDb(TestStorage storage) {
JdbcTemplate jdbcTemplate = dbService.getJdbcTemplate();
long totalCount = verifyDbObjects(jdbcTemplate, storage, storage.getRootObjects());
SqlRowSet rowSet = jdbcTemplate.queryForRowSet("SELECT count(target_id) FROM " + AbstractDbService.DEFAULT_OBJECTS_TABLE_NAME + " WHERE target_id != ''");
Assert.assertTrue(rowSet.next());
Assert.assertEquals(totalCount, rowSet.getLong(1));
jdbcTemplate.update("DELETE FROM " + AbstractDbService.DEFAULT_OBJECTS_TABLE_NAME);
}
private long verifyDbObjects(JdbcTemplate jdbcTemplate, TestStorage storage, Collection<? extends SyncObject> objects) {
Date now = new Date();
long count = 0;
for (SyncObject object : objects) {
count++;
String identifier = storage.getIdentifier(object.getRelativePath(), object.getMetadata().isDirectory());
SqlRowSet rowSet = jdbcTemplate.queryForRowSet("SELECT * FROM " + AbstractDbService.DEFAULT_OBJECTS_TABLE_NAME + " WHERE target_id=?",
identifier);
Assert.assertTrue(rowSet.next());
Assert.assertEquals(identifier, rowSet.getString("target_id"));
Assert.assertEquals(object.getMetadata().isDirectory(), rowSet.getBoolean("is_directory"));
Assert.assertEquals(object.getMetadata().getContentLength(), rowSet.getLong("size"));
// mtime in the DB is actually pulled from the target system, so we don't know what precision it will be in
// or if the target system's clock is in sync, but let's assume it will always be within 5 minutes
Assert.assertTrue(Math.abs(object.getMetadata().getModificationTime().getTime() - rowSet.getLong("mtime")) < 5 * 60 * 1000);
Assert.assertEquals(ObjectStatus.Verified.getValue(), rowSet.getString("status"));
long transferStart = rowSet.getLong("transfer_start"), transferComplete = rowSet.getLong("transfer_complete");
if (transferStart > 0)
Assert.assertTrue(now.getTime() - transferStart < 10 * 60 * 1000); // less than 10 minutes ago
if (transferComplete > 0)
Assert.assertTrue(now.getTime() - transferComplete < 10 * 60 * 1000); // less than 10 minutes ago
Assert.assertTrue(now.getTime() - rowSet.getLong("verify_start") < 10 * 60 * 1000); // less than 10 minutes ago
Assert.assertTrue(now.getTime() - rowSet.getLong("verify_complete") < 10 * 60 * 1000); // less than 10 minutes ago
Assert.assertEquals(0, rowSet.getInt("retry_count"));
if (object.getMetadata().isDirectory())
count += verifyDbObjects(jdbcTemplate, storage, storage.getChildren(identifier));
}
return count;
}
}