content: /*
   * 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;
    }
  }
  