/* * 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.apache.usergrid.chop.webapp.coordinator; import java.io.IOException; import java.net.URI; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.ws.rs.core.MediaType; import org.safehaus.jettyjam.utils.CertUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.usergrid.chop.api.BaseResult; import org.apache.usergrid.chop.api.Run; import org.apache.usergrid.chop.api.Runner; import org.apache.usergrid.chop.api.State; import org.apache.usergrid.chop.webapp.dao.RunDao; import org.apache.usergrid.chop.webapp.dao.RunnerDao; import com.google.common.base.Preconditions; import com.google.inject.Inject; import com.google.inject.Singleton; import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.WebResource; import com.sun.jersey.api.client.config.DefaultClientConfig; /** * Coordinates all runners in the server */ @Singleton public class RunnerCoordinator { private static final Logger LOG = LoggerFactory.getLogger( RunnerCoordinator.class ); @Inject private RunnerDao runnerDao; @Inject private RunDao runDao; private Map<Runner, Integer> lastRunNumbers = new HashMap<Runner, Integer>(); /** * * @param username * @param commitId * @param moduleId * @return the registered runners belonging to given username, commitId and moduleId */ public Collection<Runner> getRunners( String username, String commitId, String moduleId ) { return runnerDao.getRunners( username, commitId, moduleId ); } /** * * @param username * @param commitId * @param moduleId * @return */ public Map<Runner, State> getStates( String username, String commitId, String moduleId ) { return getStates( getRunners( username, commitId, moduleId ) ); } /** * Gets the run State of all given runners as a map. * <p> * <ul> * <li>Key of map is the runner</li> * <li>Value field is the state of a runner, or null if state could not be retrieved</li> * </ul> * * @param runners Runners to get states * @return Map of all Runner, State pairs */ public Map<Runner, State> getStates( Collection<Runner> runners ) { Map<Runner, State> states = new HashMap<Runner, State>( runners.size() ); for( Runner runner: runners ) { trustRunner( runner.getUrl() ); DefaultClientConfig clientConfig = new DefaultClientConfig(); Client client = Client.create( clientConfig ); LOG.info( "Runner to get state: {}", runner.getUrl() ); WebResource resource = client.resource( runner.getUrl() ).path( Runner.STATUS_GET ); BaseResult response = resource.type( MediaType.APPLICATION_JSON ).get( BaseResult.class ); if( ! response.getStatus() ) { LOG.warn( "Could not get the state of Runner at {}", runner.getUrl() ); LOG.warn( response.getMessage() ); states.put( runner, null ); } else { states.put( runner, response.getState() ); } } return states; } /** * * @param username * @param commitId * @param moduleId * @return */ public Map<Runner, State> start( String username, String commitId, String moduleId ) { return start( getRunners( username, commitId, moduleId ), runDao.getNextRunNumber( commitId ) ); } /** * Starts the tests on given runners and puts them into RUNNING state, if indeed they were READY. * * @param runners Runners that are going to run the tests * @param runNumber Run number of upcoming tests, this should be get from Run storage * @return Map of resulting states of <code>runners</code>, after the operation */ public Map<Runner, State> start( Collection<Runner> runners, int runNumber ) { Map<Runner, State> states = new HashMap<Runner, State>( runners.size() ); for( Runner runner: runners ) { lastRunNumbers.put( runner, runNumber ); trustRunner( runner.getUrl() ); DefaultClientConfig clientConfig = new DefaultClientConfig(); Client client = Client.create( clientConfig ); LOG.info( "Runner to start: {}", runner.getUrl() ); WebResource resource = client.resource( runner.getUrl() ).path( Runner.START_POST ); BaseResult response = resource.type( MediaType.APPLICATION_JSON ).post( BaseResult.class ); if( ! response.getStatus() ) { LOG.warn( "Tests at runner {} could not be started.", runner.getUrl() ); LOG.warn( response.getMessage() ); states.put( runner, null ); } else { states.put( runner, response.getState() ); } } return states; } /** * * @param username * @param commitId * @param moduleId * @return */ public Map<Runner, State> stop( String username, String commitId, String moduleId ) { return stop( getRunners( username, commitId, moduleId ) ); } /** * Stop the tests on given runners and puts them into STOPPED state, if indeed they were RUNNING. * * @param runners Runners that are running the tests * @return Map of resulting states of <code>runners</code>, after the operation */ public Map<Runner, State> stop( Collection<Runner> runners ) { Map<Runner, State> states = new HashMap<Runner, State>( runners.size() ); for( Runner runner: runners ) { trustRunner( runner.getUrl() ); DefaultClientConfig clientConfig = new DefaultClientConfig(); Client client = Client.create( clientConfig ); WebResource resource = client.resource( runner.getUrl() ).path( Runner.STOP_POST ); BaseResult response = resource.type( MediaType.APPLICATION_JSON ).post( BaseResult.class ); if( ! response.getStatus() ) { LOG.warn( "Tests at runner {} could not be stopped.", runner.getUrl() ); LOG.warn( response.getMessage() ); states.put( runner, null ); } else { states.put( runner, response.getState() ); } } return states; } /** * * @param username * @param commitId * @param moduleId * @return */ public Map<Runner, State> reset( String username, String commitId, String moduleId ) { return reset( getRunners( username, commitId, moduleId ) ); } /** * Resets the given runners and puts them into READY state, if indeed they were STOPPED. * * @param runners Runners to reset * @return Map of resulting states of <code>runners</code>, after the operation */ public Map<Runner, State> reset( Collection<Runner> runners ) { Map<Runner, State> states = new HashMap<Runner, State>( runners.size() ); for( Runner runner: runners ) { trustRunner( runner.getUrl() ); DefaultClientConfig clientConfig = new DefaultClientConfig(); Client client = Client.create( clientConfig ); WebResource resource = client.resource( runner.getUrl() ).path( Runner.RESET_POST ); BaseResult response = resource.type( MediaType.APPLICATION_JSON ).post( BaseResult.class ); if( ! response.getStatus() ) { LOG.warn( "Tests at runner {} could not be reset.", runner.getUrl() ); LOG.warn( response.getMessage() ); states.put( runner, null ); } else { states.put( runner, response.getState() ); } } return states; } /** * Removes incomplete set of Runs from storage, which are there due to Stopped tests. * <p> * This uses <code>lastRunNumbers</code> map to get the latest run number given runners were running. * Start method puts the run number of upcoming tests to <code>lastRunNumbers</code> map, * so if stopped, Runs with that run number in storage, if there are any, are deleted. * * @param runners All runners that were running a particular chop test * @param commitId Commit Id that defines related test */ public void trimIncompleteRuns( Collection<Runner> runners, String commitId ) { for( Runner runner: runners ) { Integer lastRunNumber = lastRunNumbers.get( runner ); List<Run> runs = runDao.getRuns( runner.getHostname(), commitId ); for( Run run: runs ) { if( run.getRunNumber() == lastRunNumber ) { LOG.info( "Removing incomplete Run {}", run ); runDao.delete( run ); } } } } /** * Registers given runner in storage, so that it belongs to given username, commitId and moduleId. * * @param username * @param commitId * @param moduleId * @param runner * @return Whether the operation succeeded */ public boolean register( String username, String commitId, String moduleId, Runner runner ) { try { LOG.info( "Registering runner: {}", runner.getUrl() ); LOG.info( " User: {}", username ); LOG.info( " Commit ID: {}", commitId ); LOG.info( " Module ID: {}", moduleId ); return runnerDao.save( runner, username, commitId, moduleId ); } catch ( IOException e ) { LOG.warn( "Error while trying to register runner. {}", e ); return false; } } /** * Removes the runner object with given runnerUrl * * @param runnerUrl Runner's url to unregister * @return Whether such a runner had existed in storage */ public boolean unregister( String runnerUrl ) { LOG.info( "Unregistering runner at {}", runnerUrl ); return runnerDao.delete( runnerUrl ); } /** * This is to resolve self signed uniform certificates in runners. * * @param runnerUrl Runner's url to trust in SSL communications */ private void trustRunner( final String runnerUrl ) { final URI uri = URI.create( runnerUrl ); /** * This is because we are using self-signed uniform certificates for now, * it should be removed if we switch to a CA signed dynamic certificate scheme! * */ javax.net.ssl.HttpsURLConnection.setDefaultHostnameVerifier( new javax.net.ssl.HostnameVerifier() { public boolean verify( String hostname, javax.net.ssl.SSLSession sslSession) { return hostname.equals( uri.getHost() ); } } ); // Need to get the configuration information for the coordinator if ( ! CertUtils.isTrusted( uri.getHost() ) ) { CertUtils.preparations( uri.getHost(), uri.getPort() ); } Preconditions.checkState( CertUtils.isTrusted( uri.getHost() ), "coordinator must be trusted" ); } }