/* * JBoss, Home of Professional Open Source. * Copyright 2011, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.arquillian.container.appscale.remote; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintStream; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.jboss.arquillian.container.common.AppEngineCommonContainer; import org.jboss.arquillian.container.spi.client.container.DeploymentException; import org.jboss.arquillian.container.spi.client.protocol.metadata.ProtocolMetaData; import org.jboss.shrinkwrap.api.Archive; import org.jboss.shrinkwrap.api.GenericArchive; import org.jboss.shrinkwrap.api.ShrinkWrap; /** * Remote AppScale container. * <p/> * To execute tests on remote AppScale instance, you must * start AppScale service on remote host * and enable password-less ssh access to remote host. * <p/> * Enable password less access to remote host: * $ cat ~/.ssh/id_rsa.pub | ssh root@HOST "cat >> /root/.ssh/authorized_keys" * <p/> * To start AppScale service on AppScale instance run "appscale up" * To stop AppScale service on AppScale instance run "appscale down" * * @author <a href="mailto:mlazar@redhat.com">Matej Lazar</a> * @author <a href="mailto:ales.justin@jboss.org">Ales Justin</a> */ public class AppScaleRemoteContainer extends AppEngineCommonContainer<AppScaleRemoteConfiguration> { protected final Logger log = Logger.getLogger(getClass().getName()); protected static final Pattern hostPortPattern = Pattern.compile("(http://[0-9\\.\\:]+)"); private AppScaleRemoteConfiguration configuration; private DeploymentInfo deploymentInfo; @Override public Class<AppScaleRemoteConfiguration> getConfigurationClass() { return AppScaleRemoteConfiguration.class; } @Override public void setup(AppScaleRemoteConfiguration configuration) { this.configuration = configuration; } @Override protected File export(Archive<?> archive) throws Exception { Archive<GenericArchive> appscaleArchive = ShrinkWrap.create(GenericArchive.class); appscaleArchive.merge(archive, "war"); return super.export(appscaleArchive); } @Override protected ProtocolMetaData doDeploy(Archive<?> archive) throws DeploymentException { List<String> uploadDeploymentCmd = new ArrayList<String>(); uploadDeploymentCmd.add("scp"); uploadDeploymentCmd.add("-r"); uploadDeploymentCmd.add(getAppLocation().getAbsolutePath()); uploadDeploymentCmd.add("root@" + configuration.getHost() + ":/root/"); List<String> deployCmd = ssh("/usr/local/appscale-tools/bin/appscale-upload-app --email " + configuration.getEmail() + " --file /root/" + getAppLocation().getName() + " --keyname " + configuration.getKeyName()); List<String> responses = new ArrayList<String>(); try { runCmd(uploadDeploymentCmd, "upload", "./", null, configuration.getUploadTimeout()); runCmd(deployCmd, "deploy", "./", responses, configuration.getDeployTimeout()); // Allow some time for the app to come up before we run tests. final long syncTime = configuration.getSyncTime(); if (syncTime > 0) { Thread.sleep(syncTime); } } catch (InterruptedException e) { throw new DeploymentException("Cannot deploy to AppScale.", e); } deploymentInfo = parseUrlFromResponse(responses); if (deploymentInfo.isValid() == false) { throw new DeploymentException("Could not deploy, invalid reponse: " + deploymentInfo); } return getProtocolMetaData(deploymentInfo.host, deploymentInfo.port, archive); } @Override protected void teardown() throws DeploymentException { List<String> undeployCmd = ssh("/usr/local/appscale-tools/bin/appscale-remove-app --confirm --appname " + deploymentInfo.appName + " --keyname " + configuration.getKeyName()); List<String> removeDeploymentArchive = ssh("rm -rf /root/" + getAppLocation().getName()); try { runCmd(undeployCmd, "undeploy", "./", null, configuration.getUndeployTimeout()); runCmd(removeDeploymentArchive, "remove", "./", null, configuration.getRemoveTimeout()); } catch (InterruptedException e) { throw new DeploymentException("Cannot undeploy from AppScale.", e); } } protected List<String> ssh(String last) { return Arrays.asList("ssh", "root@" + configuration.getHost(), last); } void runCmd(List<String> command, String processName, String workingDirectory, List<String> responses, long timeout) throws InterruptedException { log.log(Level.FINE, String.format("Process name='%s' command='%s' workingDirectory='%s'", processName, command, workingDirectory)); final ProcessBuilder builder = new ProcessBuilder(command); builder.directory(new File(workingDirectory)); final Process process; try { process = builder.start(); } catch (IOException e) { log.log(Level.SEVERE, "Cannot run command: " + command, e); return; } final InputStream stderr = process.getErrorStream(); final InputStream stdout = process.getInputStream(); final Thread stderrThread = new Thread(new ReadTask(stderr, System.err, processName, null)); stderrThread.setName(String.format("stderr for %s", processName)); stderrThread.start(); final Thread stdoutThread = new Thread(new ReadTask(stdout, System.out, processName, responses)); stdoutThread.setName(String.format("stdout for %s", processName)); stdoutThread.start(); stdoutThread.join(timeout); stderrThread.join(timeout); } private final class ReadTask implements Runnable { private final InputStream source; private final PrintStream target; private final String processName; private final List<String> response; private ReadTask(final InputStream source, final PrintStream target, String processName, List<String> response) { this.source = source; this.target = target; this.processName = processName; this.response = response; } @Override public void run() { final InputStream source = this.source; try { final BufferedReader reader = new BufferedReader(new InputStreamReader(new BufferedInputStream(source))); final OutputStreamWriter writer = new OutputStreamWriter(target); String s; while ((s = reader.readLine()) != null) { synchronized (target) { if (response != null) response.add(s); writer.write('['); writer.write(processName); writer.write("] "); writer.write(s); writer.write('\n'); writer.flush(); } } source.close(); } catch (IOException e) { log.log(Level.SEVERE, "Cannot start read task.", e); } finally { safeClose(source); } } } DeploymentInfo parseUrlFromResponse(List<String> responses) { DeploymentInfo deploymentInfo = new DeploymentInfo(); for (String response : responses) { Matcher hostPortMatcher = hostPortPattern.matcher(response); if (hostPortMatcher.find()) { String url = hostPortMatcher.group(); URI uri = URI.create(url); deploymentInfo.host = uri.getHost(); deploymentInfo.port = uri.getPort(); } if (response.startsWith("Uploading")) { /* Expected responses are: * "Uploading new version of app {appName}" * "Uploading initial version of app {appName}" */ deploymentInfo.appName = response.split("app ")[1]; } } return deploymentInfo; } static class DeploymentInfo { String appName; String host; Integer port; @Override public String toString() { return host + ":" + port + " - " + appName; } public boolean isValid() { return appName != null && !appName.equals("") && host != null && port != null && port != 0; } } }