/*_############################################################################
  _## 
  _##  SNMP4J-AgentX - TestMasterAgent.java  
  _## 
  _##  Copyright (C) 2005-2026  Frank Fock (SNMP4J.org)
  _##  
  _##  This program is free software; you can redistribute it and/or modify
  _##  it under the terms of the GNU General Public License version 2 as 
  _##  published by the Free Software Foundation.
  _##
  _##  This program 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 this program; if not, write to the Free Software
  _##  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 
  _##  MA  02110-1301  USA
  _##  
  _##########################################################################*/

package org.snmp4j.agent.agentx.master.test;

import java.io.*;

import jetbrains.exodus.env.Environments;
import org.snmp4j.Session;
import org.snmp4j.agent.*;
import org.snmp4j.agent.agentx.master.AgentXMasterAgent;
import org.snmp4j.agent.audit.*;
import org.snmp4j.agent.db.MOXodusPersistence;
import org.snmp4j.agent.db.MOXodusPersistenceProvider;
import org.snmp4j.agent.db.sample.SampleAgent;
import org.snmp4j.agent.io.ImportMode;
import org.snmp4j.agent.io.MOInput;
import org.snmp4j.agent.io.MOInputFactory;
import org.snmp4j.agent.io.prop.PropertyMOInput;
import org.snmp4j.agent.io.prop.PropertyMOTreeBuilder;
import org.snmp4j.agent.mo.snmp.dh.DHKickstartParameters;
import org.snmp4j.agent.mo.snmp.dh.DHKickstartParametersImpl;
import org.snmp4j.agent.util.MarkdownMIBTreeFormatter;
import org.snmp4j.log.*;
import org.snmp4j.mp.MessageProcessingModel;
import org.snmp4j.security.AuthSHA;
import org.snmp4j.security.UsmUser;
import org.snmp4j.security.SecurityLevel;
import org.snmp4j.agent.mo.snmp.SnmpNotificationMIB;
import org.snmp4j.smi.*;
import org.snmp4j.agent.mo.snmp.StorageType;
import org.snmp4j.agent.mo.snmp.SnmpTargetMIB;
import org.snmp4j.security.SecurityModel;
import org.snmp4j.security.PrivDES;
import org.snmp4j.security.USM;
import org.snmp4j.agent.mo.snmp.VacmMIB;
import org.snmp4j.agent.mo.snmp.TransportDomains;
import org.snmp4j.TransportMapping;
import org.snmp4j.transport.DefaultTcpTransportMapping;
import org.snmp4j.agent.mo.snmp.RowStatus;
import org.snmp4j.agent.mo.snmp.SnmpCommunityMIB;

import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.NoSuchAlgorithmException;
import java.util.*;

import org.snmp4j.mp.MPv3;
import org.snmp4j.MessageDispatcher;
import org.snmp4j.transport.unix.UnixDomainAddress;
import org.snmp4j.transport.unix.UnixSocketStreamTransportMapping;
import org.snmp4j.util.SnmpConfigurator;
import org.snmp4j.util.ThreadPool;

import org.snmp4j.MessageDispatcherImpl;

import org.snmp4j.transport.TransportMappings;
import org.snmp4j.util.ArgumentParser;

import javax.crypto.Cipher;

/**
 * Sample master agent with programmatically applied security configuration. To create your own master agent,
 * create a similar class and implement your own MIB initialization code either programmatically or by using
 * a {@link org.snmp4j.agent.io.MOInputFactory} which uses a {@link org.snmp4j.agent.io.prop.PropertyMOInput}
 * to read configuration from a properties file (see the {@code TestSubagentConfig.properties} file of the
 * {@link org.snmp4j.agent.example} package.
 *
 * @author Frank Fock
 * @version 3.0.0
 */
public class TestMasterAgent {

    // initialize logging
    static {
        LogFactory.setLogFactory(new ConsoleLogFactory());
        LogFactory.getLogFactory().getRootLogger().setLogLevel(LogLevel.ALL);
    }

    /**
     * The logger used by this master agent to report status and error messages.
     */
    public static LogAdapter logger = LogFactory.getLogger(TestMasterAgent.class);

