/** * 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.ambari.server.notifications.dispatchers; import java.io.IOException; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.ambari.server.notifications.Notification; import org.apache.ambari.server.notifications.NotificationDispatcher; import org.apache.ambari.server.notifications.Recipient; import org.apache.ambari.server.notifications.TargetConfigurationResult; import org.apache.ambari.server.state.alert.TargetType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.snmp4j.CommunityTarget; import org.snmp4j.PDU; import org.snmp4j.Snmp; import org.snmp4j.Target; import org.snmp4j.UserTarget; import org.snmp4j.mp.MPv3; import org.snmp4j.mp.SnmpConstants; import org.snmp4j.security.AuthMD5; import org.snmp4j.security.PrivDES; import org.snmp4j.security.SecurityLevel; import org.snmp4j.security.SecurityModel; import org.snmp4j.security.SecurityModels; import org.snmp4j.security.SecurityProtocols; import org.snmp4j.security.USM; import org.snmp4j.security.UsmUser; import org.snmp4j.smi.OID; import org.snmp4j.smi.OctetString; import org.snmp4j.smi.UdpAddress; import org.snmp4j.smi.VariableBinding; import org.snmp4j.transport.DefaultUdpTransportMapping; import org.snmp4j.transport.UdpTransportMapping; import org.snmp4j.util.DefaultPDUFactory; import com.google.inject.Singleton; /** * The {@link SNMPDispatcher} class is used to dispatch {@link Notification} via SNMP. */ @Singleton public class SNMPDispatcher implements NotificationDispatcher { private static final Logger LOG = LoggerFactory.getLogger(SNMPDispatcher.class); // Trap's object identifiers public static final String BODY_OID_PROPERTY = "ambari.dispatch.snmp.oids.body"; public static final String SUBJECT_OID_PROPERTY = "ambari.dispatch.snmp.oids.subject"; public static final String TRAP_OID_PROPERTY = "ambari.dispatch.snmp.oids.trap"; // SNMP Server port public static final String PORT_PROPERTY = "ambari.dispatch.snmp.port"; // SNMP version public static final String SNMP_VERSION_PROPERTY = "ambari.dispatch.snmp.version"; // Properties for community-based security model configuration public static final String COMMUNITY_PROPERTY = "ambari.dispatch.snmp.community"; // Properties for user-based security model configuration public static final String SECURITY_USERNAME_PROPERTY = "ambari.dispatch.snmp.security.username"; public static final String SECURITY_AUTH_PASSPHRASE_PROPERTY = "ambari.dispatch.snmp.security.auth.passphrase"; public static final String SECURITY_PRIV_PASSPHRASE_PROPERTY = "ambari.dispatch.snmp.security.priv.passphrase"; public static final String SECURITY_LEVEL_PROPERTY = "ambari.dispatch.snmp.security.level"; private Snmp snmp; private final Integer port; private volatile UdpTransportMapping transportMapping; public SNMPDispatcher(Snmp snmp) { this.port = null; this.snmp = snmp; } public SNMPDispatcher() throws IOException { this((Integer) null); } /** * Creates SNMP server with specified port. In case port is null will be used random value as default * @param port port * @throws IOException */ public SNMPDispatcher(Integer port) throws IOException { if(port != null && port >= 0 && port <= '\uffff') { //restrict invalid ports to avoid exception on socket create this.port = port; } else { this.port = null; } } private void createTransportMapping() throws IOException { if (transportMapping == null) { synchronized (this) { if (transportMapping == null) { if (port != null) { LOG.info("Setting SNMP dispatch port: " + port); transportMapping = new DefaultUdpTransportMapping(new UdpAddress(port), true); } else { transportMapping = new DefaultUdpTransportMapping(); } } } } } /** * {@inheritDoc} */ @Override public String getType() { return TargetType.SNMP.name(); } /** * {@inheritDoc} */ @Override public boolean isNotificationContentGenerationRequired() { return true; } /** * {@inheritDoc} */ @Override public void dispatch(Notification notification) { LOG.info("Sending SNMP trap: {}", notification.Subject); try { createTransportMapping(); snmp = new Snmp(transportMapping); SnmpVersion snmpVersion = getSnmpVersion(notification.DispatchProperties); sendTraps(notification, snmpVersion); successCallback(notification); } catch (InvalidSnmpConfigurationException ex) { LOG.error("Unable to dispatch SNMP trap with invalid configuration. " + ex.getMessage()); failureCallback(notification); } catch (Exception ex) { LOG.error("Error occurred during SNMP trap dispatching.", ex); failureCallback(notification); transportMapping = null; } } /** * {@inheritDoc} */ @Override public TargetConfigurationResult validateTargetConfig(Map<String, Object> properties) { Map<String, String> stringValuesConfig = new HashMap<>(properties.size()); for (Map.Entry<String, Object> propertyEntry : properties.entrySet()) { stringValuesConfig.put(propertyEntry.getKey(), propertyEntry.getValue().toString()); } try { for (String property : getSetOfDefaultNeededPropertyNames()) { getDispatchProperty(stringValuesConfig, property); } SnmpVersion snmpVersion = getSnmpVersion(stringValuesConfig); switch (snmpVersion) { case SNMPv3: getDispatchProperty(stringValuesConfig, SECURITY_USERNAME_PROPERTY); TrapSecurity securityLevel = getSecurityLevel(stringValuesConfig); switch (securityLevel) { case AUTH_PRIV: getDispatchProperty(stringValuesConfig, SECURITY_PRIV_PASSPHRASE_PROPERTY); getDispatchProperty(stringValuesConfig, SECURITY_AUTH_PASSPHRASE_PROPERTY); break; case AUTH_NOPRIV: getDispatchProperty(stringValuesConfig, SECURITY_AUTH_PASSPHRASE_PROPERTY); break; } break; case SNMPv2c: case SNMPv1: getDispatchProperty(stringValuesConfig, COMMUNITY_PROPERTY); break; } } catch (InvalidSnmpConfigurationException ex) { return TargetConfigurationResult.invalid(ex.getMessage()); } return TargetConfigurationResult.valid(); } /** * @return Set that contains names of properties that are needed for all SNMP configurations. */ protected Set<String> getSetOfDefaultNeededPropertyNames() { return new HashSet<>(Arrays.asList(BODY_OID_PROPERTY, SUBJECT_OID_PROPERTY, TRAP_OID_PROPERTY, PORT_PROPERTY)); } /** * Creates protocol data unit (PDU) with corresponding SNMP version for alert notification. * @param notification alert notification to dispatch * @param snmpVersion SNMP version * @return PDU containing notification info * @throws InvalidSnmpConfigurationException if notification's dispatch properties don't contain any of required properties. */ protected PDU prepareTrap(Notification notification, SnmpVersion snmpVersion) throws InvalidSnmpConfigurationException { PDU pdu = DefaultPDUFactory.createPDU(snmpVersion.getTargetVersion()); pdu.setType(snmpVersion.getTrapType()); // Set trap oid for PDU pdu.add(new VariableBinding(SnmpConstants.snmpTrapOID, new OID(getDispatchProperty(notification.DispatchProperties, TRAP_OID_PROPERTY)))); // Set notification body and subject for PDU objects with identifiers specified in dispatch properties. pdu.add(new VariableBinding(new OID(getDispatchProperty(notification.DispatchProperties, BODY_OID_PROPERTY)), new OctetString(notification.Body))); pdu.add(new VariableBinding(new OID(getDispatchProperty(notification.DispatchProperties, SUBJECT_OID_PROPERTY)), new OctetString(notification.Subject))); return pdu; } /** * Creates trap based on alerts notification and sends it to hosts specified in recipients list. * @param notification alert notification to dispatch * @param snmpVersion SNMP version * @throws InvalidSnmpConfigurationException if notification's dispatch properties don't contain any of required properties or recipient list is empty. * @throws IOException if the SNMP trap could not be sent */ protected void sendTraps(Notification notification, SnmpVersion snmpVersion) throws InvalidSnmpConfigurationException, IOException { PDU trap = prepareTrap(notification, snmpVersion); String udpPort = getDispatchProperty(notification.DispatchProperties, PORT_PROPERTY); for (Recipient recipient : getNotificationRecipients(notification)) { String address = recipient.Identifier; Target target = createTrapTarget(notification, snmpVersion); target.setAddress(new UdpAddress(address + "/" + udpPort)); snmp.send(trap, target); } } /** * Creates snmp target with security model corresponding to snmp version. * @param notification alerts notification * @param snmpVersion SNMP version * @return target with corresponding security model * @throws InvalidSnmpConfigurationException if notification's dispatch properties don't contain any of required properties */ protected Target createTrapTarget(Notification notification, SnmpVersion snmpVersion) throws InvalidSnmpConfigurationException { if (snmpVersion.isCommunityTargetRequired()) { OctetString community = new OctetString(getDispatchProperty(notification.DispatchProperties, COMMUNITY_PROPERTY)); CommunityTarget communityTarget = new CommunityTarget(); communityTarget.setCommunity(community); communityTarget.setVersion(snmpVersion.getTargetVersion()); return communityTarget; } else { OctetString userName = new OctetString(getDispatchProperty(notification.DispatchProperties, SECURITY_USERNAME_PROPERTY)); if (snmp.getUSM() == null) { // provide User-based Security Model (USM) with user specified USM usm = new USM(SecurityProtocols.getInstance(), new OctetString(MPv3.createLocalEngineID()), 0); // authPassphraseProperty and privPassphraseProperty can be null for NoAuthNoPriv security level String authPassphraseProperty = notification.DispatchProperties.get(SECURITY_AUTH_PASSPHRASE_PROPERTY); String privPassphraseProperty = notification.DispatchProperties.get(SECURITY_PRIV_PASSPHRASE_PROPERTY); OctetString authPassphrase = authPassphraseProperty != null ? new OctetString(authPassphraseProperty) : null; OctetString privPassphrase = privPassphraseProperty != null ? new OctetString(privPassphraseProperty) : null; UsmUser usmUser = new UsmUser(userName, AuthMD5.ID, authPassphrase, PrivDES.ID, privPassphrase); usm.addUser(userName, usmUser); SecurityModels.getInstance().addSecurityModel(usm); } UserTarget userTarget = new UserTarget(); userTarget.setSecurityName(userName); userTarget.setSecurityLevel(getSecurityLevel(notification.DispatchProperties).getSecurityLevel()); userTarget.setSecurityModel(SecurityModel.SECURITY_MODEL_USM); userTarget.setVersion(snmpVersion.getTargetVersion()); return userTarget; } } /** * {@inheritDoc} */ @Override public boolean isDigestSupported() { return false; } /** * Possible SNMP security levels */ protected enum TrapSecurity { /** * No password authentication and the communications between the agent and the server are not encrypted. */ NOAUTH_NOPRIV(SecurityLevel.NOAUTH_NOPRIV), /** * Password authentication is hash based and no encryption is used for communications between the hosts. */ AUTH_NOPRIV(SecurityLevel.AUTH_NOPRIV), /** * Password authentication is hash based and the communications between the agent and the server are also encrypted. */ AUTH_PRIV(SecurityLevel.AUTH_PRIV); int securityLevel; TrapSecurity(int securityLevel) { this.securityLevel = securityLevel; } public int getSecurityLevel() { return securityLevel; } } /** * Supported versions of SNMP */ protected enum SnmpVersion { SNMPv1(PDU.V1TRAP, SnmpConstants.version1, true), SNMPv2c(PDU.TRAP, SnmpConstants.version2c, true), SNMPv3(PDU.TRAP, SnmpConstants.version3, false); private int trapType; private int targetVersion; private boolean communityTargetRequired; SnmpVersion(int trapType, int targetVersion, boolean communityTargetRequired) { this.trapType = trapType; this.targetVersion = targetVersion; this.communityTargetRequired = communityTargetRequired; } public int getTrapType() { return trapType; } public int getTargetVersion() { return targetVersion; } public boolean isCommunityTargetRequired() { return communityTargetRequired; } } /** * Exception thrown when Notification configuration doesn't contain required properties. */ protected static class InvalidSnmpConfigurationException extends Exception { public InvalidSnmpConfigurationException(String message) { super(message); } } /** * Get list of recipients for notification * @param notification alerts notification * @return list of recipients * @throws InvalidSnmpConfigurationException if recipients is <code>null</code> or empty */ private List<Recipient> getNotificationRecipients(Notification notification) throws InvalidSnmpConfigurationException { if (notification.Recipients == null || notification.Recipients.isEmpty()) { throw new InvalidSnmpConfigurationException("Destination addresses should be set."); } return notification.Recipients; } /** * Get dispatch property with specific key from dispatch properties. * @param dispatchProperties dispatch properties * @param key property key * @return property value * @throws InvalidSnmpConfigurationException if property with such key does not exist */ protected static String getDispatchProperty(Map<String, String> dispatchProperties, String key) throws InvalidSnmpConfigurationException { if (dispatchProperties == null || !dispatchProperties.containsKey(key)) { throw new InvalidSnmpConfigurationException(String.format("Property \"%s\" should be set.", key)); } return dispatchProperties.get(key); } /** * Returns {@link SnmpVersion} instance corresponding to dispatch property <code>ambari.dispatch.snmp.version</code> from dispatch properties. * @param dispatchProperties dispatch properties * @return corresponding SnmpVersion instance * @throws InvalidSnmpConfigurationException if dispatch properties doesn't contain required property */ protected SnmpVersion getSnmpVersion(Map<String, String> dispatchProperties) throws InvalidSnmpConfigurationException { String snmpVersion = getDispatchProperty(dispatchProperties, SNMP_VERSION_PROPERTY); try { return SnmpVersion.valueOf(snmpVersion); } catch (IllegalArgumentException ex) { String errorMessage = String.format("Incorrect SNMP version - \"%s\". Possible values for \"%s\": %s", snmpVersion, SNMP_VERSION_PROPERTY, Arrays.toString(SnmpVersion.values())); throw new InvalidSnmpConfigurationException(errorMessage); } } /** * Returns {@link TrapSecurity} instance corresponding to dispatch property <code>ambari.dispatch.snmp.security.level</code> from dispatch properties. * @param dispatchProperties dispatch properties * @return corresponding TrapSecurity instance * @throws InvalidSnmpConfigurationException if dispatch properties doesn't contain required property */ protected TrapSecurity getSecurityLevel(Map<String, String> dispatchProperties) throws InvalidSnmpConfigurationException { String securityLevel = getDispatchProperty(dispatchProperties, SECURITY_LEVEL_PROPERTY); try { return TrapSecurity.valueOf(securityLevel); } catch (IllegalArgumentException ex) { String errorMessage = String.format("Incorrect security level for trap - \"%s\". Possible values for \"%s\": %s", securityLevel, SECURITY_LEVEL_PROPERTY, Arrays.toString(TrapSecurity.values())); throw new InvalidSnmpConfigurationException(errorMessage); } } private void failureCallback(Notification notification) { if (notification.Callback != null) { notification.Callback.onFailure(notification.CallbackIds); } } private void successCallback(Notification notification) { if (notification.Callback != null) { notification.Callback.onSuccess(notification.CallbackIds); } } public Integer getPort() { return port; } protected UdpTransportMapping getTransportMapping() { return transportMapping; } }