blob: d5dee9b00bc0389ac0b73ccbb234c327f9c07e15 [file] [log] [blame]
// 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
// 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 com.cloud.usage;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import com.cloud.utils.exception.CloudRuntimeException;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import com.cloud.utils.db.TransactionLegacy;
/**
* This class must not be used concurrently because its state changes often during
* execution in a non synchronized way
*/
public class UsageSanityChecker {
protected static Logger LOGGER = LogManager.getLogger(UsageSanityChecker.class);
protected static final int DEFAULT_AGGREGATION_RANGE = 1440;
protected StringBuilder errors;
protected List<CheckCase> checkCases;
protected String lastCheckFile = "/usr/local/libexec/sanity-check-last-id";
protected String lastCheckId = "";
protected int lastId = -1;
protected int maxId = -1;
protected Connection conn;
protected void reset() {
errors = new StringBuilder();
checkCases = new ArrayList<>();
}
protected boolean checkItemCountByPstmt() throws SQLException {
boolean checkOk = true;
for(CheckCase check : checkCases) {
checkOk &= checkItemCountByPstmt(check);
}
return checkOk;
}
protected boolean checkItemCountByPstmt(CheckCase checkCase) throws SQLException {
boolean checkOk = true;
/*
* Check for item usage records which are created after it is removed
*/
try (PreparedStatement pstmt = conn.prepareStatement(checkCase.getSqlTemplate())) {
if (checkCase.isCheckId()) {
if (lastId > 0) {
pstmt.setInt(1, lastId);
}
if (maxId > lastId) {
pstmt.setInt(2, maxId);
}
}
checkOk = isCheckOkForPstmt(checkCase, checkOk, pstmt);
}
catch (Exception e)
{
throwPreparedStatementExcecutionException("preparing statement", checkCase.getSqlTemplate(), e);
}
return checkOk;
}
private boolean isCheckOkForPstmt(CheckCase checkCase, boolean checkOk, PreparedStatement pstmt) {
try (ResultSet rs = pstmt.executeQuery();) {
if (rs.next() && (rs.getInt(1) > 0)) {
errors.append(String.format("Error: Found %s %s%n", rs.getInt(1), checkCase.getItemName()));
checkOk = false;
}
} catch (Exception e)
{
throwPreparedStatementExcecutionException("check is failing", pstmt.toString(), e);
}
return checkOk;
}
private static void throwPreparedStatementExcecutionException(String msgPrefix, String stmt, Exception e) {
String msg = String.format("%s for prepared statement \"%s\" reason: %s", msgPrefix, stmt, e.getMessage());
LOGGER.error(msg);
throw new CloudRuntimeException(msg, e);
}
protected void checkMaxUsage() throws SQLException {
int aggregationRange = DEFAULT_AGGREGATION_RANGE;
String sql = "SELECT value FROM `cloud`.`configuration` WHERE name = 'usage.stats.job.aggregation.range'";
try (PreparedStatement pstmt = conn.prepareStatement(sql);)
{
aggregationRange = getAggregationRange(aggregationRange, pstmt);
} catch (SQLException e) {
throwPreparedStatementExcecutionException("preparing atatement", sql, e);
}
int aggregationHours = aggregationRange / 60;
addCheckCase("SELECT count(*) FROM `cloud_usage`.`cloud_usage` cu where usage_type not in (4,5,13) and raw_usage > " + aggregationHours,
"usage records with raw_usage > " + aggregationHours,
lastCheckId);
addCheckCase("SELECT count(*) " +
" FROM ( SELECT cu.id, max(cu.raw_usage)/count(n.id) as avg_usage " +
" FROM `cloud_usage`.`cloud_usage` AS cu " +
" INNER JOIN cloud.nics AS n ON (n.instance_id = cu.vm_instance_id) " +
" WHERE cu.usage_type = 13 AND ((n.created <= cu.end_date) AND (n.removed is null OR n.removed > cu.start_date)) " +
" GROUP BY cu.id) as cu " +
" WHERE cu.avg_usage > " + aggregationHours,
"network offering usage records with raw_usage > " + aggregationHours,
lastCheckId);
}
private static int getAggregationRange(int aggregationRange, PreparedStatement pstmt) {
try (ResultSet rs = pstmt.executeQuery();) {
if (rs.next()) {
aggregationRange = rs.getInt(1);
} else {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Failed to retrieve aggregation range. Using default : " + aggregationRange);
}
}
} catch (SQLException e) {
throwPreparedStatementExcecutionException("retrieval aggregate value is failing", pstmt.toString(), e);
}
return aggregationRange;
}
protected void checkVmUsage() {
addCheckCase("select count(*) from cloud_usage.cloud_usage cu inner join cloud.vm_instance vm "
+ "where vm.type = 'User' and cu.usage_type in (1 , 2) "
+ "and cu.usage_id = vm.id and cu.start_date > vm.removed ",
"Vm usage records which are created after Vm is destroyed",
lastCheckId);
addCheckCase("select sum(cnt) from (select count(*) as cnt from cloud_usage.usage_vm_instance "
+ "where usage_type =1 and end_date is null group by vm_instance_id "
+ "having count(vm_instance_id) > 1) c ;",
"duplicate running Vm entries in vm usage helper table");
addCheckCase("select sum(cnt) from (select count(*) as cnt from cloud_usage.usage_vm_instance "
+ "where usage_type =2 and end_date is null group by vm_instance_id "
+ "having count(vm_instance_id) > 1) c ;",
"duplicate allocated Vm entries in vm usage helper table");
addCheckCase("select count(vm_instance_id) from cloud_usage.usage_vm_instance o "
+ "where o.end_date is null and o.usage_type=1 and not exists "
+ "(select 1 from cloud_usage.usage_vm_instance i where "
+ "i.vm_instance_id=o.vm_instance_id and usage_type=2 and i.end_date is null)",
"running Vm entries without corresponding allocated entries in vm usage helper table");
}
protected void checkVolumeUsage() {
addCheckCase("select count(*) from cloud_usage.cloud_usage cu inner join cloud.volumes v where "
+ "cu.usage_type = 6 and cu.usage_id = v.id and cu.start_date > v.removed ",
"volume usage records which are created after volume is removed",
lastCheckId);
addCheckCase("select sum(cnt) from (select count(*) as cnt from cloud_usage.usage_volume "
+ "where deleted is null group by id having count(id) > 1) c;",
"duplicate records in volume usage helper table");
}
protected void checkTemplateISOUsage() {
addCheckCase("SELECT count(*) " +
" FROM cloud_usage.cloud_usage AS cu " +
" INNER JOIN cloud.template_zone_ref AS c_tzr ON ( c_tzr.template_id = cu.usage_id " +
" AND c_tzr.zone_id = cu.zone_id) " +
" WHERE cu.usage_type in (7,8) " +
" AND cu.start_date > c_tzr.removed " +
" AND NOT EXISTS ( SELECT 1 " +
" FROM cloud.template_zone_ref c_tzr_internal " +
" WHERE c_tzr_internal.template_id = c_tzr.template_id " +
" AND c_tzr_internal.zone_id = c_tzr.zone_id " +
" AND c_tzr_internal.removed IS NULL) ",
"template/ISO usage records which are created after it is removed",
lastCheckId);
}
protected void checkSnapshotUsage() {
addCheckCase("select count(*) from cloud_usage.cloud_usage cu inner join cloud.snapshots s where "
+ "cu.usage_id = s.id and cu.usage_type = 9 and cu.start_date > s.removed ",
"snapshot usage records which are created after it is removed",
lastCheckId);
}
protected void readLastCheckId(){
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("reading last checked id for sanity check");
}
try(BufferedReader reader = new BufferedReader(new FileReader(lastCheckFile));) {
String lastIdText = null;
lastId = -1;
if ((lastIdText = reader.readLine()) != null) {
LOGGER.info("Read {} as lastId for Usage sanity checking.", lastIdText);
lastId = Integer.parseInt(lastIdText);
}
} catch (Exception e) {
String msg = String.format("error reading the LastCheckId reason: %s", e.getMessage());
LOGGER.error(msg);
LOGGER.debug(msg, e);
} finally {
LOGGER.info(String.format("using %d as last checked id to start from in sanity check", lastId));
}
}
protected void readMaxId() throws SQLException {
try (PreparedStatement pstmt = conn.prepareStatement("select max(id) from cloud_usage.cloud_usage");
ResultSet rs = pstmt.executeQuery();)
{
maxId = -1;
if (rs.next() && (rs.getInt(1) > 0)) {
maxId = rs.getInt(1);
LOGGER.info("Read {} as maxId for Usage sanity checking.", maxId);
if (maxId > lastId) {
LOGGER.info("The max id {} is greater than the last id {}; adding id check to the query.", maxId, lastId);
lastCheckId += " and cu.id <= ?";
}
}
}catch (Exception e) {
LOGGER.error("readMaxId:"+e.getMessage(),e);
}
}
protected void updateNewMaxId() {
LOGGER.info(String.format("writing %d as the new last id checked", maxId));
try (FileWriter fstream = new FileWriter(lastCheckFile);
BufferedWriter out = new BufferedWriter(fstream);
){
out.write("" + maxId);
} catch (IOException e) {
LOGGER.error(String.format("Exception writing the last checked id: %d reason: %s", maxId, e.getMessage()));
// Error while writing last check id
}
}
public String runSanityCheck() throws SQLException {
readLastCheckId();
if (lastId > 0) {
lastCheckId = " and cu.id > ?";
}
conn = getConnection();
readMaxId();
reset();
checkMaxUsage();
checkVmUsage();
checkVolumeUsage();
checkTemplateISOUsage();
checkSnapshotUsage();
checkItemCountByPstmt();
updateNewMaxId();
return errors.toString();
}
/**
* Local acquisition of {@link Connection} to remove static cling
* @return
*/
protected Connection getConnection() {
return TransactionLegacy.getStandaloneConnection();
}
/**
* usage something like: /usr/bin/java -Xmx2G -cp /usr/share/cloudstack-usage/*:/usr/share/cloudstack-usage/lib/*:/usr/share/cloudstack-mysql-ha/lib/*:/etc/cloudstack/usage:/usr/share/java/mysql-connector-java.jar:/usr/share/cloudstack-common com.cloud.usage.UsageSanityChecker
* @param args none
*/
public static void main(String[] args) {
UsageSanityChecker usc = new UsageSanityChecker();
String sanityErrors;
try {
sanityErrors = usc.runSanityCheck();
if (sanityErrors.length() > 0) {
LOGGER.error(sanityErrors);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
protected void addCheckCase(String sqlTemplate, String itemName, String lastCheckId) {
checkCases.add(new CheckCase(sqlTemplate, itemName, lastCheckId));
}
protected void addCheckCase(String sqlTemplate, String itemName) {
checkCases.add(new CheckCase(sqlTemplate, itemName));
}
}
/**
* Just an abstraction of the kind of check to repeat across these cases
* encapsulating what change for each specific case
*/
class CheckCase {
public String getSqlTemplate() {
return this.sqlTemplate;
}
public void setSqlTemplate(final String sqlTemplate) {
this.sqlTemplate = sqlTemplate;
}
public String getItemName() {
return this.itemName;
}
public void setItemName(final String itemName) {
this.itemName = itemName;
}
public boolean isCheckId() {
return this.checkId;
}
public void setCheckId(final boolean checkId) {
this.checkId = checkId;
}
private String sqlTemplate;
private String itemName;
private boolean checkId = false;
public CheckCase(String sqlTemplate, String itemName, String lastCheckId) {
setCheckId(true);
setSqlTemplate(sqlTemplate + lastCheckId);
setItemName(itemName);
}
public CheckCase(String sqlTemplate, String itemName) {
checkId = false;
setSqlTemplate(sqlTemplate);
setItemName(itemName);
}
}