    /**
     * The {@link ArgumentParser} format string describing the supported command line options.
     */
    public static final String COMMAND_LINE_OPTIONS = "-c[s{=SampleAgent.cfg}] +dhks[s] +u[s] " +
            "+tls-trust-ca[s] +tls-peer-id[s] +tls-local-id[s] +tls-version[s{=TLSv1}<(TLSv1|TLSv1.1|TLSv1.2|TLSv1.3)>] +dtls-version[s{=TLSv1.2}<(TLSv1.0|TLSv1.2)>]" +
            "+Djavax.net.ssl.keyStore +Djavax.net.ssl.keyStorePassword " +
            "+Djavax.net.ssl.trustStore +Djavax.net.ssl.trustStorePassword " +
            "+ts[s] +cfg[s] +write-init-config[s] +viewer[s] +admin[s] +x " +
            "-X[s{=tcp:0.0.0.0/705}<(unix|tcp):.*[/[0-9]+]?>] +h +v";
    /**
     * The {@link ArgumentParser} format string describing the required command line parameters.
     */
    public static final String COMMAND_LINE_PARAMS = "#address[s<(udp|tcp|tls|dtls):.*[/[0-9]+]?>] ..";

    static final AgentAuditPolicy SAMPLE_MASTER_AGENT_POLICY = new DefaultAgentAuditPolicy(
            new AgentPolicyProfile(
                    AgentPolicyProfile.ManagedObjectAccess.UNRESTRICTED,
                    AgentPolicyProfile.NetworkAccess.SITE_LOCAL_NETWORK,
                    AgentPolicyProfile.FileAccess.LIMITED_DIRECTORY_ACCESS,
                    SecurityLevel.authNoPriv

            ), AgentAuditLevel.RESTRICT);


    /**
     * The listen address of the agent.
     */
    protected String address;
    /**
     * The {@link AgentXMasterAgent} instance managed and run by this test agent.
     */
    protected AgentXMasterAgent agent;
    /**
     * The {@link MOServer} that holds the managed objects served by the agent.
     */
    protected MOServer server;
    private final MOXodusPersistence moXodusPersistence;
    private final String configFile;

    // Optionally provided named secrets (pass keys) that are injected into the PropertyMOInput as
    // predefined random values "viewer" and "admin" referenced by the configuration properties file.
    private String viewerPasskey;
    private String adminPasskey;

    // Optional file name (+write-init-config): if set, a human-readable Markdown overview of the
    // initial MIB configuration is written to this file when the configuration properties are loaded.
    private final String initConfigOverviewFile;

    /**
     * The table size limits applied to the agent's tables, keyed by table OID.
     */
    protected Properties tableSizeLimits;
    /**
     * The audit policy to be used for the agent.
     */
    private final AgentAuditPolicy auditPolicy;

