WaarpSnmpAgent.java

/*
 * This file is part of Waarp Project (named also Waarp or GG).
 *
 *  Copyright (c) 2019, Waarp SAS, and individual contributors by the @author
 *  tags. See the COPYRIGHT.txt in the distribution for a full listing of
 * individual contributors.
 *
 *  All Waarp Project is free software: you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or (at your
 * option) any later version.
 *
 * Waarp is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along with
 * Waarp . If not, see <http://www.gnu.org/licenses/>.
 */
package org.waarp.snmp;

import org.snmp4j.TransportMapping;
import org.snmp4j.agent.BaseAgent;
import org.snmp4j.agent.CommandProcessor;
import org.snmp4j.agent.DuplicateRegistrationException;
import org.snmp4j.agent.MOGroup;
import org.snmp4j.agent.mo.snmp.RowStatus;
import org.snmp4j.agent.mo.snmp.SNMPv2MIB;
import org.snmp4j.agent.mo.snmp.SnmpCommunityMIB;
import org.snmp4j.agent.mo.snmp.SnmpCommunityMIB.SnmpCommunityEntryRow;
import org.snmp4j.agent.mo.snmp.SnmpNotificationMIB;
import org.snmp4j.agent.mo.snmp.SnmpTargetMIB;
import org.snmp4j.agent.mo.snmp.StorageType;
import org.snmp4j.agent.mo.snmp.VacmMIB;
import org.snmp4j.agent.security.MutableVACM;
import org.snmp4j.mp.MPv3;
import org.snmp4j.mp.MessageProcessingModel;
import org.snmp4j.mp.SnmpConstants;
import org.snmp4j.security.SecurityLevel;
import org.snmp4j.security.SecurityModel;
import org.snmp4j.security.USM;
import org.snmp4j.security.UsmUser;
import org.snmp4j.smi.Address;
import org.snmp4j.smi.GenericAddress;
import org.snmp4j.smi.Integer32;
import org.snmp4j.smi.OID;
import org.snmp4j.smi.OctetString;
import org.snmp4j.smi.Variable;
import org.snmp4j.smi.VariableBinding;
import org.snmp4j.transport.TransportMappings;
import org.snmp4j.util.ThreadPool;
import org.snmp4j.util.WorkerPool;
import org.waarp.common.logging.SysErrLogger;
import org.waarp.common.logging.WaarpLogger;
import org.waarp.common.logging.WaarpLoggerFactory;
import org.waarp.snmp.SnmpConfiguration.TargetElement;
import org.waarp.snmp.interf.WaarpInterfaceMib;
import org.waarp.snmp.interf.WaarpInterfaceMib.TrapLevel;
import org.waarp.snmp.interf.WaarpInterfaceMonitor;

import java.io.File;
import java.io.IOException;
import java.util.List;

/**
 * This Agent contains some functionalities for running a version 2c and 3 of
 * SNMP agent.
 */
public class WaarpSnmpAgent extends BaseAgent {
  /**
   * Internal Logger
   */
  private static final WaarpLogger logger =
      WaarpLoggerFactory.getLogger(WaarpSnmpAgent.class);

  private String[] address = { SnmpConfiguration.DEFAULTADDRESS };

  private final boolean isFilterAccessEnabled;

  private boolean useTrap = true;

  private int trapLevel;

  private final List<UsmUser> listUsmUser;

  private final List<TargetElement> listTargetElements;

  private final boolean hasV2;

  private final boolean hasV3;

  private final long systemTimeStart = System.currentTimeMillis();

  private final WorkerPool workerPool;
  /**
   * The associated monitor with this Agent
   */
  private WaarpInterfaceMonitor monitor;
  /**
   * The associated MIB with this Agent
   */
  private WaarpInterfaceMib mib;

