/* This file is part of VoltDB. * Copyright (C) 2008-2017 VoltDB Inc. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ package org.voltdb.regressionsuites; import java.io.File; import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.concurrent.atomic.AtomicInteger; import org.voltdb.BackendTarget; import org.voltdb.TheHashinator; import org.voltdb.VoltDB.Configuration; import org.voltdb.VoltTable; import org.voltdb.benchmark.tpcc.TPCCProjectBuilder; import org.voltdb.benchmark.tpcc.procedures.InsertNewOrder; import org.voltdb.client.Client; import org.voltdb.client.ClientResponse; import org.voltdb.client.ClientUtils; import org.voltdb.client.ProcedureCallback; import org.voltdb.client.SyncCallback; import org.voltdb.common.Constants; import org.voltdb.compiler.VoltProjectBuilder.RoleInfo; import org.voltdb.compiler.VoltProjectBuilder.UserInfo; import org.voltdb.export.ExportDataProcessor; import org.voltdb.utils.MiscUtils; import junit.framework.Test; /** * Tests a mix of multi-partition and single partition procedures on a * mix of replicated and partititioned tables on a mix of single-site and * multi-site VoltDB instances. * */ public class TestUpdateDeployment extends RegressionSuite { static final int SITES_PER_HOST = 2; static final int HOSTS = 2; static final int K = MiscUtils.isPro() ? 1 : 0; // procedures used by these tests static Class<?>[] BASEPROCS = { org.voltdb.benchmark.tpcc.procedures.InsertNewOrder.class, org.voltdb.benchmark.tpcc.procedures.SelectAll.class, org.voltdb.benchmark.tpcc.procedures.delivery.class }; // users used by these test static final RoleInfo GROUPS[] = new RoleInfo[] { new RoleInfo("export", false, false, false, false, false, false), new RoleInfo("proc", true, false, true, true, false, false), new RoleInfo("admin", true, false, true, true, false, false) }; static final UserInfo[] USERS = new UserInfo[] { new UserInfo("fancy pants", "export", new String[]{"export"}), new UserInfo("default", "password", new String[]{"proc"}), new UserInfo("admin", "admin", new String[]{"proc", "admin"}) }; static final UserInfo[] USERS_BAD_PASSWORD = new UserInfo[] { new UserInfo("user1", "E7FA8F38396EF1332A60B629BA69257C462CBF3B95C81F3C556DDB79BD2226BEBCF2086983707FF5CFA72BE03B8B763199BBFFD3", new String[]{"admin"}, false), new UserInfo("user2", "password", new String[]{"admin", "proc"}, false) }; /** * Constructor needed for JUnit. Should just pass on parameters to superclass. * @param name The name of the method to test. This is just passed to the superclass. */ public TestUpdateDeployment(String name) { super(name); } AtomicInteger m_outstandingCalls = new AtomicInteger(0); boolean callbackSuccess; class CatTestCallback implements ProcedureCallback { final byte m_expectedStatus; CatTestCallback(byte expectedStatus) { m_expectedStatus = expectedStatus; m_outstandingCalls.incrementAndGet(); } @Override public void clientCallback(ClientResponse clientResponse) { m_outstandingCalls.decrementAndGet(); if (m_expectedStatus != clientResponse.getStatus()) { if (clientResponse.getStatusString() != null) System.err.println(clientResponse.getStatusString()); callbackSuccess = false; } } } /** * Start with snapshots disabled. Enable them to one directory, check that the snapshot files are created * with the correct prefix. Update the catalog to do the snapshots in a different directory with a * different prefix and check to make sure they start going to the right place. Update the catalog * to disable them and then make sure no snapshots appear. * @throws Exception */ public void testEnableModifyDisableSnapshot() throws Exception { m_config.deleteDirectory(new File("/tmp/snapshotdir1")); m_config.deleteDirectory(new File("/tmp/snapshotdir2")); try { m_config.createDirectory(new File("/tmp/snapshotdir1")); m_config.createDirectory(new File("/tmp/snapshotdir2")); Client client = getClient(); // // Test that we can enable snapshots // String deploymentURL = Configuration.getPathToCatalogForTest("catalogupdate-cluster-enable_snapshot.xml"); // Mix in various ways to specify no catalog. Use java null here. String depBytes = new String(ClientUtils.fileToBytes(new File(deploymentURL)), Constants.UTF8ENCODING); VoltTable[] results = client.callProcedure("@UpdateApplicationCatalog", null, depBytes).getResults(); //client.updateApplicationCatalog(null, new File(deploymentURL)).getResults(); assertTrue(results.length == 1); Thread.sleep(5000); // // Make sure snapshot files are generated // for (File f : m_config.listFiles(new File("/tmp/snapshotdir1"))) { assertTrue(f.getName().startsWith("foo1")); } // // Test that we can change settings like the path // deploymentURL = Configuration.getPathToCatalogForTest("catalogupdate-cluster-change_snapshot.xml"); // Mix in various ways to specify no catalog. Use empty string here. depBytes = new String(ClientUtils.fileToBytes(new File(deploymentURL)), Constants.UTF8ENCODING); results = client.callProcedure("@UpdateApplicationCatalog", "", depBytes).getResults(); assertTrue(results.length == 1); Thread.sleep(5000); // // Check that files are made in the new path // for (File f : m_config.listFiles(new File("/tmp/snapshotdir2"))) { assertTrue(f.getName().startsWith("foo2")); } // // Change the snapshot path to something that doesn't exist, no crashes // deploymentURL = Configuration.getPathToCatalogForTest("catalogupdate-cluster-change_snapshot_dir_not_exist.xml"); // Mix in various ways to specify no catalog. Use empty array here. depBytes = new String(ClientUtils.fileToBytes(new File(deploymentURL)), Constants.UTF8ENCODING); results = client.callProcedure("@UpdateApplicationCatalog", new byte[] {}, depBytes).getResults(); assertTrue(results.length == 1); System.out.println("Waiting for failed snapshots"); Thread.sleep(5000); // // Change it back // deploymentURL = Configuration.getPathToCatalogForTest("catalogupdate-cluster-base.xml"); // Mix in various ways to specify no catalog. Make sure the client convenience method // works with a null file. results = client.updateApplicationCatalog(null, new File(deploymentURL)).getResults(); assertTrue(results.length == 1); Thread.sleep(5000); // // Make sure snapshots resume // for (File f : m_config.listFiles(new File("/tmp/snapshotdir2"))) { assertTrue(f.getName().startsWith("foo2")); } // // Make sure you can disable snapshots // deploymentURL = Configuration.getPathToCatalogForTest("catalogupdate-cluster-base.xml"); results = client.updateApplicationCatalog(null, new File(deploymentURL)).getResults(); assertTrue(results.length == 1); for (File f : m_config.listFiles(new File("/tmp/snapshotdir2"))) { f.delete(); } Thread.sleep(5000); // // Make sure you can reenable snapshot files // assertEquals( 0, m_config.listFiles(new File("/tmp/snapshotdir2")).size()); // // Test that we can enable snapshots // deploymentURL = Configuration.getPathToCatalogForTest("catalogupdate-cluster-enable_snapshot.xml"); results = client.updateApplicationCatalog(null, new File(deploymentURL)).getResults(); assertTrue(results.length == 1); Thread.sleep(5000); // // Make sure snapshot files are generated // for (File f : m_config.listFiles(new File("/tmp/snapshotdir1"))) { assertTrue(f.getName().startsWith("foo1")); } // // Turn snapshots off so that we can clean up // deploymentURL = Configuration.getPathToCatalogForTest("catalogupdate-cluster-base.xml"); results = client.updateApplicationCatalog(null, new File(deploymentURL)).getResults(); assertTrue(results.length == 1); Thread.sleep(1000); m_config.deleteDirectory(new File("/tmp/snapshotdir1")); m_config.deleteDirectory(new File("/tmp/snapshotdir2")); m_config.createDirectory(new File("/tmp/snapshotdir1")); m_config.createDirectory(new File("/tmp/snapshotdir2")); Thread.sleep(5000); assertTrue(m_config.listFiles(new File("/tmp/snapshotdir1")).isEmpty()); assertTrue(m_config.listFiles(new File("/tmp/snapshotdir2")).isEmpty()); } finally { deleteDirectory(new File("/tmp/snapshotdir1")); deleteDirectory(new File("/tmp/snapshotdir2")); } } private void loadSomeData(Client client, int start, int count) throws Exception { for (int i = start; i < (start + count); i++) { CatTestCallback callback = new CatTestCallback(ClientResponse.SUCCESS); client.callProcedure(callback, InsertNewOrder.class.getSimpleName(), i, i, (short)i); } } public void testConsecutiveCatalogDeploymentRace() throws Exception { System.out.println("\n\n-----\n testConsecutiveCatalogDeploymentRace \n-----\n\n"); Client client = getClient(); loadSomeData(client, 0, 10); client.drain(); assertTrue(callbackSuccess); String newCatalogURL = Configuration.getPathToCatalogForTest("catalogupdate-cluster-addtable.jar"); String deploymentURL = Configuration.getPathToCatalogForTest("catalogupdate-cluster-addtable.xml"); // Asynchronously attempt consecutive catalog update and deployment update client.updateApplicationCatalog(new CatTestCallback(ClientResponse.SUCCESS), new File(newCatalogURL), null); // Then, update the users in the deployment SyncCallback cb2 = new SyncCallback(); client.updateApplicationCatalog(cb2, null, new File(deploymentURL)); cb2.waitForResponse(); assertEquals(ClientResponse.USER_ABORT, cb2.getResponse().getStatus()); assertTrue(cb2.getResponse().getStatusString().contains("Invalid catalog update")); // Verify the heartbeat timeout change didn't take Client client3 = getClient(); boolean found = false; int timeout = -1; VoltTable result = client3.callProcedure("@SystemInformation", "DEPLOYMENT").getResults()[0]; while (result.advanceRow()) { if (result.getString("PROPERTY").equalsIgnoreCase("heartbeattimeout")) { found = true; timeout = Integer.valueOf(result.getString("VALUE")); } } assertTrue(found); assertEquals(org.voltcore.common.Constants.DEFAULT_HEARTBEAT_TIMEOUT_SECONDS, timeout); // Verify that table A exists ClientResponse response = client3.callProcedure("@AdHoc", "insert into NEWTABLE values (100);"); assertEquals(ClientResponse.SUCCESS, response.getStatus()); } public void testUpdateSchemaModificationIsBlacklisted() throws Exception { System.out.println("\n\n-----\n testUpdateSchemaModificationIsBlacklisted \n-----\n\n"); Client client = getClient(); loadSomeData(client, 0, 10); client.drain(); assertTrue(callbackSuccess); String deploymentURL = Configuration.getPathToCatalogForTest("catalogupdate-cluster-change_schema_update.xml"); // Try to change the schem setting SyncCallback cb = new SyncCallback(); client.updateApplicationCatalog(cb, null, new File(deploymentURL)); cb.waitForResponse(); assertEquals(ClientResponse.GRACEFUL_FAILURE, cb.getResponse().getStatus()); System.out.println(cb.getResponse().getStatusString()); assertTrue(cb.getResponse().getStatusString().contains("May not dynamically modify")); } public void testUpdateSecurityNoUsers() throws Exception { System.out.println("\n\n-----\n testUpdateSecurityNoUsers \n-----\n\n"); Client client = getClient(); loadSomeData(client, 0, 10); client.drain(); assertTrue(callbackSuccess); String deploymentURL = Configuration.getPathToCatalogForTest("catalogupdate-security-no-users.xml"); // Try to change the schem setting SyncCallback cb = new SyncCallback(); client.updateApplicationCatalog(cb, null, new File(deploymentURL)); cb.waitForResponse(); assertEquals(ClientResponse.GRACEFUL_FAILURE, cb.getResponse().getStatus()); System.out.println(cb.getResponse().getStatusString()); assertTrue(cb.getResponse().getStatusString().contains("Unable to update")); } public void testUpdateBadExport() throws Exception { System.out.println("\n\n-----\n testUpdateBadExport \n-----\n\n"); System.setProperty(ExportDataProcessor.EXPORT_TO_TYPE, "org.voltdb.export.ExportTestClient"); Map<String, String> additionalEnv = new HashMap<>(); additionalEnv.put(ExportDataProcessor.EXPORT_TO_TYPE, "org.voltdb.export.ExportTestClient"); LocalCluster config = new LocalCluster("catalogupdate-bad-export.jar", SITES_PER_HOST, HOSTS, K, BackendTarget.NATIVE_EE_JNI, LocalCluster.FailureState.ALL_RUNNING, true, false, additionalEnv); TPCCProjectBuilder project = new TPCCProjectBuilder(); project.addDefaultSchema(); project.addDefaultPartitioning(); project.addProcedures(BASEPROCS); Properties props = buildProperties( "type", "csv", "batched", "false", "with-schema", "true", "complain", "true", "outdir", "/tmp/" + System.getProperty("user.name")); project.addExport(true /* enabled */, "custom", props); // build the jarfile boolean compile = config.compile(project); assertTrue(compile); Client client = getClient(); loadSomeData(client, 0, 10); client.drain(); assertTrue(callbackSuccess); // Try to change the schem setting SyncCallback cb = new SyncCallback(); client.updateApplicationCatalog(cb, null, new File(project.getPathToDeployment())); cb.waitForResponse(); assertEquals(ClientResponse.GRACEFUL_FAILURE, cb.getResponse().getStatus()); System.out.println(cb.getResponse().getStatusString()); assertTrue(cb.getResponse().getStatusString().contains("Unable to update")); } public void testUpdateSecurityBadUsername() throws Exception { System.out.println("\n\n-----\n testUpdateSecurityBadUsername \n-----\n\n"); Client client = getClient(); loadSomeData(client, 0, 10); client.drain(); assertTrue(callbackSuccess); String deploymentURL = Configuration.getPathToCatalogForTest("catalogupdate-bad-username.xml"); // Try to change the schem setting SyncCallback cb = new SyncCallback(); client.updateApplicationCatalog(cb, null, new File(deploymentURL)); cb.waitForResponse(); assertEquals(ClientResponse.GRACEFUL_FAILURE, cb.getResponse().getStatus()); System.out.println(cb.getResponse().getStatusString()); assertTrue(cb.getResponse().getStatusString().contains("Unable to update")); } public void testBadMaskPassword() throws Exception { System.out.println("\n\n-----\n testBadMaskPassword \n-----\n\n"); Client client = getClient(); loadSomeData(client, 0, 10); client.drain(); assertTrue(callbackSuccess); String deploymentURL = Configuration.getPathToCatalogForTest("catalogupdate-bad-masked-password.xml"); // Try to change schema setting SyncCallback cb = new SyncCallback(); client.updateApplicationCatalog(cb, null, new File(deploymentURL)); cb.waitForResponse(); assertEquals(ClientResponse.GRACEFUL_FAILURE, cb.getResponse().getStatus()); assertTrue(cb.getResponse().getStatusString().contains("Unable to update deployment configuration")); } private void deleteDirectory(File dir) { if (!dir.exists() || !dir.isDirectory()) { return; } for (File f : dir.listFiles()) { assertTrue(f.delete()); } assertTrue(dir.delete()); } /** * Build a list of the tests that will be run when TestTPCCSuite gets run by JUnit. * Use helper classes that are part of the RegressionSuite framework. * This particular class runs all tests on the the local JNI backend with both * one and two partition configurations, as well as on the hsql backend. * * @return The TestSuite containing all the tests to be run. * @throws Exception */ static public Test suite() throws Exception { TheHashinator.initialize(TheHashinator.getConfiguredHashinatorClass(), TheHashinator.getConfigureBytes(2)); // the suite made here will all be using the tests from this class MultiConfigSuiteBuilder builder = new MultiConfigSuiteBuilder(TestUpdateDeployment.class); ///////////////////////////////////////////////////////////// // CONFIG #1: 1 Local Site/Partitions running on JNI backend ///////////////////////////////////////////////////////////// // get a server config for the native backend with one sites/partitions VoltServerConfig config = new LocalCluster("catalogupdate-cluster-base.jar", SITES_PER_HOST, HOSTS, K, BackendTarget.NATIVE_EE_JNI); // Catalog upgrade test(s) sporadically fail if there's a local server because // a file pipe isn't available for grepping local server output. ((LocalCluster) config).setHasLocalServer(true); // build up a project builder for the workload TPCCProjectBuilder project = new TPCCProjectBuilder(); project.addDefaultSchema(); project.addDefaultPartitioning(); project.addProcedures(BASEPROCS); // build the jarfile boolean basecompile = config.compile(project); assertTrue(basecompile); MiscUtils.copyFile(project.getPathToDeployment(), Configuration.getPathToCatalogForTest("catalogupdate-cluster-base.xml")); // add this config to the set of tests to run builder.addServerConfig(config, false); ///////////////////////////////////////////////////////////// // DELTA CATALOGS FOR TESTING ///////////////////////////////////////////////////////////// // Generate a catalog that adds a table and a deployment file that changes the dead host timeout. config = new LocalCluster("catalogupdate-cluster-addtable.jar", SITES_PER_HOST, HOSTS, K, BackendTarget.NATIVE_EE_JNI); project = new TPCCProjectBuilder(); project.addDefaultSchema(); project.addDefaultPartitioning(); project.addLiteralSchema("CREATE TABLE NEWTABLE (A1 INTEGER, PRIMARY KEY (A1));"); project.setDeadHostTimeout(6); boolean compile = config.compile(project); assertTrue(compile); MiscUtils.copyFile(project.getPathToDeployment(), Configuration.getPathToCatalogForTest("catalogupdate-cluster-addtable.xml")); // A catalog change that enables snapshots config = new LocalCluster("catalogupdate-cluster-enable_snapshot.jar", SITES_PER_HOST, HOSTS, K, BackendTarget.NATIVE_EE_JNI); project = new TPCCProjectBuilder(); project.addDefaultSchema(); project.addDefaultPartitioning(); project.addProcedures(BASEPROCS); project.setSnapshotSettings( "1s", 3, "/tmp/snapshotdir1", "foo1"); // build the jarfile compile = config.compile(project); assertTrue(compile); MiscUtils.copyFile(project.getPathToDeployment(), Configuration.getPathToCatalogForTest("catalogupdate-cluster-enable_snapshot.xml")); //Another catalog change to modify the schedule config = new LocalCluster("catalogupdate-cluster-change_snapshot.jar", SITES_PER_HOST, HOSTS, K, BackendTarget.NATIVE_EE_JNI); project = new TPCCProjectBuilder(); project.addDefaultSchema(); project.addDefaultPartitioning(); project.addProcedures(BASEPROCS); project.setSnapshotSettings( "1s", 3, "/tmp/snapshotdir2", "foo2"); // build the jarfile compile = config.compile(project); assertTrue(compile); MiscUtils.copyFile(project.getPathToDeployment(), Configuration.getPathToCatalogForTest("catalogupdate-cluster-change_snapshot.xml")); //Another catalog change to modify the schedule config = new LocalCluster("catalogupdate-cluster-change_snapshot_dir_not_exist.jar", SITES_PER_HOST, HOSTS, K, BackendTarget.NATIVE_EE_JNI); project = new TPCCProjectBuilder(); project.addDefaultSchema(); project.addDefaultPartitioning(); project.addProcedures(BASEPROCS); project.setSnapshotSettings( "1s", 3, "/tmp/snapshotdirasda2", "foo2"); // build the jarfile compile = config.compile(project); assertTrue(compile); MiscUtils.copyFile(project.getPathToDeployment(), Configuration.getPathToCatalogForTest("catalogupdate-cluster-change_snapshot_dir_not_exist.xml")); // A deployment change that changes the schema change mechanism config = new LocalCluster("catalogupdate-cluster-change_schema_update.jar", SITES_PER_HOST, HOSTS, K, BackendTarget.NATIVE_EE_JNI); project = new TPCCProjectBuilder(); project.addDefaultSchema(); project.addDefaultPartitioning(); project.addProcedures(BASEPROCS); project.setUseDDLSchema(true); // build the jarfile compile = config.compile(project); assertTrue(compile); MiscUtils.copyFile(project.getPathToDeployment(), Configuration.getPathToCatalogForTest("catalogupdate-cluster-change_schema_update.xml")); // A deployment change that changes the schema change mechanism config = new LocalCluster("catalogupdate-security-no-users.jar", SITES_PER_HOST, HOSTS, K, BackendTarget.NATIVE_EE_JNI); project = new TPCCProjectBuilder(); project.addDefaultSchema(); project.addDefaultPartitioning(); project.addProcedures(BASEPROCS); project.setSecurityEnabled(true, false); // build the jarfile compile = config.compile(project); assertTrue(compile); MiscUtils.copyFile(project.getPathToDeployment(), Configuration.getPathToCatalogForTest("catalogupdate-security-no-users.xml")); // A deployment change that changes the schema change mechanism config = new LocalCluster("catalogupdate-bad-username.jar", SITES_PER_HOST, HOSTS, K, BackendTarget.NATIVE_EE_JNI); project = new TPCCProjectBuilder(); project.addDefaultSchema(); project.addDefaultPartitioning(); project.addProcedures(BASEPROCS); project.setSecurityEnabled(true,true); project.addRoles(GROUPS); project.addUsers(USERS); // build the jarfile compile = config.compile(project); assertTrue(compile); MiscUtils.copyFile(project.getPathToDeployment(), Configuration.getPathToCatalogForTest("catalogupdate-bad-username.xml")); // A deployment change that has bad masked password config = new LocalCluster("catalogupdate-bad-masked-password.jar", SITES_PER_HOST, HOSTS, K, BackendTarget.NATIVE_EE_JNI); project = new TPCCProjectBuilder(); project.addDefaultSchema(); project.addDefaultPartitioning(); project.addProcedures(BASEPROCS); project.setSecurityEnabled(true,true); project.addRoles(GROUPS); project.addUsers(USERS_BAD_PASSWORD); // build the jarfile compile = config.compile(project); assertTrue(compile); MiscUtils.copyFile(project.getPathToDeployment(), Configuration.getPathToCatalogForTest("catalogupdate-bad-masked-password.xml")); return builder; } @Override public void tearDown() throws Exception { super.tearDown(); assertTrue(callbackSuccess); } @Override public void setUp() throws Exception { super.setUp(); callbackSuccess = true; } }