    /**
     * Creates a new {@code TestMasterAgent} from the parsed command line arguments and the given audit policy.
     * The constructor sets up the {@link MOServer}, the persistence storage, the initial configuration factory,
     * the table size limits, and the underlying {@link AgentXMasterAgent}, and adds the configured listen addresses.
     *
     * @param args
     *    the parsed command line arguments as returned by the {@link ArgumentParser}.
     * @param auditPolicy
     *    the {@link AgentAuditPolicy} used to control managed object, network, and file access.
     */
    public TestMasterAgent(Map<String, List<Object>> args, AgentAuditPolicy auditPolicy) {
        this.auditPolicy = auditPolicy;
        try {
            logger.info("Max supported AES key length is " + Cipher.getMaxAllowedKeyLength("AES"));
        } catch (NoSuchAlgorithmException e) {
            logger.error("AES privacy not supported by this VM: ", e);
        }

         /*
           Activate deadlock prevention. This requires few more CPU time and memory consumption during SET request
           and {@link DefaultMOServer#lookup(MOQuery, LockRequest, MOServerLookupEvent, Class)} requests for update
           intention but ensures that SNMP requests and internal updates do not enter a deadlock state (which will
           time out of course but result in a failed request with genErr) when the OIDs locked by a transaction
           are not sorted by their lexicographic order.
         */
        server = new DefaultMOServer().setDeadlockPreventionEnabled(true);
        MOServer[] moServers = new MOServer[]{server};
        String configFilename = null;
        if (args.containsKey("cfg")) {
            configFilename = (String) ArgumentParser.getValue(args, "cfg", 0);
        }
        configFile = (String) (args.get("c")).get(0);
        moXodusPersistence = new MOXodusPersistence(moServers, Environments.newInstance(configFile));

        viewerPasskey = (String) ArgumentParser.getFirstValue(args.get("viewer"));
        adminPasskey = (String) ArgumentParser.getFirstValue(args.get("admin"));

        if (viewerPasskey == null) {
            viewerPasskey = System.getenv("SNMP4J_AGENTX_TEST_VIEWER_PASSKEY");
        }
        if (adminPasskey == null) {
            adminPasskey = System.getenv("SNMP4J_AGENTX_TEST_ADMIN_PASSKEY");
        }

        List<?> tlsVersions = args.get("tls-version");
        if (tlsVersions != null && (!tlsVersions.isEmpty())) {
            System.setProperty(SnmpConfigurator.P_TLS_VERSION, (String) tlsVersions.get(0));
        }

        MOInputFactory configurationFactory = null;
        // load initial configuration from properties file only if there is no persistent data for the default context:
        if (!moXodusPersistence.isContextLoadable(null)) {
            configurationFactory = createMOInputFactory(configFilename, ImportMode.restoreChanges);
        }
        this.tableSizeLimits = getTableSizeLimitsProperties(args, auditPolicy);
        String dhKickstartInfoPath = (String) ArgumentParser.getFirstValue(args.get("dhks"));
        setupAgent(args, moServers, configurationFactory, args.get("address"), dhKickstartInfoPath, auditPolicy);
        initConfigOverviewFile = (String) ArgumentParser.getFirstValue(args.get("write-init-config"));
        // Register Unix Domain Transport Mapping
        TransportMappings.getInstance().registerTransportMapping(UnixSocketStreamTransportMapping.class,
                UnixDomainAddress.class);

        OctetString defaultEngineID = new OctetString(MPv3.createLocalEngineID());
        MessageDispatcher messageDispatcher = new MessageDispatcherImpl();
        addListenAddresses(messageDispatcher, args.get("address"));
    }

