package org.ovirt.engine.core.notifier; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.Map; import org.apache.commons.lang.StringUtils; import org.ovirt.engine.core.common.EventNotificationMethods; import org.ovirt.engine.core.common.businessentities.DbUser; import org.ovirt.engine.core.common.businessentities.event_audit_log_subscriber; import org.ovirt.engine.core.common.businessentities.event_notification_hist; import org.ovirt.engine.core.common.businessentities.event_notification_methods; import org.ovirt.engine.core.compat.Guid; import org.ovirt.engine.core.compat.LogCompat; import org.ovirt.engine.core.compat.LogFactoryCompat; import org.ovirt.engine.core.compat.NGuid; import org.ovirt.engine.core.notifier.methods.EventMethodFiller; import org.ovirt.engine.core.notifier.methods.NotificationMethodMapBuilder; import org.ovirt.engine.core.notifier.methods.NotificationMethodMapBuilder.NotificationMethodFactoryMapper; import org.ovirt.engine.core.notifier.utils.ConnectionHelper; import org.ovirt.engine.core.notifier.utils.ConnectionHelper.NaiveConnectionHelperException; import org.ovirt.engine.core.notifier.utils.NotificationConfigurator; import org.ovirt.engine.core.notifier.utils.NotificationProperties; import org.ovirt.engine.core.notifier.utils.sender.EventSender; import org.ovirt.engine.core.notifier.utils.sender.EventSenderResult; /** * Responsible for an execution of the service for the current events in the system which should be notified to the * subscribers. */ public class NotificationService implements Runnable { private static final LogCompat log = LogFactoryCompat.getLog(NotificationService.class); private ConnectionHelper connectionHelper = null; private Map<String, String> prop = null; private NotificationMethodFactoryMapper methodsMapper = null; private boolean shouldDeleteHistory = false; private int daysToKeepHistory; public NotificationService(NotificationConfigurator notificationConf) throws NotificationServiceException { this.prop = notificationConf.getProperties(); initConfigurationProperties(); } /** * Validates the correctness of properties set in the configuration file.<br> * If any of the properties is invalid, an error will be sent and service initialization fails.<br> * Validated properties are: <li>DAYS_TO_KEEP_HISTORY - property could be omitted, if specified should be a positive * <li>INTERVAL_IN_SECONDS - property is mandatory, if specified should be a positive <li>DB Connectivity * Credentials - if failed to obtain connection to database, fails * @throws NotificationServiceException * configuration setting error */ private void initConfigurationProperties() throws NotificationServiceException { String daysHistoryStr = prop.get(NotificationProperties.DAYS_TO_KEEP_HISTORY); // verify property of history is well defined if (StringUtils.isNotEmpty(daysHistoryStr)) { try { daysToKeepHistory = Integer.valueOf(daysHistoryStr).intValue(); if (daysToKeepHistory < 0) { throw new NumberFormatException(NotificationProperties.DAYS_TO_KEEP_HISTORY + " value should be a positive number"); } daysToKeepHistory = daysToKeepHistory * -1; shouldDeleteHistory = true; } catch (NumberFormatException e) { String err = String.format("Invalid format of %s: %s", NotificationProperties.DAYS_TO_KEEP_HISTORY, daysHistoryStr); log.error(err, e); throw new NotificationServiceException(err,e); } } initConnectivity(); initMethodMapper(); } /** * Executes event notification to subscribers */ public void run() { try { log.debug("Start event notification service iteration"); startup(); processEvents(); if (shouldDeleteHistory) { deleteObseleteHistoryData(); } log.debug("Finish event notification service iteration"); } catch (Throwable e) { if (!Thread.interrupted()) { log.error(String.format("Failed to run the service: [%s]", e.getMessage()), e); } // since notification service uses same instance to recurrent notification process - // only upon an exception the DB connection will be closed and will be reopened the next // time the service will start to run by calling ConnectionHelper.getConnection() // the connection won't be closed in a finally block since the service is planned to work constantly // against the DB and same connection could be served for a long period. shutdown(); } } /** * Starts or verified that required resources for the service are available * @throws NotificationServiceException * specifies which resource not available and a cause */ private void startup() throws NotificationServiceException { try { if (connectionHelper == null) { connectionHelper = new ConnectionHelper(prop); } else { connectionHelper.getConnection(); } } catch (NaiveConnectionHelperException e) { throw new NotificationServiceException("Failed to initialize the connection helper", e); } } /** * Releases any resources which is held by the service:<br> * <li>Close open DB connection */ public void shutdown() { if (connectionHelper != null) { connectionHelper.closeConnection(); } } // TODO: Consider adding deleteObseleteHistoryData() as a separate scheduled thread run on a daily basis private void deleteObseleteHistoryData() throws SQLException, NaiveConnectionHelperException { Calendar cal = Calendar.getInstance(); cal.setTime(new Date()); cal.add(Calendar.DATE, daysToKeepHistory); java.sql.Timestamp startDeleteFrom = new java.sql.Timestamp(cal.getTimeInMillis()); PreparedStatement deleteStmt = null; int deletedRecords; try { deleteStmt = connectionHelper.getConnection().prepareStatement("delete from event_notification_hist where sent_at < ?"); deleteStmt.setTimestamp(1, startDeleteFrom); deletedRecords = deleteStmt.executeUpdate(); } finally { if (deleteStmt != null) { deleteStmt.close(); } } if (deletedRecords > 0) { log.debug(String.valueOf(deletedRecords) + " records were deleted from event_notification_hist table"); } } private void initMethodMapper() throws NotificationServiceException { EventMethodFiller methodFiller = new EventMethodFiller(); try { methodFiller.fillEventNotificationMethods(connectionHelper.getConnection()); } catch (Exception e) { throw new NotificationServiceException("Failed to initialize method mapper", e); } List<event_notification_methods> eventNotificationMethods = methodFiller.getEventNotificationMethods(); methodsMapper = NotificationMethodMapBuilder.instance().createMethodsMapper(eventNotificationMethods, prop); } private void initConnectivity() throws NotificationServiceException { try { connectionHelper = new ConnectionHelper(prop); } catch (NaiveConnectionHelperException e) { throw new NotificationServiceException("Failed to obtain database connectivity", e); } } private void processEvents() throws SQLException, NaiveConnectionHelperException { PreparedStatement ps = null; ResultSet rs = null; try { ps = connectionHelper.getConnection() .prepareStatement("select * from event_audit_log_subscriber_view " + "where audit_log_id <= (select max(audit_log_id) from audit_log)"); rs = ps.executeQuery(); event_audit_log_subscriber eventSubscriber; DbUser dbUser = null; while (rs.next()) { eventSubscriber = getEventAuditLogSubscriber(rs); dbUser = getUserByUserId(eventSubscriber.getsubscriber_id()); if (dbUser != null) { EventSender method = methodsMapper.getMethod(EventNotificationMethods.forValue(eventSubscriber.getmethod_id())); EventSenderResult sendResult = null; try { sendResult = method.send(eventSubscriber, dbUser.getemail()); } catch (Exception e) { log.error("Failed to dispatch message", e); sendResult = new EventSenderResult(); sendResult.setSent(false); sendResult.setReason(e.getMessage()); } addEventNotificationHistory(geteventNotificationHist(eventSubscriber, sendResult.isSent(), sendResult.getReason())); updateAuditLogEventProcessed(eventSubscriber); } } } finally { if (rs != null) { try { rs.close(); } catch (SQLException e) { log.error("Failed to release resultset of event_audit_log_subscriber", e); } } if (ps != null) { try { ps.close(); } catch (SQLException e) { log.error("Failed to release statement of event_audit_log_subscriber", e); throw e; } } } } private void updateAuditLogEventProcessed(event_audit_log_subscriber eventSubscriber) throws SQLException, NaiveConnectionHelperException { PreparedStatement ps = null; try { ps = connectionHelper.getConnection() .prepareStatement("update audit_log set processed = 'true' where audit_log_id = ?"); ps.setLong(1, eventSubscriber.getaudit_log_id()); int updated = ps.executeUpdate(); if (updated != 1) { log.error("Failed to mark audit_log entry as processed for audit_log_id: " + eventSubscriber.getaudit_log_id()); } } finally { if (ps != null) { ps.close(); } } } private void addEventNotificationHistory(event_notification_hist eventHistory) throws SQLException, NaiveConnectionHelperException { CallableStatement cs = null; try { cs = connectionHelper.getConnection().prepareCall("{call Insertevent_notification_hist(?,?,?,?,?,?,?)}"); cs.setLong(1, eventHistory.getaudit_log_id()); cs.setString(2, eventHistory.getevent_name()); cs.setString(3, eventHistory.getmethod_type()); cs.setString(4, eventHistory.getreason()); cs.setTimestamp(5, new java.sql.Timestamp(eventHistory.getsent_at().getTime())); cs.setBoolean(6, eventHistory.getstatus()); cs.setString(7, eventHistory.getsubscriber_id().toString()); cs.executeUpdate(); } finally { if (cs != null) { cs.close(); } } } private event_notification_hist geteventNotificationHist(event_audit_log_subscriber eals, boolean isNotified, String reason) { event_notification_hist eventHistory = new event_notification_hist(); eventHistory.setaudit_log_id(eals.getaudit_log_id()); eventHistory.setevent_name(eals.getevent_up_name()); eventHistory.setmethod_type(EventNotificationMethods.forValue(eals.getmethod_id()).name()); eventHistory.setreason(reason); eventHistory.setsent_at(new Date()); eventHistory.setstatus(isNotified); eventHistory.setsubscriber_id(eals.getsubscriber_id()); return eventHistory; } private DbUser getUserByUserId(Guid userId) throws SQLException, NaiveConnectionHelperException { // Using preparedStatement instead of STP GetUserByUserId to skip handling supporting dialects // for MSSQL and PG. PG doesn't support parameter name which matches a column name. This is supported // by the backend, since using a plan JDBC, bypassing this issue by prepared statement. // in additional, required only partial email field of the DbUser Statement ps = null; ResultSet rs = null; DbUser dbUser = null; try { ps = connectionHelper.getConnection().createStatement(); rs = ps.executeQuery(String.format("SELECT email FROM users WHERE user_id = '%s'", userId.toString())); if (rs.next()) { dbUser = new DbUser(); dbUser.setuser_id(userId); dbUser.setemail(rs.getString("email")); } } finally { if (rs != null) { try { rs.close(); } catch (SQLException e) { log.error("Failed to release resultset of db user query", e); } } if (ps != null) { ps.close(); } } return dbUser; } private event_audit_log_subscriber getEventAuditLogSubscriber(ResultSet rs) throws SQLException { event_audit_log_subscriber eals = new event_audit_log_subscriber(); eals.setevent_type(rs.getInt("event_type")); eals.setsubscriber_id(Guid.createGuidFromString(rs.getString("subscriber_id"))); eals.setevent_up_name(rs.getString("event_up_name")); eals.setmethod_id(rs.getInt("method_id")); eals.setmethod_address(rs.getString("method_address")); eals.settag_name(rs.getString("tag_name")); eals.setaudit_log_id(rs.getLong("audit_log_id")); eals.setuser_id(NGuid.createGuidFromString(rs.getString("user_id"))); eals.setuser_name(rs.getString("user_name")); eals.setvm_id(NGuid.createGuidFromString(rs.getString("vm_id"))); eals.setvm_name(rs.getString("vm_name")); eals.setvm_template_id(NGuid.createGuidFromString(rs.getString("vm_template_id"))); eals.setvm_template_name(rs.getString("vm_template_name")); eals.setvds_id(NGuid.createGuidFromString(rs.getString("vds_id"))); eals.setvds_name(rs.getString("vds_name")); eals.setstorage_pool_id(Guid.createGuidFromString(rs.getString("storage_pool_id"))); eals.setstorage_pool_name(rs.getString("storage_pool_name")); eals.setstorage_domain_id(Guid.createGuidFromString(rs.getString("storage_domain_id"))); eals.setstorage_domain_name(rs.getString("storage_domain_name")); eals.setlog_time(rs.getTimestamp("log_time")); eals.setseverity(rs.getInt("severity")); eals.setmessage(rs.getString("message")); return eals; } }