  /**
   * @param configurationFile XML format
   * @param monitor the associated monitor
   * @param mib the associated MIB
   *
   * @throws IllegalArgumentException
   */
  public WaarpSnmpAgent(final File configurationFile,
                        final WaarpInterfaceMonitor monitor,
                        final WaarpInterfaceMib mib)
      throws IllegalArgumentException {
    /*
     * Creates a base agent with boot-counter, config file, and a CommandProcessor for processing SNMP requests.
     *
     * Parameters:
     *
     * These files does not exist and are not used but has to be specified. Read snmp4j docs for more info
     *
     * "bootCounterFile" - a file with serialized boot-counter information (read/write). If the file does not
     * exist it is created on shutdown of the agent.
     *
     * "configFile" - a file with serialized configuration information (read/write). If the file does not exist it
     * is created on shutdown of the agent.
     *
     * "commandProcessor" - the CommandProcessor instance that handles the SNMP requests.
     */
    super(new File(configurationFile.getParentFile(), "dummyConf.agent"),
          new File(configurationFile.getParentFile(), "dummyBootCounter.agent"),
          new CommandProcessor(new OctetString(MPv3.createLocalEngineID())));
    if (!SnmpConfiguration.setConfigurationFromXml(configurationFile)) {
      throw new IllegalArgumentException("Cannot load configuration");
    }
    address = SnmpConfiguration.address;
    final int nbThread = SnmpConfiguration.nbThread;
    isFilterAccessEnabled = SnmpConfiguration.isFilterAccessEnabled;
    useTrap = SnmpConfiguration.isUsingTrap;
    setTrapLevel(SnmpConfiguration.trapLevel);
    listUsmUser = SnmpConfiguration.listUsmUser;
    listTargetElements = SnmpConfiguration.listTargetElements;
    hasV2 = SnmpConfiguration.hasV2;
    hasV3 = SnmpConfiguration.hasV3;

    logger.debug("SNMP Configuration loaded: {}:{}", address[0], nbThread);
    workerPool = ThreadPool.create("SnmpRequestPool", nbThread);
    agent.setWorkerPool(workerPool);
    setMonitor(monitor);
    getMonitor().setAgent(this);
    setMib(mib);
    getMib().setAgent(this);
  }

  /**
   * @return the monitor
   */
  public final WaarpInterfaceMonitor getMonitor() {
    return monitor;
  }

  /**
   * @return the mib
   */
  public final WaarpInterfaceMib getMib() {
    return mib;
  }

  /**
   * @return the uptime in ms
   */
  public final long getUptime() {
    return getSnmpv2MIB().getUpTime().toMilliseconds();
  }

  /**
   * @return the uptime but in System time in ms
   */
  public final long getUptimeSystemTime() {
    return systemTimeStart;
  }

  /**
   * Register additional managed objects at the agent's server.
   */
  @Override
  protected final void registerManagedObjects() {
    logger.debug("Registers");
    try {
      getMib().registerMOs(server, null);
    } catch (final DuplicateRegistrationException e) {
      logger.error("Cannot register Mib", e);
    }
  }

  /**
   * Unregister the basic MIB modules from the agent's MOServer.
   */
  @Override
  protected final void unregisterManagedObjects() {
    logger.debug("Unregisters");
    getMib().unregisterMOs(server, null);
  }

  /**
   * @param moGroup
   */
  public final void unregisterManagedObject(final MOGroup moGroup) {
    logger.debug("Unregister {}", moGroup);
    moGroup.unregisterMOs(server, getContext(moGroup));
  }

  /**
   * Adds all the necessary initial users to the USM. Only applicable to SNMP
   * V3
   */
  @Override
  protected final void addUsmUser(final USM usm) {
    for (final UsmUser userlist : listUsmUser) {
      logger.debug("User: {}", userlist);
      usm.addUser(userlist.getSecurityName(), usm.getLocalEngineID(), userlist);
    }
    final UsmUser usernotify =
        new UsmUser(new OctetString(SnmpConfiguration.V3NOTIFY), null, null,
                    null, null);
    usm.addUser(usernotify.getSecurityName(), null, usernotify);
  }