    /**
     * Sets up the {@link AgentConfigManager} including its message dispatcher, listen addresses,
     * optional Diffie-Hellman kickstart parameters, and persistence provider.
     *
     * @param args                 the parsed command line arguments as returned by the {@link ArgumentParser}.
     * @param moServers            the managed object servers the agent registers its managed objects with.
     * @param configurationFactory the {@link MOInputFactory} providing the initial configuration, or {@code null} if none.
     * @param listenAddress        the list of transport addresses the agent should listen on.
     * @param dhKickstartInfoPath  the path to the Diffie-Hellman kickstart parameters file, or {@code null} if none.
     * @param auditPolicy          the {@link AgentAuditPolicy} to use for the agent.
     */
    protected void setupAgent(Map<String, List<Object>> args,
                              MOServer[] moServers, MOInputFactory configurationFactory,
                              List<Object> listenAddress,
                              String dhKickstartInfoPath, AgentAuditPolicy auditPolicy) {
        MessageDispatcher messageDispatcher = new MessageDispatcherImpl();
        addListenAddresses(messageDispatcher, listenAddress);
        Collection<DHKickstartParameters> dhKickstartParameters = Collections.emptyList();
        if (dhKickstartInfoPath != null) {
            if (auditPolicy == null || auditPolicy.fileAudit(
                    new FileAuditEvent(this, FileAuditEvent.FileOperation.READ, dhKickstartInfoPath))) {
                File dhKickstartInfoFile = new File(dhKickstartInfoPath);
                if (dhKickstartInfoFile.canRead()) {
                    try {
                        Properties kickstartProperties = new Properties();
                        FileInputStream fileInputStream = new FileInputStream(dhKickstartInfoFile);
                        kickstartProperties.load(fileInputStream);
                        fileInputStream.close();
                        dhKickstartParameters =
                                DHKickstartParametersImpl.readFromProperties("org.snmp4j.", kickstartProperties);
                    } catch (IOException iox) {
                        logger.error("Failed to load Diffie Hellman kickstart parameters from '" +
                                dhKickstartInfoPath + "': " + iox.getMessage(), iox);
                    }
                } else {
                    logger.warn("Diffie Hellman kickstart parameters file cannot be read: " + dhKickstartInfoFile);
                }
            }
        }
        MOXodusPersistenceProvider moXodusPersistenceProvider =
                new MOXodusPersistenceProvider(moXodusPersistence, auditPolicy);
        OctetString defaultEngineID = new OctetString(MPv3.createLocalEngineID());
        OctetString engineID = moXodusPersistenceProvider.getEngineId(defaultEngineID);
        SnmpConfigurator snmpConfigurator = new SnmpConfigurator(true);
        agent = new AgentXMasterAgent(
                engineID,
                messageDispatcher,
                null,
                moServers,
                ThreadPool.create("SampleAgent", 3),
                configurationFactory,
                moXodusPersistenceProvider, moXodusPersistenceProvider, null, dhKickstartParameters,
                auditPolicy) {

            @Override
            protected Session createSnmpSession(MessageDispatcher dispatcher) {
                Session session = super.createSnmpSession(dispatcher);
                snmpConfigurator.configure(session, getUsm(), messageDispatcher, args);
                return session;
            }
        };
        /* Programmtic intialization without properties file:
        agent = new AgentXMasterAgent(
                moXodusPersistenceProvider.getEngineId(defaultEngineID),
                messageDispatcher,
                null,
                moServers,
                ThreadPool.create("SampleAgentXMaster", 3),
                null, // by config file: configurationFactory,
                moXodusPersistenceProvider, moXodusPersistenceProvider,
                null, null, SAMPLE_MASTER_AGENT_POLICY) {
            public void configure() {
                // Check if persistent data is available or not
                if (!moXodusPersistence.isContextLoadable(null)) {
                    moXodusPersistence.setIgnoreChangeListenerEvents(true);
                    super.configure();
                    // Here configuration is added programmatically (alternatively
                    // configuration can be done by properties as commented out above).
                    addCommunities(communityMIB);
                    addUsmUser(usm);
                    addNotificationTargets(targetMIB, notificationMIB);
                    addViews(vacmMIB);
                    moXodusPersistence.save();
                    // Implicitly done already done by above save operation:
                    // moXodusPersistence.setIgnoreChangeListenerEvents(false);
                }
            }
        };
        */
        agent.addAgentStateListener((agentConfigManager, agentState) -> {
            switch (agentState.getState()) {
                case AgentState.STATE_INITIALIZED -> moXodusPersistence.registerChangeListenersWithServer(server);
                case AgentState.STATE_SHUTDOWN -> moXodusPersistence.unregisterChangeListenersWithServer(server);
            }
        });
    }

    /**
     * Read the content of the {@code SampleAgentTableSizeLimits.properties} file.
     *
     * @param args        the command line arguments. If the arguements contain the key "ts" the default file name is replaced by the
     *                    key's value.
     * @param auditPolicy the audit policy to use or {@code null} if no audit policy is to be used.
     * @return a Properties instance with the table size properties read from the given file or an empty Properties instance
     * if the file could not be read.
     */
    protected static Properties getTableSizeLimitsProperties(Map<String, List<Object>> args, AgentAuditPolicy auditPolicy) {
        String configFilename = (String) ArgumentParser.getValue(args, "ts", 0);
        if (auditPolicy == null || configFilename == null || auditPolicy.fileAudit(
                new FileAuditEvent(SampleAgent.class, FileAuditEvent.FileOperation.READ, configFilename))) {
            InputStream tableSizeLimitsInputStream =
                    TestMasterAgent.class.getResourceAsStream("SampleAgentTableSizeLimits.properties");
            if (configFilename != null) {
                try {
                    tableSizeLimitsInputStream = new FileInputStream(configFilename);
                } catch (FileNotFoundException fileNotFoundException) {
                    logger.warn("Could not read table size limits file '" + configFilename + "':" + fileNotFoundException);
                }
            }
            Properties tableSizeLimits = new Properties();
            try {
                tableSizeLimits.load(tableSizeLimitsInputStream);
            } catch (IOException ex) {
                logger.warn("Could not load table size limits file '" + configFilename + "':" + ex);
            }
            return tableSizeLimits;
        }
        return new Properties();
    }

