package org.ovirt.engine.core.bll.hostdeploy; import java.io.BufferedReader; import java.io.Closeable; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import javax.naming.TimeLimitExceededException; import org.ovirt.engine.core.bll.utils.EngineSSHDialog; import org.ovirt.engine.core.common.businessentities.VDS; import org.ovirt.engine.core.common.config.Config; import org.ovirt.engine.core.common.config.ConfigValues; import org.ovirt.engine.core.uutils.ssh.SSHDialog; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * ovirt-node upgrade. */ public class OVirtNodeUpgrade implements SSHDialog.Sink, Closeable { public enum DeployStatus {Failed, Reboot} private static final int BUFFER_SIZE = 10 * 1024; private static final int THREAD_JOIN_TIMEOUT = 20 * 1000; private static final Logger log = LoggerFactory.getLogger(OVirtNodeUpgrade.class); private SSHDialog.Control _control; private Thread _thread; private EngineSSHDialog _dialog; private final InstallerMessages _messages; private BufferedReader _incoming; private VDS _vds; private File _iso; private Exception _failException = null; private DeployStatus _deployStatus = DeployStatus.Failed; /** * Dialog implementation. * Handle events incoming from host. */ private void threadMain() { boolean error = false; try { String line; while ( _incoming != null && (line = _incoming.readLine()) != null ) { log.info("update from host '{}': {}", _vds.getHostName(), line); error = _messages.postOldXmlFormat(line) || error; } if (error) { throw new RuntimeException( "Upgrade failed, please refer to logs for further information" ); } } catch (Exception e) { _failException = e; log.error("Error during upgrade: {}", e.getMessage()); log.debug("Exception", e); try { _control.close(); } catch (IOException ee) { log.error("Error during close: {}", ee.getMessage()); log.debug("Exception", ee); } } } /** * Constructor. * @param vds vds to install. * @param iso image to send. */ public OVirtNodeUpgrade(VDS vds, File iso) { _vds = vds; _iso = iso; _messages = new InstallerMessages(_vds); _dialog = new EngineSSHDialog(); _thread = new Thread( this::threadMain, "OVirtNodeUpgrade" ); } /** * Destructor. */ @Override protected void finalize() { try { close(); } catch (IOException e) { log.error("Exception during finalize: {}", e.getMessage()); log.debug("Exception", e); } } public void setCorrelationId(String correlationId) { _messages.setCorrelationId(correlationId); } /** * Free resources. */ @Override public void close() throws IOException { stop(); if (_dialog != null) { _dialog.close(); _dialog = null; } } /** * Returns the installation status * * @return the installation status */ public DeployStatus getDeployStatus() { return _deployStatus; } /** * Main method. * Execute the command and initiate the dialog. */ public void execute() throws Exception { try { _dialog.setVds(_vds); _dialog.useDefaultKeyPair(); _dialog.connect(); _messages.post( InstallerMessages.Severity.INFO, String.format( "Connected to host %1$s with SSH key fingerprint: %2$s", _vds.getHostName(), _dialog.getHostFingerprint() ) ); _dialog.authenticate(); String dest = Config.getValue(ConfigValues.oVirtUploadPath); _messages.post( InstallerMessages.Severity.INFO, String.format( "Sending file %1$s to %2$s", _iso, dest ) ); /* * Create the directory where * file is stored, in the past * it was done by vdsm, then vdsm-reg * well, as we use hard coded path * we can as well do this, until we * have proper node upgrade script * that can take the image from stdin. */ _dialog.executeCommand( new SSHDialog.Sink() { @Override public void setControl(SSHDialog.Control control) {} @Override public void setStreams(InputStream incoming, OutputStream outgoing) {} @Override public void start() {} @Override public void stop() {} }, String.format( "mkdir -p '%1$s'", new File(dest).getParent() ), null ); if (_failException != null) { throw _failException; } _dialog.sendFile( _iso.getAbsolutePath(), dest ); String command = Config.getValue(ConfigValues.oVirtUpgradeScriptName); _messages.post( InstallerMessages.Severity.INFO, String.format( "Executing %1$s", command ) ); _dialog.executeCommand( this, command, null ); if (_failException != null) { throw _failException; } _deployStatus = DeployStatus.Reboot; } catch (TimeLimitExceededException e){ log.error( "Timeout during node '{}' upgrade: {}", _vds.getHostName(), e.getMessage() ); log.debug("Exception", e); _messages.post( InstallerMessages.Severity.ERROR, "Processing stopped due to timeout" ); throw e; } catch (Exception e) { log.error("Error during node '{}' upgrade: {}", _vds.getHostName(), e.getMessage()); log.error("Exception", e); if (_failException == null) { throw e; } else { log.error( "Error during node '{}' upgrade, prefering first exception: {}", _vds.getHostName(), _failException.getMessage() ); throw _failException; } } } /* * SSHDialog.Sink */ @Override public void setControl(SSHDialog.Control control) { _control = control; } @Override public void setStreams(InputStream incoming, OutputStream outgoing) { _incoming = incoming == null ? null : new BufferedReader( new InputStreamReader( incoming, StandardCharsets.UTF_8 ), BUFFER_SIZE ); } @Override public void start() { _thread.start(); } @Override public void stop() { if (_thread != null) { /* * We cannot just interrupt the thread as the * implementation of jboss connection pooling * drops the connection when interrupted. * As we may have log events pending to be written * to database, we wait for some time for thread * complete before interrupting. */ try { _thread.join(THREAD_JOIN_TIMEOUT); } catch (InterruptedException e) { log.error("interrupted", e); } if (_thread.isAlive()) { _thread.interrupt(); while(true) { try { _thread.join(); break; } catch (InterruptedException ignore) {} } } _thread = null; } } }