  /**
   * Adds initial notification targets and filters.
   */
  @Override
  protected final void addNotificationTargets(final SnmpTargetMIB targetMIB,
                                              final SnmpNotificationMIB notificationMIB) {
    targetMIB.addDefaultTDomains();

    for (final TargetElement element : listTargetElements) {
      logger.debug("AddTarget: {}", element);
      targetMIB.addTargetAddress(element.name, element.transportDomain,
                                 element.address, element.timeout,
                                 element.retries, element.tagList,
                                 element.params, element.storageType);
    }
    /*
     * Example
     *
     * targetMIB.addTargetAddress(new OctetString("notificationV2c"), TransportDomains.transportDomainUdpIpv4, new
     * OctetString(new UdpAddress("127.0.0.1/162").getValue()), 200, 1, new OctetString("notify"), new
     * OctetString("v2c"), StorageType.permanent); targetMIB.addTargetAddress(new OctetString("notificationV3"),
     * TransportDomains.transportDomainUdpIpv4, new OctetString(new UdpAddress("127.0.0.1/1162").getValue()), 200,
     * 1, new OctetString("notify"), new OctetString("v3notify"), StorageType.permanent);
     */
    logger.debug("HasV2: {} HasV3: {}", hasV2, hasV3);
    if (hasV2) {
      targetMIB.addTargetParams(new OctetString(SnmpConfiguration.V2C),
                                MessageProcessingModel.MPv2c,
                                SecurityModel.SECURITY_MODEL_SNMPv2c,
                                new OctetString("cpublic"),
                                SecurityLevel.AUTH_PRIV, StorageType.permanent);
    }
    if (hasV3) {
      targetMIB.addTargetParams(new OctetString(SnmpConfiguration.V3NOTIFY),
                                MessageProcessingModel.MPv3,
                                SecurityModel.SECURITY_MODEL_USM,
                                new OctetString("v3notify"),
                                SecurityLevel.NOAUTH_NOPRIV,
                                StorageType.permanent);
    }
    int trapOrInform = SnmpNotificationMIB.SnmpNotifyTypeEnum.inform;
    if (useTrap) {
      trapOrInform = SnmpNotificationMIB.SnmpNotifyTypeEnum.trap;
    }
    notificationMIB.addNotifyEntry(new OctetString("default"),
                                   new OctetString(SnmpConfiguration.NOTIFY),
                                   trapOrInform, StorageType.permanent);
  }