    /**
     * Creates a {@link MOInputFactory} that supplies the initial agent configuration from a properties file.
     *
     * @param configFilename the path to the configuration properties file or {@code null} to use the bundled
     *                       {@code SampleAgentConfig.properties} resource.
     * @param importMode     the {@link ImportMode} that controls how the configuration is applied to existing data.
     * @return a {@link MOInputFactory} providing the parsed configuration.
     */
    protected MOInputFactory createMOInputFactory(String configFilename, ImportMode importMode) {
        MOInputFactory configurationFactory;
        InputStream configInputStream =
                SampleAgent.class.getResourceAsStream("SampleAgentConfig.properties");
        final Properties props = new Properties();
        if (configFilename != null) {
            try {
                if (agent.getAuditPolicy().fileAudit(new FileAuditEvent(this,
                        FileAuditEvent.FileOperation.READ, configFile))) {
                    configInputStream = new FileInputStream(configFilename);
                } else {
                    String error = "Config file '" + configFilename + "' not audited: " +
                            "audit policy does not allow reading of file.";
                    logger.error(error);
                    throw new RuntimeException(error);
                }
            } catch (FileNotFoundException ex1) {
                logger.error("Config file '" + configFilename + "' not found: " + ex1.getMessage(), ex1);
                throw new RuntimeException(ex1);
            }
        }
        try {
            props.load(configInputStream);
        } catch (IOException ex) {
            logger.error("Failed to load config file '" + configFilename + "': " + ex.getMessage(), ex);
        }
        if (initConfigOverviewFile != null) {
            writeInitialConfigOverview(props, initConfigOverviewFile);
        }
        configurationFactory = () -> {
            PropertyMOInput propertyMOInput = new PropertyMOInput(props, this.agent, importMode);
            // Inject the optionally provided named secrets (pass keys) so that they are used instead of
            // randomly generated values wherever the configuration references {r}viewer/{r}admin (or
            // {R}viewer/{R}admin). This has to happen before the configuration is read.
            if (viewerPasskey != null) {
                propertyMOInput.setRandomValue("viewer", new OctetString(viewerPasskey));
            }
            if (adminPasskey != null) {
                propertyMOInput.setRandomValue("admin", new OctetString(adminPasskey));
            }
            return propertyMOInput;
        };
        return configurationFactory;
    }

    /**
     * Writes a human-readable Markdown ({@code .md}) overview of the initial MIB configuration defined by
     * the supplied properties (see {@code SampleAgentConfig.properties}). The overview is produced by
     * building a {@link org.snmp4j.agent.io.prop.MIBTree} with a
     * {@link org.snmp4j.agent.io.prop.PropertyMOTreeBuilder} and rendering it with a
     * {@link MarkdownMIBTreeFormatter}. Any {@code -viewer}/{@code -admin} pass keys are injected so the
     * overview reflects the same secrets the agent would use; otherwise references to unresolved runtime
     * instance values are shown with their configured reference.
     *
     * @param props    the initial configuration properties in {@code MOInput} format.
     * @param fileName the target file name for the generated Markdown overview.
     */
    protected void writeInitialConfigOverview(Properties props, String fileName) {
        if (auditPolicy != null &&
                auditPolicy.fileAudit(new FileAuditEvent(this, FileAuditEvent.FileOperation.WRITE, fileName))) {
            try {
                PropertyMOTreeBuilder builder = new PropertyMOTreeBuilder(props);
                if (viewerPasskey != null) {
                    builder.setRandomValue("viewer", new OctetString(viewerPasskey));
                }
                if (adminPasskey != null) {
                    builder.setRandomValue("admin", new OctetString(adminPasskey));
                }
                String markdown = new MarkdownMIBTreeFormatter().format(builder.build());
                try (Writer writer =
                             new OutputStreamWriter(Files.newOutputStream(Paths.get(fileName)), StandardCharsets.UTF_8)) {
                    writer.write(markdown);
                }
                logger.info("Wrote initial MIB configuration overview to '" + fileName + "'");
            } catch (IOException | RuntimeException ex) {
                logger.error("Failed to write initial MIB configuration overview to '" + fileName + "': " +
                        ex.getMessage(), ex);
            }
        } else {
            logger.info("Skipping writing initial MIB configuration overview to '" + fileName + "' due to audit policy");
        }
    }

