blob: 3136b13b45b810aa97f9674b381f2f29e391687c [file] [log] [blame]
<?php
/*
* 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.
*/
include_once "../db/OrchestratorDB.php";
include_once "../db/Transaction.php";
include_once "../puppet/PuppetInvoker.php";
include_once "State.php";
include_once "ServiceComponent.php";
/**
* Service represents one of the main services deployed such as:
* HDFS, MapReduce, ZooKeeper, HBase, HCatalog, Oozie
*/
class Service {
// Name of the service
public $name;
// Display Name of the service
public $displayName;
// Service state
public $state;
// Cluster Name
public $clusterName;
// Service dependencies
public $dependencies;
// Service dependents
public $dependents;
// Service components
public $components;
// Database
public $db;
// Puppet
public $puppet;
// logger
private $logger;
// current action being done
private $currentAction;
// whether all components are clients only or not
private $isClientOnly;
// Set state whether smoke tests are required to be run for client-only services
// For client-components set smoke tests to be run only when install is done
private $runClientSmokeTest;
function __construct($clusterName, $serviceName, $serviceState,
$odb, $puppet, $displayName) {
$this->name = $serviceName;
$this->displayName = $displayName;
$this->state = $serviceState;
$this->clusterName = $clusterName;
$this->db = $odb;
$this->puppet = $puppet;
$this->logger = new HMCLogger("Service: $serviceName ($clusterName)");
$this->logger->log_debug("Service: $serviceName, $serviceState, $clusterName");
$this->currentAction = "";
$this->isClientOnly = NULL;
$this->runClientSmokeTest = FALSE;
}
/**
* Persist state into DB
* @param State $state
* @param Transaction $transaction
* @param bool $dryRun
* @param bool $persistTxn - FALSE in case of INSTALL only
*/
function setState($state, $transaction, $dryRun, $persistTxn) {
if ($persistTxn) {
$txnProgress = getTransactionProgressFromState($state);
$desc = getActionDescription($this->displayName, $this->currentAction,
TransactionProgress::$PROGRESS[$txnProgress]);
if ($dryRun) {
$desc = getActionDescription($this->displayName, $this->currentAction,
"PENDING");
}
$result =
$this->db->persistTransaction($transaction, State::$STATE[$state],
$desc, TransactionProgress::$PROGRESS[$txnProgress],
"SERVICE", $dryRun);
if ($result['result'] !== 0) {
$this->state == State::FAILED;
$this->logger->log_error($this->name." - ".State::$STATE[$state]);
$this->logger->log_error("Failed to persist transaction: " . $transaction->toString());
$this->db->setServiceState($this, $state);
return $result;
}
}
if (!$dryRun) {
$result = $this->db->setServiceState($this, $state);
if ($result['result'] !== 0) {
$this->state == State::FAILED;
$this->logger->log_error("Failed to persist state for Service "
. "$this->name - ".State::$STATE[$state] . " dryRun=$dryRun");
$this->db->setServiceState($this, $state);
return $result;
}
}
$this->state = $state;
$this->logger->log_info("$this->name - " . State::$STATE[$state]
. " dryRun=$dryRun");
return array("result" => 0, "error" => "");
}
/**
* Uninstall the service.
* @return mixed
* array( "result" => 0, "error" => msg)
*/
public function uninstall($transaction, $dryRun) {
$this->currentAction = "uninstall";
// Check if it's already INSTALLED or STARTED
if ($this->state === State::UNINSTALLED) {
$this->logger->log_debug("Service $this->name is already UNINSTALLED!");
return array("result" => 0, "error" => "");
}
// Ensure state is UNINSTALLED or FAILED
if ($this->state !== State::STOPPED &&
$this->state !== State::FAILED &&
$this->state !== State::UNKNOWN &&
$this->state != State::INSTALLED) {
$this->logger->log_error("Service $this->name is not UNKNOWN or INSTALLED or FAILED or STOPPED! state=" . State::$STATE[$this->state]);
return array("result" => -1, "error" => "Service $this->name is not INSTALLED or FAILED or STOPPED! state=" . State::$STATE[$this->state]);
}
// Note that we are about to UNINSTALL
$result = $this->setState(State::UNINSTALLING, $transaction, $dryRun, FALSE);
if ($result['result'] !== 0) {
return $result;
}
// Mark each component as UNINSTALLED
$result = $this->getComponents($transaction);
if ($result["result"] !== 0) {
return $result;
}
foreach ($this->components as $component) {
$subTxn = $transaction->createSubTransaction();
$s = $component->uninstall($subTxn, $dryRun);
}
// Done!
return $this->setState(State::UNINSTALLED, $transaction, $dryRun, FALSE);
}
/**
* Install & configure the service.
* @return mixed
* array( "result" => 0, "error" => msg)
*/
public function install($transaction, $dryRun) {
$this->currentAction = "install";
// set flag to ensure smoke tests are run for client-only services
// as we just installed or re-configured something
$this->runClientSmokeTest = TRUE;
// Check if it's already INSTALLED or STARTED
if ($this->state === State::INSTALLED ||
$this->state === State::STARTED) {
$this->logger->log_debug("Service $this->name is already INSTALLED!");
return array("result" => 0, "error" => "");
}
// Ensure state is UNINSTALLED or FAILED
if ($this->state !== State::UNINSTALLED &&
$this->state !== State::FAILED &&
$this->state !== State::UNKNOWN &&
$this->state !== State::STOPPED) {
$this->logger->log_error("Service $this->name is not UNKNOWN or UNINSTALLED or FAILED or STOPPED! state=" . State::$STATE[$this->state]);
return array("result" => -1, "error" => "Service $this->name is not UNINSTALLED or FAILED or STOPPED! state=" . State::$STATE[$this->state]);
}
// Ensure each dependent service is INSTALLED
$result = $this->getDependencies($transaction);
if ($result["result"] !== 0) {
return $result;
}
foreach ($this->dependencies as $dep) {
$subTxn = $transaction->createSubTransaction();
$s = $dep->install($subTxn, $dryRun);
$depResult = $s['result'];
$depErrMsg = $s['error'];
if ($depResult !== 0) {
return array("result" => $depResult, "error" => "Failed to install $dep->name with $depResult (\'$depErrMsg\')");
}
}
// Note that we are about to INSTALL
$result = $this->setState(State::INSTALLING, $transaction, $dryRun, FALSE);
if ($result['result'] !== 0) {
return $result;
}
// Install self
// TODO: Special case, don't use Puppet here!
// Mark each component as INSTALLED
$result = $this->getComponents($transaction);
if ($result["result"] !== 0) {
return $result;
}
foreach ($this->components as $component) {
$subTxn = $transaction->createSubTransaction();
$s = $component->install($subTxn, $dryRun);
}
// Done!
return $this->setState(State::INSTALLED, $transaction, $dryRun, FALSE);
}
/**
* Requires components to be set before calling this api.
* @return boolean whether service has only client components
*/
private function checkIsClientOnly() {
if (isset($this->isClientOnly)
&& $this->isClientOnly != NULL) {
return $this->isClientOnly;
}
$isClientOnly = TRUE;
foreach ($this->components as $component) {
if (!$component->isClient) {
$isClientOnly = FALSE;
break;
}
}
$this->isClientOnly = $isClientOnly;
return $isClientOnly;
}
/**
* Start the service.
* @return mixed
* array( "result" => 0, "error" => msg)
*/
public function start($transaction, $dryRun) {
$this->currentAction = "start";
$result = $this->getComponents($transaction);
if ($result["result"] !== 0) {
return $result;
}
// Ensure each dependent service is STARTED
$result = $this->getDependencies($transaction);
if ($result["result"] !== 0) {
return $result;
}
foreach ($this->dependencies as $dep) {
$s = $dep->start($transaction->createSubTransaction(), $dryRun, TRUE);
$depResult = $s['result'];
$depErrMsg = $s['error'];
if ($depResult !== 0) {
return array("result" => $depResult,
"error" => "Failed to start $dep->name with $depResult (\'$depErrMsg\')");
}
}
$this->checkIsClientOnly();
$this->logger->log_debug("Service - " . $this->name . " - isClientOnly="
. $this->isClientOnly
. ", dryRun=" . $dryRun);
$persistTxn = TRUE;
$actualDryRun = $dryRun;
if ($this->isClientOnly) {
// this is to ensure that we do not persist the start sub-txn into the DB
// also start-stop state does not make sense for a client-only service
// we retain notion of START state in memory to ensure that we do not
// kick the smoke test twice
$persistTxn = FALSE;
$dryRun = TRUE;
}
// Check if it's already STARTED
if ($this->state === State::STARTED) {
$this->logger->log_debug("Service $this->name is already STARTED!");
return array("result" => 0, "error" => "");
}
// Ensure state is INSTALLED or STOPPED or FAILED
if ($this->state !== State::INSTALLED
&& $this->state !== State::STARTING
&& $this->state !== State::STOPPING
&& $this->state !== State::STOPPED
&& $this->state !== State::FAILED) {
$this->logger->log_error("Service $this->name is not INSTALLED or STOPPED or FAILED!");
return array("result" => -1,
"error" => "Service $this->name is not INSTALLED or STOPPED or FAILED!");
}
// Note that we are about to START
$result = $this->setState(State::STARTING, $transaction, $dryRun, $persistTxn);
if ($result['result'] !== 0) {
$this->setState(State::FAILED, $transaction, $dryRun, $persistTxn);
return $result;
}
if (!$this->isClientOnly) {
// Start each component
foreach ($this->components as $component) {
$s = $component->start($transaction->createSubTransaction(), $dryRun);
$cmpResult = $s['result'];
$cmpErrMsg = $s['error'];
if ($cmpResult !== 0) {
$this->setState(State::FAILED, $transaction, $dryRun, $persistTxn);
return array("result" => $cmpResult, "error" => "Failed to start $component->name with $cmpResult (\'$cmpErrMsg\')");
}
}
}
// Done!
$result = $this->setState(State::STARTED, $transaction, $dryRun, $persistTxn);
if ($result["result"] != 0) {
$this->setState(State::FAILED, $transaction, $dryRun, $persistTxn);
$this->logger->log_error("Failed to set state to STARTED with " . $result["error"]);
return $result;
}
return $this->smoke($transaction->getNextSubTransaction(), $actualDryRun);
}
private function setSmokeProgress($transaction, $dryRun, $txnProgress) {
$this->logger->log_debug("Setting smoke test progress for service="
. $this->name . ", dryrun=" . $dryRun
. ", progress=" . TransactionProgress::$PROGRESS[$txnProgress]);
if ($dryRun) {
$txnProgress = TransactionProgress::PENDING;
}
$desc = getActionDescription($this->displayName, "test",
TransactionProgress::$PROGRESS[$txnProgress]);
$result =
$this->db->persistTransaction($transaction,
TransactionProgress::$PROGRESS[$txnProgress],
$desc, TransactionProgress::$PROGRESS[$txnProgress],
"SERVICE-SMOKETEST", $dryRun);
/*
TODO error check later
if ($result['result'] !== 0) {
$this->state == State::FAILED;
$this->logger->log_error($this->name." - ".State::$STATE[$state]);
$this->db->setServiceState($this, $state);
return $result;
}
*/
return array("result" => 0, "error" => "");
}
public function smoke($transaction, $dryRun) {
$this->currentAction = "test";
$this->checkIsClientOnly();
if ($this->isClientOnly
&& !$this->runClientSmokeTest) {
$this->logger->log_info("Skipping client-only service smoke tests"
. " as nothing installed in this cycle");
return array("result" => 0, "error" => "");
}
$result = $this->db->getServiceClientNode($this->name);
if ($result == FALSE || $result["result"] != 0) {
$this->logger->log_error("Failed to access db to get service-client node for $this->name");
$this->setSmokeProgress($transaction, $dryRun,
TransactionProgress::FAILED);
return;
}
if (!is_array($result["nodes"]) || count($result["nodes"]) == 0 ) {
$this->logger->log_warn("Cannot find service-client node for $this->name");
// treating this as a no-op
// TODO - should it be a failure instead?
return array("result" => 0, "error" => "");
}
$clientNode = $result["nodes"][0];
// set smoke starting state
$this->setSmokeProgress($transaction, $dryRun,
TransactionProgress::IN_PROGRESS);
$result = $this->getComponents($transaction);
if ($result["result"] !== 0) {
return $result;
}
if (!$this->isClientOnly) {
// Check if it's already STARTED
// only in case service has non-client components
if ($this->state !== State::STARTED) {
$this->logger->log_debug("Service $this->name is not STARTED, cannot run smoke tests!");
$this->setSmokeProgress($transaction, $dryRun, TransactionProgress::FAILED);
return array("result" => -2, "error" =>
"Service $this->name is not STARTED, cannot run smoke tests!");
}
}
if (!$dryRun) {
$this->logger->log_debug("Kicking puppet for smoketesting service on "
. " cluster=" . $this->clusterName
. ", service=" . $this->name
. ", txn=" . $transaction->toString());
$startTime = time();
$result =
$this->puppet->kickServiceCheck( array($this->name => $clientNode),
$transaction, $this->clusterName);
$this->logger->log_debug("Puppet kick response for smoketesting service on "
. " cluster=" . $this->clusterName
. ", service=" . $this->name
. ", txn=" . $transaction->toString()
. ", response=" . print_r($result, true));
// handle puppet response
$timeTaken = time() - $startTime;
$opStatus = array(
"stats" =>
array (
"NODE_COUNT" => 1,
"TIME_TAKEN_SECS" => $timeTaken),
"nodeReport" =>
array ( "PUPPET_KICK_FAILED" => $result[KICKFAILED],
"PUPPET_OPERATION_FAILED" => $result[FAILEDNODES],
"PUPPET_OPERATION_SUCCEEDED" => $result[SUCCESSFULLNODES]));
$this->logger->log_info("Persisting puppet report for smoke testing "
. $this->name);
$this->db->persistTransactionOpStatus($transaction,
json_encode($opStatus));
if ($result["result"] != 0
|| count($result[SUCCESSFULLNODES]) != 1) {
$this->logger->log_error("Service smoke check failed with "
. print_r($result, true));
$this->setState(State::FAILED, $transaction, $dryRun, TRUE);
$this->setSmokeProgress($transaction, $dryRun,
TransactionProgress::FAILED);
return array("result" => -2, "error" =>
"Service $this->name is not STARTED, smoke tests failed!");
}
}
$this->setSmokeProgress($transaction, $dryRun,
TransactionProgress::COMPLETED);
return array("result" => 0, "error" => "");
}
/**
* Stop the service.
* @return mixed
* array( "result" => 0, "error" => msg)
*/
public function stop($transaction, $dryRun) {
$this->currentAction = "stop";
$result = $this->getComponents($transaction);
if ($result["result"] !== 0) {
return $result;
}
// Ensure each dependent service is STOPPED
$result = $this->getDependents($transaction);
if ($result["result"] !== 0) {
return $result;
}
foreach ($this->dependents as $dep) {
$s = $dep->stop($transaction->createSubTransaction(), $dryRun);
$depResult = $s['result'];
$depErrMsg = $s['error'];
if ($depResult !== 0) {
return array("result" => $depResult, "error" => "Failed to stop $dep->name with $depResult (\'$depErrMsg\')");
}
}
$this->checkIsClientOnly();
if (!$this->isClientOnly) {
// Check if it's already STOPPED
if ($this->state === State::STOPPED) {
$this->logger->log_info("Service $this->name is already STOPPED!");
return array("result" => 0, "error" => "");
}
// Only stop if state is STARTED/STARTING/STOPPING/FAILED
if ($this->state !== State::STARTED
&& $this->state !== State::STARTING
&& $this->state !== State::STOPPING
&& $this->state !== State::FAILED) {
$this->logger->log_info("Service " . $this->name . " is not STARTED/STOPPING/FAILED!"
. "Current state = " . State::$STATE[$this->state]
. " - STOP is a no-op");
return array("result" => 0, "error" => "");
}
// Note we are about to STOP
$result = $this->setState(State::STOPPING, $transaction, $dryRun, TRUE);
if ($result['result'] !== 0) {
$this->setState(State::FAILED, $transaction, $dryRun, TRUE);
return $result;
}
// Stop each component
foreach ($this->components as $component) {
$s = $component->stop($transaction->createSubTransaction(), $dryRun);
$cmpResult = $s['result'];
$cmpErrMsg = $s['error'];
if ($cmpResult !== 0) {
$this->setState(State::FAILED, $transaction, $dryRun, TRUE);
return array("result" => $cmpResult, "error" => "Failed to stop $component->name with $cmpResult (\'$cmpErrMsg\')");
}
}
// Done!
return $this->setState(State::STOPPED, $transaction, $dryRun, TRUE);
}
return array("result" => 0, "error" => "");
}
private function getDependencies($transaction) {
if (!isset($this->dependencies)) {
$this->dependencies = $this->db->getServiceDependencies($this->name);
}
return $this->checkDBReturn($transaction, $this->dependencies);
}
private function getDependents($transaction) {
if (!isset($this->dependents)) {
$this->dependents = $this->db->getServiceDependents($this->name);
}
return $this->checkDBReturn($transaction, $this->dependents);
}
private function getComponents($transaction) {
if (!isset($this->components)) {
$this->components = $this->db->getServiceComponents($this->name);
}
return $this->checkDBReturn($transaction, $this->components);
}
private function checkDBReturn($transaction, $dbResult) {
if ($dbResult === FALSE) {
$trace = debug_backtrace();
$this->logger->log_error("DB Error: " . $trace[1]["function"]);
$this->setState(State::FAILED, $transaction, FALSE, TRUE);
return array("result" => $dbResult, "error" => "Failed to update db for $this->name with $dbResult");
}
return array("result" => 0, "error" => "");
}
}
?>