  /**
   * Minimal View based Access Control
   * <p>
   * http://www.faqs.org/rfcs/rfc2575.html
   */
  @Override
  protected final void addViews(final VacmMIB vacm) {
    vacm.addGroup(SecurityModel.SECURITY_MODEL_SNMPv1,
                  new OctetString("cpublic"), new OctetString("v1v2group"),
                  StorageType.nonVolatile);
    vacm.addGroup(SecurityModel.SECURITY_MODEL_SNMPv2c,
                  new OctetString("cpublic"), new OctetString("v1v2group"),
                  StorageType.nonVolatile);
    vacm.addGroup(SecurityModel.SECURITY_MODEL_USM, new OctetString("v3notify"),
                  new OctetString("v3group"), StorageType.nonVolatile);

    for (final UsmUser user : listUsmUser) {
      logger.debug("Groups: {} Restricted? {}", user.getSecurityName(),
                   user.getPrivacyProtocol() == null);
      if (user.getPrivacyProtocol() == null) {
        vacm.addGroup(SecurityModel.SECURITY_MODEL_USM,
                      new OctetString(user.getSecurityName()),
                      new OctetString("v3restricted"), StorageType.nonVolatile);
      } else {
        vacm.addGroup(SecurityModel.SECURITY_MODEL_USM,
                      new OctetString(user.getSecurityName()),
                      new OctetString("v3group"), StorageType.nonVolatile);
      }
    }

    vacm.addAccess(new OctetString("v1v2group"), new OctetString("public"),
                   SecurityModel.SECURITY_MODEL_ANY,
                   SecurityLevel.NOAUTH_NOPRIV, MutableVACM.VACM_MATCH_EXACT,
                   new OctetString("fullReadView"),
                   new OctetString("fullWriteView"),
                   new OctetString("fullNotifyView"), StorageType.nonVolatile);
    vacm.addAccess(new OctetString("v3group"), new OctetString(),
                   SecurityModel.SECURITY_MODEL_USM, SecurityLevel.AUTH_PRIV,
                   MutableVACM.VACM_MATCH_EXACT,
                   new OctetString("fullReadView"),
                   new OctetString("fullWriteView"),
                   new OctetString("fullNotifyView"), StorageType.nonVolatile);
    vacm.addAccess(new OctetString("v3restricted"), new OctetString(),
                   SecurityModel.SECURITY_MODEL_USM,
                   SecurityLevel.NOAUTH_NOPRIV, MutableVACM.VACM_MATCH_EXACT,
                   new OctetString("restrictedReadView"),
                   new OctetString("restrictedWriteView"),
                   new OctetString("restrictedNotifyView"),
                   StorageType.nonVolatile);

    vacm.addViewTreeFamily(new OctetString("fullReadView"), new OID("1.3"),
                           new OctetString(), VacmMIB.vacmViewIncluded,
                           StorageType.nonVolatile);
    vacm.addViewTreeFamily(new OctetString("fullWriteView"), new OID("1.3"),
                           new OctetString(), VacmMIB.vacmViewIncluded,
                           StorageType.nonVolatile);
    vacm.addViewTreeFamily(new OctetString("fullNotifyView"), new OID("1.3"),
                           new OctetString(), VacmMIB.vacmViewIncluded,
                           StorageType.nonVolatile);

    vacm.addViewTreeFamily(new OctetString("restrictedReadView"),
                           new OID("1.3.6.1.2"), new OctetString(),
                           VacmMIB.vacmViewIncluded, StorageType.nonVolatile);
    vacm.addViewTreeFamily(new OctetString("restrictedWriteView"),
                           new OID("1.3.6.1.2.1"), new OctetString(),
                           VacmMIB.vacmViewIncluded, StorageType.nonVolatile);
    vacm.addViewTreeFamily(new OctetString("restrictedNotifyView"),
                           new OID("1.3.6.1.2"), new OctetString(),
                           VacmMIB.vacmViewIncluded, StorageType.nonVolatile);
    vacm.addViewTreeFamily(new OctetString("restrictedNotifyView"),
                           new OID("1.3.6.1.6.3.1"), new OctetString(),
                           VacmMIB.vacmViewIncluded, StorageType.nonVolatile);
  }

  /**
   * The table of community strings configured in the SNMP engine's Local
   * Configuration Datastore (LCD).
   * <p>
   * We only configure one, "public".
   */
  @Override
  protected final void addCommunities(final SnmpCommunityMIB communityMIB) {
    final Variable[] com2sec = {
        new OctetString("public"), // community name
        new OctetString("cpublic"), // security name
        getAgent().getContextEngineID(), // local engine ID
        new OctetString("public"), // default context name
        new OctetString(), // transport tag
        new Integer32(StorageType.nonVolatile), // storage type
        new Integer32(RowStatus.active) // row status
    };
    final SnmpCommunityEntryRow row = communityMIB.getSnmpCommunityEntry()
                                                  .createRow(new OctetString(
                                                      "public2public").toSubIndex(
                                                      true), com2sec);
    communityMIB.getSnmpCommunityEntry().addRow(row);
    if (isFilterAccessEnabled) {
      snmpCommunityMIB.setSourceAddressFiltering(true);
    }
  }

  @Override
  protected final void initTransportMappings() throws IOException {
    final TransportMapping<?>[] testMappings =
        new TransportMapping[address.length];
    int nb = 0;
    for (final String addres : address) {
      final Address addr = GenericAddress.parse(addres);
      if (addr != null) {
        logger.info("SNMP Agent InitTransport: {} {}",
                    addr.getClass().getSimpleName(), addr);
        final TransportMapping<?> tm;
        try {
          tm = TransportMappings.getInstance().createTransportMapping(addr);
        } catch (final RuntimeException e) {
          continue;
        }
        if (tm != null) {
          testMappings[nb] = tm;
          nb++;
        }
      }
    }
    if (nb > 0) {
      transportMappings = new TransportMapping<?>[nb];
      System.arraycopy(testMappings, 0, transportMappings, 0, nb);
    } else {
      transportMappings = null;
      throw new IOException("No address to connect");
    }
  }