    /**
     * Adds the given transport addresses as listen addresses to the supplied message dispatcher by
     * creating and starting a matching {@link TransportMapping} for each address.
     *
     * @param md
     *    the {@link MessageDispatcher} the created transport mappings are added to.
     * @param addresses
     *    the list of transport addresses to listen on; {@code null} entries are ignored.
     */
    protected void addListenAddresses(MessageDispatcher md, List<Object> addresses) {
        for (Object addr : addresses) {
            if (addr == null) {
                continue;
            }
            Address address = GenericAddress.parse(addr.toString());
            TransportMapping<? extends Address> tm =
                    TransportMappings.getInstance().createTransportMapping(address);
            if (tm != null) {
                try {
                    tm.listen();
                    md.addTransportMapping(tm);
                } catch (IOException e) {
                    logger.error("IO exception while listening on address '" +
                            address + "', transport mapping disabled.");
                }
            } else {
                logger.warn("No transport mapping available for address '" +
                        address + "'.");
            }
        }
    }

    /*
    protected void addNotificationTargets(SnmpTargetMIB targetMIB,
                                          SnmpNotificationMIB notificationMIB) {
        targetMIB.addDefaultTDomains();

        targetMIB.addTargetAddress(new OctetString("notification"),
                TransportDomains.transportDomainUdpIpv4,
                new OctetString(new UdpAddress("127.0.0.1/162").getValue()),
                200, 1,
                new OctetString("notify"),
                new OctetString("v2c"),
                StorageType.permanent);
        targetMIB.addTargetParams(new OctetString("v2c"),
                MessageProcessingModel.MPv2c,
                SecurityModel.SECURITY_MODEL_SNMPv2c,
                new OctetString("public"),
                SecurityLevel.NOAUTH_NOPRIV,
                StorageType.permanent);
        notificationMIB.addNotifyEntry(new OctetString("default"),
                new OctetString("notify"),
                SnmpNotificationMIB.SnmpNotifyTypeEnum.trap,
                StorageType.permanent);
    }

    protected void addViews(VacmMIB vacm) {
        vacm.addGroup(SecurityModel.SECURITY_MODEL_SNMPv1,
                new OctetString("public"),
                new OctetString("v1v2group"),
                StorageType.nonVolatile);
        vacm.addGroup(SecurityModel.SECURITY_MODEL_SNMPv2c,
                new OctetString("public"),
                new OctetString("v1v2group"),
                StorageType.nonVolatile);
        vacm.addGroup(SecurityModel.SECURITY_MODEL_USM,
                new OctetString("SHADES"),
                new OctetString("v3group"),
                StorageType.nonVolatile);
        vacm.addGroup(SecurityModel.SECURITY_MODEL_USM,
                new OctetString("TEST"),
                new OctetString("v3test"),
                StorageType.nonVolatile);
        vacm.addGroup(SecurityModel.SECURITY_MODEL_USM,
                new OctetString("SHA"),
                new OctetString("v3restricted"),
                StorageType.nonVolatile);

        vacm.addAccess(new OctetString("v1v2group"), new OctetString(),
                SecurityModel.SECURITY_MODEL_ANY,
                SecurityLevel.NOAUTH_NOPRIV, VacmMIB.vacmExactMatch,
                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, VacmMIB.vacmExactMatch,
                new OctetString("fullReadView"),
                new OctetString("fullWriteView"),
                new OctetString("fullNotifyView"),
                StorageType.nonVolatile);
        vacm.addAccess(new OctetString("v3restricted"), new OctetString(),
                SecurityModel.SECURITY_MODEL_USM,
                SecurityLevel.AUTH_NOPRIV, VacmMIB.vacmExactMatch,
                new OctetString("restrictedReadView"),
                new OctetString("restrictedWriteView"),
                new OctetString("restrictedNotifyView"),
                StorageType.nonVolatile);
        vacm.addAccess(new OctetString("v3test"), new OctetString(),
                SecurityModel.SECURITY_MODEL_USM,
                SecurityLevel.AUTH_PRIV, VacmMIB.vacmExactMatch,
                new OctetString("testReadView"),
                new OctetString("testWriteView"),
                new OctetString("testNotifyView"),
                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("testReadView"),
                new OID("1.3.6.1.2"),
                new OctetString(), VacmMIB.vacmViewIncluded,
                StorageType.nonVolatile);
        vacm.addViewTreeFamily(new OctetString("testReadView"),
                new OID("1.3.6.1.2.1.1"),
                new OctetString(), VacmMIB.vacmViewExcluded,
                StorageType.nonVolatile);
        vacm.addViewTreeFamily(new OctetString("testWriteView"),
                new OID("1.3.6.1.2.1"),
                new OctetString(),
                VacmMIB.vacmViewIncluded,
                StorageType.nonVolatile);
        vacm.addViewTreeFamily(new OctetString("testNotifyView"),
                new OID("1.3.6.1.2"),
                new OctetString(), VacmMIB.vacmViewIncluded,
                StorageType.nonVolatile);

    }

    @SuppressWarnings("unchecked")
    protected void addCommunities(SnmpCommunityMIB communityMIB) {
        Variable[] com2sec = new Variable[]{
                new OctetString("public"),              // community name
                new OctetString("public"),              // security name
                agent.getLocalEngineID(),               // local engine ID
                new OctetString(),                      // default context name
                new OctetString(),                      // transport tag
                new Integer32(StorageType.nonVolatile), // storage type
                new Integer32(RowStatus.active)         // row status
        };
        SnmpCommunityMIB.SnmpCommunityEntryRow row =
                communityMIB.getSnmpCommunityEntry().createRow(
                        new OctetString("public2public").toSubIndex(true), com2sec);
        communityMIB.getSnmpCommunityEntry().addRow(row);
    }

    protected void addUsmUser(USM usm) {
        UsmUser user = new UsmUser(new OctetString("SHADES"),
                AuthSHA.ID,
                new OctetString("SHADESAuthPassword"),
                PrivDES.ID,
                new OctetString("SHADESPrivPassword"));
        usm.addUser(user, usm.getLocalEngineID());
        user = new UsmUser(new OctetString("TEST"),
                AuthSHA.ID,
                new OctetString("maplesyrup"),
                PrivDES.ID,
                new OctetString("maplesyrup"));
        usm.addUser(user, usm.getLocalEngineID());
        user = new UsmUser(new OctetString("SHA"),
                AuthSHA.ID,
                new OctetString("SHAAuthPassword"),
                null,
                null);
        usm.addUser(user, usm.getLocalEngineID());
    }
*/