  /**
   * Start method invokes some initialization methods needed to start the
   * agent
   *
   * @throws IOException
   */
  public final void start() throws IOException {
    logger.debug("WaarpSnmpAgent starting: {} 1 on {}", address[0],
                 address.length);
    try {
      init();
    } catch (final IOException e) {
      logger.warn("Error while SNMP starts " + " : {}", e.getMessage());
      throw e;
    }
    addShutdownHook();
    getServer().addContext(new OctetString("public"));
    finishInit();
    run();
    if (TrapLevel.StartStop.isLevelValid(getTrapLevel())) {
      sendColdStartNotification();
    }
  }

  @Override
  protected final void sendColdStartNotification() {
    logger.debug("ColdStartNotification: {}",
                 getMib().getBaseOidStartOrShutdown());
    final SNMPv2MIB snmpv2 = getMib().getSNMPv2MIB();
    notificationOriginator.notify(new OctetString("public"),
                                  SnmpConstants.coldStart,
                                  new VariableBinding[] {
                                      new VariableBinding(
                                          getMib().getBaseOidStartOrShutdown(),
                                          new OctetString("Startup Service")),
                                      new VariableBinding(
                                          SnmpConstants.sysDescr,
                                          snmpv2.getDescr()),
                                      new VariableBinding(
                                          SnmpConstants.sysObjectID,
                                          snmpv2.getObjectID()),
                                      new VariableBinding(
                                          SnmpConstants.sysContact,
                                          snmpv2.getContact()),
                                      new VariableBinding(SnmpConstants.sysName,
                                                          snmpv2.getName()),
                                      new VariableBinding(
                                          SnmpConstants.sysLocation,
                                          snmpv2.getLocation())
                                  });
  }

  /**
   * Send a Notification just before Shutdown the SNMP service.
   */
  protected final void sendShutdownNotification() {
    if (getMib() == null || notificationOriginator == null) {
      return;
    }
    final SNMPv2MIB snmpv2 = getMib().getSNMPv2MIB();
    notificationOriginator.notify(new OctetString("public"),
                                  SnmpConstants.linkDown,
                                  new VariableBinding[] {
                                      new VariableBinding(
                                          getMib().getBaseOidStartOrShutdown(),
                                          new OctetString("Shutdown Service")),
                                      new VariableBinding(
                                          SnmpConstants.sysDescr,
                                          snmpv2.getDescr()),
                                      new VariableBinding(
                                          SnmpConstants.sysObjectID,
                                          snmpv2.getObjectID()),
                                      new VariableBinding(
                                          SnmpConstants.sysContact,
                                          snmpv2.getContact()),
                                      new VariableBinding(SnmpConstants.sysName,
                                                          snmpv2.getName()),
                                      new VariableBinding(
                                          SnmpConstants.sysLocation,
                                          snmpv2.getLocation())
                                  });
    try {
      Thread.sleep(100);
    } catch (final InterruptedException e) {//NOSONAR
      SysErrLogger.FAKE_LOGGER.ignoreLog(e);
    }
  }

  @Override
  public final void stop() {
    logger.info("Stopping SNMP support");
    if (TrapLevel.StartStop.isLevelValid(getTrapLevel())) {
      sendShutdownNotification();
    }
    if (session != null) {
      super.stop();
    }
    if (getMonitor() != null) {
      getMonitor().releaseResources();
      try {
        Thread.sleep(100);
      } catch (final InterruptedException e) {//NOSONAR
        SysErrLogger.FAKE_LOGGER.ignoreLog(e);
      }
      if (workerPool != null) {
        workerPool.cancel();
      }
    }
  }

  /**
   * @return the trapLevel
   */
  public final int getTrapLevel() {
    return trapLevel;
  }

  /**
   * @param trapLevel the trapLevel to set
   */
  public final void setTrapLevel(final int trapLevel) {
    this.trapLevel = trapLevel;
  }

  /**
   * @param monitor the monitor to set
   */
  private void setMonitor(final WaarpInterfaceMonitor monitor) {
    this.monitor = monitor;
  }

  /**
   * @param mib the mib to set
   */
  private void setMib(final WaarpInterfaceMib mib) {
    this.mib = mib;
  }
}