    /**
     * The program entry point that parses the command line arguments, creates the master agent,
     * binds the AgentX master transport, and runs the agent until the process is interrupted.
     *
     * @param args
     *    the command line arguments.
     */
    public static void main(String[] args) {
        ArgumentParser parser = new ArgumentParser(COMMAND_LINE_OPTIONS, COMMAND_LINE_PARAMS);
        Map<String, List<Object>> commandLineParameters = null;
        try {
            commandLineParameters = parser.parse(args);
            if (commandLineParameters.containsKey("h")) {
                printUsage();
                System.exit(0);
            }
            if (commandLineParameters.containsKey("v")) {
                System.out.println("Options: " + commandLineParameters);
            }
            TestMasterAgent sampleAgent = new TestMasterAgent(commandLineParameters, SAMPLE_MASTER_AGENT_POLICY);
            String agentXAddress =
                    ((String) (commandLineParameters.get("X")).get(0));
            Address agentXMasterAddress = GenericAddress.parse(agentXAddress);
            if (agentXMasterAddress == null) {
                System.err.println("AgentX master address: '" + agentXAddress + "' cannot be bound, aborting");
                System.exit(1);
            }
            sampleAgent.agent.addAgentXTransportMapping(TransportMappings.getInstance().createTransportMapping(agentXMasterAddress));
            sampleAgent.agent.registerShutdownHook();
            sampleAgent.agent.run();
            sampleAgent.agent.getCommandProcessor().setAcceptNewContexts(true);
            while (true) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException ex1) {
                    break;
                }
            }
        } catch (ArgumentParser.ArgumentParseException ax) {
            printUsage();
            System.out.println(ax.getMessage());
        } catch (Exception ex) {
            logger.fatal("Caught exception while starting the agent", ex);
            ex.printStackTrace();
        }
    }

    private static void printUsage() {
        String[] txt = {
                "Usage: TestMasterAgent [-c <config-path>] [-cfg <config-file>]",
                "                       [-ts <table-size-limits-file>] [-dhks <dh-kickstart-file>]",
                "                       [-u <security-name>]",
                "                       [-tls-trust-ca <ca-alias>] [-tls-peer-id <peer-id>]",
                "                       [-tls-local-id <local-id>]",
                "                       [-tls-version <TLSv1|TLSv1.1|TLSv1.2|TLSv1.3>]",
                "                       [-dtls-version <TLSv1.0|TLSv1.2>]",
                "                       [-Djavax.net.ssl.keyStore <key-store>]",
                "                       [-Djavax.net.ssl.keyStorePassword <password>]",
                "                       [-Djavax.net.ssl.trustStore <trust-store>]",
                "                       [-Djavax.net.ssl.trustStorePassword <password>]",
                "                       [-write-init-config <markdown-file>]",
                "                       [-viewer <viewer-passkey>] [-admin <admin-passkey>]",
                "                       [-x] [-h] [-v]",
                "                       [-X <masterAddress>] <address1> [<address2> ..]",
                "",
                "where ",
                "  <config-path>        is the directory where persistent MIB data is stored/read",
                "                       (default: SampleAgent.cfg).",
                "  <config-file>        is the initial agent configuration properties file. If",
                "                       omitted, the bundled SampleAgentConfig.properties is used.",
                "  <table-size-limits-file>",
                "                       is the table size limits properties file. If omitted, the",
                "                       bundled SampleAgentTableSizeLimits.properties is used.",
                "  <dh-kickstart-file>  is the Diffie-Hellman kickstart parameters properties file.",
                "  <security-name>      is the SNMPv3 security name used by SNMP configuration.",
                "  <ca-alias>           is the accepted TLS trust CA alias.",
                "  <peer-id>            is the expected TLS peer identity.",
                "  <local-id>           is the local TLS identity.",
                "  <TLS version>        selects the TLS or DTLS protocol version.",
                "  -Djavax.net.ssl.*    configures the Java SSL key store and trust store settings.",
                "  <markdown-file>      receives a Markdown overview of the initial MIB",
                "                       configuration before the agent starts.",
                "  <viewer-passkey>     overrides the named random value 'viewer' used by the",
                "                       initial configuration (alternatively use env variable",
                "                       SNMP4J_AGENTX_TEST_VIEWER_PASSKEY).",
                "  <admin-passkey>      overrides the named random value 'admin' used by the",
                "                       initial configuration (alternatively use env variable",
                "                       SNMP4J_AGENTX_TEST_ADMIN_PASSKEY).",
                "  -x                   enables SNMP4J extensibility.",
                "  -h                   prints this usage help information and exits.",
                "  -v                   prints parsed command line parameters.",
                "  <masterAddress>      is the AgentX master agent address of the local host",
                "                       following the format '(tcp|unix):<host>/<port>'",
                "                       (default: tcp:0.0.0.0/705).",
                "  <address>            is a listen address following the format",
                "                       'udp|tcp|tls|dtls:<host>/<port>', for example",
                "                       udp:0.0.0.0/161.",
                ""
        };
        for (String line : txt) {
            System.out.println(line);
        }
    }
}