All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.snmp4j.agent.db.MOXodusPersistence Maven / Gradle / Ivy

/*_############################################################################
  _## 
  _##  SNMP4J-Agent-DB 3 - MOXodusPersistence.java  
  _## 
  _##  Copyright (C) 2017-2018  Frank Fock (SNMP4J.org)
  _##  
  _##  Licensed 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.snmp4j.agent.db;

import jetbrains.exodus.ArrayByteIterable;
import jetbrains.exodus.ByteIterable;
import jetbrains.exodus.ByteIterator;
import jetbrains.exodus.bindings.CompressedUnsignedLongArrayByteIterable;
import jetbrains.exodus.env.*;
import org.jetbrains.annotations.NotNull;
import org.snmp4j.agent.*;
import org.snmp4j.agent.io.ImportMode;
import org.snmp4j.agent.mo.*;
import org.snmp4j.asn1.BER;
import org.snmp4j.asn1.BERInputStream;
import org.snmp4j.asn1.BEROutputStream;
import org.snmp4j.log.LogAdapter;
import org.snmp4j.log.LogFactory;
import org.snmp4j.smi.*;

import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.*;

/**
 * The {@link MOXodusPersistence} class provides persistent storage for SNMP4J-Agent using the
 * {@link MOXodusPersistenceProvider} wrapper that actually implements the
 * {@link org.snmp4j.agent.io.MOPersistenceProvider} interface of SNMP4J-Agent.
 * As storage engine, the Xodus open source (Apache 2 License) DB is used.
 * See https://github.com/JetBrains/xodus} for details.
 * The database approach has the following advantages compared to the standard sequential persistence provider coming
 * with SNMP4J-Agent:
 * 
    *
  • Only changed objects are written again to disk. The default DefaultMOPersistenceProvider needs to save all * objects in a sequence.
  • *
  • DB size is smaller - if changes are limited to approximately less than 40% of the MIB objects during * runtime.
  • *
  • Agent shutdown is much faster because no objects need to be saved anymore.
  • *
  • No data loss if agent is killed.
  • *
* The following sample code from {@link org.snmp4j.agent.db.sample.SampleAgent} illustrates how this class is created * and assigned to the agent during its initialization. * *
 *     File configFile = new File(myConfigDir);
 *     MOXodusPersistence moXodusPersistence = new MOXodusPersistence(moServers, Environments.newInstance(configFile));
 *     MOXodusPersistenceProvider moXodusPersistenceProvider = new MOXodusPersistenceProvider(moXodusPersistence);
 *     OctetString defaultEngineID = new OctetString(MPv3.createLocalEngineID());
 *     OctetString engineID = moXodusPersistenceProvider.getEngineId(defaultEngineID);
 *     ...
 *     agent = new AgentConfigManager(engineID, messageDispatcher, null, moServers, ThreadPool.create("SampleAgent", 3),
 *                                    (defaultEngineID == engineID) ? configurationFactory : null,
 *                                    moXodusPersistenceProvider,
 *                                    new EngineBootsCounterFile(bootCounterFile), null, dhKickstartParameters);
 *     agent.addAgentStateListener(new AgentStateListener() {
 *         public void agentStateChanged(AgentConfigManager agentConfigManager, AgentState newState) {
 *             switch (newState.getState()) {
 *                 case AgentState.STATE_INITIALIZED:
 *                     moXodusPersistence.registerChangeListenersWithServer(server);
 *                     break;
 *                 case AgentState.STATE_SHUTDOWN:
 *                     moXodusPersistence.unregisterChangeListenersWithServer(server);
 *                     break;
 *             }
 *         }
 *     });
 *
 * 
* * @author Frank Fock * @version 3.2.0 * @since 3.0 */ public class MOXodusPersistence implements MOChangeListener { private static final LogAdapter logger = LogFactory.getLogger(MOXodusPersistence.class); public enum SavingStrategy { /** * The default strategy saves changed data only if a corresponding {@link MOChangeEvent} has been received. */ onChangeEventsOnly, /** * Save only when the {@link #save()} method is being called and the persistent data do no longer match the * data in the agent. */ checkForModificationsOnSave, /** * Always save everything when {@link #save()} is being called without checking for modifications. */ fullDumpOnSave } private MOServer[] moServers; private Environment environment; private boolean ignoreChangeListenerEvents; private int continuousChangeListening = 0; private SavingStrategy savingStrategy = SavingStrategy.onChangeEventsOnly; /** * Creates a new {@link MOXodusPersistence} from an array of {@link MOServer} instances and an Xodus * {@link Environment}. The data of modified objects are stored whenever a corresponding {@link MOChangeEvent} is * received. * @param moServers * the {@link ManagedObject} servers of the agent to be supported with persistent storage capabilities by this * object. * @param environment * the Xodus environment that actually holds the persistent data. */ public MOXodusPersistence(MOServer[] moServers, Environment environment) { this(moServers, environment, SavingStrategy.onChangeEventsOnly); } /** * Creates a new {@link MOXodusPersistence} from an array of {@link MOServer} instances and an Xodus * {@link Environment}. * @param moServers * the {@link ManagedObject} servers of the agent to be supported with persistent storage capabilities by this * object. * @param environment * the Xodus environment that actually holds the persistent data. * @param savingStrategy * defines when and how modified objects of the agent should be saved into persistent storage. */ public MOXodusPersistence(MOServer[] moServers, Environment environment, SavingStrategy savingStrategy) { this.moServers = moServers; this.environment = environment; this.savingStrategy = savingStrategy; } public SavingStrategy getSavingStrategy() { return savingStrategy; } public void setSavingStrategy(SavingStrategy savingStrategy) { this.savingStrategy = savingStrategy; } public boolean isIgnoreChangeListenerEvents() { return ignoreChangeListenerEvents; } /** * Defines whether {@link MOChangeEvent}s should be ignored or not. This method can be used to disable persistent * storage activities when the default strategy {@link SavingStrategy#onChangeEventsOnly} is active and other bulk * operations change MIB data in the agent. When activating the processing of {@link MOChangeEvent}s is activated * again by setting this value to {@code false}, the missed events will not be processed again. Thus, if data has * changed that need to be persistent, the {@link #save()} has to be called with strategy * {@link SavingStrategy#checkForModificationsOnSave} or {@link SavingStrategy#fullDumpOnSave} manually. * * @param ignoreChangeListenerEvents * {@code true} to disable event processing and saving changes triggered by {@link MOChangeEvent}s. */ public void setIgnoreChangeListenerEvents(boolean ignoreChangeListenerEvents) { this.ignoreChangeListenerEvents = ignoreChangeListenerEvents; } public Environment getEnvironment() { return environment; } public boolean isContinuousChangeListening() { return continuousChangeListening > 1; } /** * Register this object as {@link MOChangeListener} on all {@link RandomAccessManagedObject} instances in the * provided {@link MOServer}. * * @param moServer * a {@link MOServer} holding {@link RandomAccessManagedObject}s that should be persisted. * */ public synchronized void registerChangeListenersWithServer(MOServer moServer) { DefaultMOServer.registerChangeListener(moServer, this, mo -> mo instanceof RandomAccessManagedObject); continuousChangeListening = 1; } /** * Removes a former registration of this object as {@link MOChangeListener} on all {@link RandomAccessManagedObject} * instances in the provided {@link MOServer}. * * @param moServer * a {@link MOServer} holding {@link RandomAccessManagedObject}s that should not be persisted anymore. * */ public synchronized void unregisterChangeListenersWithServer(MOServer moServer) { DefaultMOServer.unregisterChangeListener(moServer, this, mo -> mo instanceof RandomAccessManagedObject); continuousChangeListening = 0; } /** * Checks if there is already MIB data stored for the specified context. * To check the default context ({@code null}), please use the empty {@link OctetString}. This method should be * called before calling {@link #load(ImportMode)} because afterwards it will return {@code true} for all contexts * that were present in {@link #getMOServer()} and for the default context (empty context). * @param context * a non-null context string. The empty (zero length) {@link OctetString} represents the default context. * @return * {@code true} if there has been data stored for this context - even if no {@link RandomAccessManagedObject} * actually has stored any data. * @since 3.0.1 */ public synchronized boolean isContextLoadable(OctetString context) { final Transaction txn = environment.beginReadonlyTransaction(); try { return environment.storeExists(storeNameFromContext(context), txn); } finally { txn.abort(); } } /** * Load the contents of all {@link RandomAccessManagedObject}s using * {@link RandomAccessManagedObject#importInstance(OID, List, ImportMode)} calls. The provided {@link ImportMode} * thereby defines how the data handles existing data. * Data is loaded for all contexts and managed objects found in the {link MOServer} instances provided during * object creation. * While loading, the member {@link #ignoreChangeListenerEvents} is set to {@code true} to ignore updates caused * by loading data into the {@link RandomAccessManagedObject} instances. * * @param importMode * controls how existing data is used or not used during import. */ public synchronized void load(ImportMode importMode) { try { setIgnoreChangeListenerEvents(true); continuousChangeListening = (continuousChangeListening > 0) ? continuousChangeListening = 2 : 0; final Transaction txn = environment.beginReadonlyTransaction(); for (MOServer moServer : moServers) { Map stores = new HashMap<>(); for (OctetString context : moServer.getContexts()) { try { if (!stores.containsKey(context)) { stores.put(context, createStore(txn, context)); } Iterator>> moIterator = moServer.iterator(); runSynchronization(stores, txn, importMode, moIterator); } catch (ReadonlyTransactionException rotex) { logger.info("No persistent data for context '" + context + "' context found"); } } if (moServer.isContextSupported(null)) { try { stores.put(new OctetString(), environment.openStore("", StoreConfig.WITHOUT_DUPLICATES, txn)); Iterator>> moIterator = moServer.iterator(); runSynchronization(stores, txn, importMode, moIterator); } catch (ReadonlyTransactionException rotex) { logger.info("No persistent data for context default context found"); } } } txn.abort(); } finally { setIgnoreChangeListenerEvents(false); } } @NotNull protected Store createStore(Transaction txn, OctetString context) { return environment.openStore(storeNameFromContext(context), StoreConfig.WITHOUT_DUPLICATES, txn); } /** * Return a string store name for the provided SNMPv3 context. * @param context * a context name or {@code null} for the default context. * @return * a store name, by default {@code context == null ? "" : context.toHexString()}. * @since 3.0.1 */ protected String storeNameFromContext(OctetString context) { return context == null ? "" : context.toHexString(); } public MOServer[] getMOServer() { return moServers; } protected void runSynchronization(Map stores, Transaction txn, ImportMode importMode, Iterator>> moIterator) { // import mode == null -> export for (; moIterator.hasNext(); ) { Map.Entry> entry = moIterator.next(); if (entry.getValue() instanceof MOScalar) { logger.debug("MOScalar " + entry.getValue()); } if (entry.getValue() instanceof RandomAccessManagedObject) { RandomAccessManagedObject randomAccessManagedObject = (RandomAccessManagedObject) entry.getValue(); if (randomAccessManagedObject.isVolatile()) { if (logger.isDebugEnabled()) { logger.debug("Ignored " + randomAccessManagedObject + " because it is volatile"); } continue; } OctetString context = new OctetString(); if (entry.getKey() instanceof MOContextScope) { context = ((MOContextScope) entry.getKey()).getContext(); } Store store = stores.get(context); if (store == null) { logger.info("DB store for context '"+context+"' not found, creating it."); store = createStore(txn, context); } Cursor cursor = store.openCursor(txn); OID oid = entry.getKey().getLowerBound(); HashSet exported = null; final ByteIterable objectKey = cursor.getSearchKeyRange(getKey(oid, new OID())); if (objectKey != null) { OID instanceOID; while ((instanceOID = getKeyOid(cursor.getKey())).startsWith(oid)) { OID index = instanceOID.getSuffix(oid); ByteIterable rawValues = cursor.getValue(); List rawVBS = decodeInstanceData(rawValues); if (rawVBS != null && rawVBS.size() > 0) { if (importMode == null) { if (exported == null) { exported = new HashSet<>(randomAccessManagedObject.instanceCount()); } List exportVbs = null; if (!randomAccessManagedObject.isVolatile(index)) { exportVbs = randomAccessManagedObject.exportInstance(index); } if (exportVbs == null) { cursor.deleteCurrent(); } else { if (savingStrategy == SavingStrategy.checkForModificationsOnSave) { byte[] exportRawValues = encodeInstanceData(exportVbs); ArrayByteIterable exportByteIterable = new ArrayByteIterable(exportRawValues); if (rawValues.compareTo(exportByteIterable) != 0) { ByteIterable key = getKey(oid, index); if (logger.isDebugEnabled()) { logger.debug("Saving modified " + context + ":" + oid + "|" + index + " ("+key+") = " + new OctetString(exportRawValues).toHexString() + " <- " + exportVbs); } store.put(txn, key, new ArrayByteIterable(rawValues)); exported.add(index); } } else { putInstanceData(txn, context, store, oid, index, exportVbs); exported.add(index); } } } else { if (logger.isDebugEnabled()) { logger.debug("Loading data for " + oid + " with index " + index + " ("+cursor.getKey()+"): " + rawVBS); } randomAccessManagedObject.importInstance(index, rawVBS, importMode); } } else { // unknown & unsupported entity if (logger.isWarnEnabled()) { logger.warn("Unable to load persistent data: " + new OctetString(rawValues.getBytesUnsafe()).toHexString()); } break; } if (!cursor.getNext()) { break; } } } // export new instances if (importMode == null) { // export mode for (Iterator oidIterator = randomAccessManagedObject.instanceIterator(); oidIterator.hasNext(); ) { OID nextInstanceOID = oidIterator.next(); if (exported == null || !exported.contains(nextInstanceOID)) { List vbs = null; if (!randomAccessManagedObject.isVolatile(nextInstanceOID)) { vbs = randomAccessManagedObject.exportInstance(nextInstanceOID); } if (vbs != null) { putInstanceData(txn, context, store, oid, nextInstanceOID, vbs); } } } } } } } private void putInstanceData(Transaction txn, OctetString context, Store store, OID oid, OID index, List vbs) { byte[] rawValues = encodeInstanceData(vbs); ByteIterable key = getKey(oid, index); if (logger.isDebugEnabled()) { logger.debug("Saving " + context + ":" + oid + "|" + index + " ("+key+") = " + new OctetString(rawValues).toHexString() + " <- " + vbs); } store.put(txn, key, new ArrayByteIterable(rawValues)); } protected List decodeInstanceData(ByteIterable rawData) { BERInputStream inputStream = new BERInputStream(ByteBuffer.wrap(rawData.getBytesUnsafe(), 0, rawData.getLength())); try { BER.MutableByte pduType; pduType = new BER.MutableByte(); int vbLength = BER.decodeHeader(inputStream, pduType); if (pduType.getValue() != BER.SEQUENCE) { throw new IOException("Encountered invalid tag, SEQUENCE expected: " + pduType.getValue()); } // rest read count int startPos = (int) inputStream.getPosition(); ArrayList variableBindings = new ArrayList<>(); while (inputStream.getPosition() - startPos < vbLength) { VariableBinding vb = decodeVariableBinding(inputStream); variableBindings.add(vb); } if (inputStream.getPosition() - startPos != vbLength) { throw new IOException("Length of VB sequence (" + vbLength + ") does not match real length: " + ((int) inputStream.getPosition() - startPos)); } return variableBindings; } catch (IOException e) { logger.error(e); } return null; } protected byte[] encodeInstanceData(List vbs) { ArrayList exports = new ArrayList<>(vbs.size()); // exports.add(new VariableBinding(instanceSubID, new Integer32(0))); exports.addAll(vbs); int vbLength = 0; for (VariableBinding vb : exports) { int indexOIDLength = getIndexOIDLength(vb.getOid().getValue()); int subLength = indexOIDLength + BER.getBERLengthOfLength(indexOIDLength) + 1 + vb.getVariable().getBERLength(); vbLength += BER.getBERLengthOfLength(subLength) + 1 + subLength; } BEROutputStream berOutputStream = new BEROutputStream(ByteBuffer.allocate(vbLength + BER.getBERLengthOfLength(vbLength) + 1)); try { BER.encodeHeader(berOutputStream, BER.SEQUENCE, vbLength); for (VariableBinding vb : exports) { encodeVariableBinding(vb, berOutputStream); } berOutputStream.flush(); berOutputStream.close(); return berOutputStream.getBuffer().array(); } catch (IOException e) { logger.error(e); return null; } } protected VariableBinding decodeVariableBinding(BERInputStream inputStream) throws IOException { BER.MutableByte type = new BER.MutableByte(); int length = BER.decodeHeader(inputStream, type); if (type.getValue() != BER.SEQUENCE) { throw new IOException("Invalid sequence encoding: " + type.getValue()); } OID index = new OID(decodeIndexOID(inputStream, type)); Variable variable = AbstractVariable.createFromBER(inputStream); return new VariableBinding(index, variable); } protected void encodeVariableBinding(VariableBinding vb, BEROutputStream outputStream) throws IOException { Variable variable = vb.getVariable(); int indexOIDLength = getIndexOIDLength(vb.getOid().getValue()); int length = indexOIDLength + BER.getBERLengthOfLength(indexOIDLength) + variable.getBERLength(); BER.encodeHeader(outputStream, BER.SEQUENCE, length); encodeIndexOID(outputStream, BER.OID, vb.getOid().getValue()); variable.encodeBER(outputStream); } protected ByteIterable getKey(OID oid, OID instanceID) { if (instanceID == null || instanceID.size() == 0) { return CompressedUnsignedLongArrayByteIterable.getIterable(oid.toUnsignedLongArray()); } OID instanceOID = new OID(oid.getValue(), instanceID.getValue()); return CompressedUnsignedLongArrayByteIterable.getIterable(instanceOID.toUnsignedLongArray()); } protected OID getKeyOid(ByteIterable key) { int len = key.getLength(); ByteIterator byteIterator = key.iterator(); int bytesPerLong = byteIterator.next(); int[] values = new int[(len-1)/bytesPerLong]; for (int i = 0; i < values.length; ++i) { values[i] = (int) byteIterator.nextLong(bytesPerLong); } return new OID(values); } /** * Saves the data of the {@link MOServer}s associated with this instance to persistent storage depoending on * the currently configured {@link SavingStrategy}. If that strategy is {@link SavingStrategy#onChangeEventsOnly}, * calling this method will have no effect, except that it sets {@link #setIgnoreChangeListenerEvents(boolean)} to * {@code false} in any case. */ public synchronized void save() { try { if (savingStrategy != SavingStrategy.onChangeEventsOnly || !isContinuousChangeListening()) { for (MOServer moServer : moServers) { final Transaction txn = environment.beginExclusiveTransaction(); Map stores = new HashMap<>(); for (OctetString context : moServer.getContexts()) { if (!stores.containsKey(context)) { stores.put(context, createStore(txn, context)); } Iterator>> moIterator = moServer.iterator(); runSynchronization(stores, txn, null, moIterator); } if (moServer.isContextSupported(null)) { stores.put(new OctetString(), environment.openStore("", StoreConfig.WITHOUT_DUPLICATES, txn)); Iterator>> moIterator = moServer.iterator(); runSynchronization(stores, txn, null, moIterator); } txn.flush(); txn.commit(); } } } finally { setIgnoreChangeListenerEvents(false); } } public static void encodeIndexOID(OutputStream os, byte type, int[] oid) throws IOException { BER.encodeHeader(os, type, getIndexOIDLength(oid)); int encodedLength = oid.length; int rpos = 0; while (encodedLength-- > 0) { BER.encodeSubID(os, oid[rpos++]); } } public static int getIndexOIDLength(int[] value) { int length = 0; for (int i = 0; i < value.length; i++) { length += BER.getSubIDLength(value[i]); } return length; } public static int[] decodeIndexOID(BERInputStream is, BER.MutableByte type) throws IOException { int subidentifier; int length; // get the type type.setValue((byte) is.read()); if (type.getValue() != 0x06) { throw new IOException("Wrong type. Not an OID: " + type.getValue() + BER.getPositionMessage(is)); } length = BER.decodeLength(is); int[] oid = new int[length]; // in SNMP pos = 1, but we want to encode any unsigned int at first/second position! int pos = 0; while (length > 0) { subidentifier = 0; int b; do { /* shift and add in low order 7 bits */ int next = is.read(); if (next < 0) { throw new IOException("Unexpected end of input stream" + BER.getPositionMessage(is)); } b = next & 0xFF; subidentifier = (subidentifier << 7) + (b & ~BER.ASN_BIT8); length--; } while ((length > 0) && ((b & BER.ASN_BIT8) != 0)); /* last byte has high bit clear */ oid[pos++] = subidentifier; } if (pos < oid.length) { int[] value = new int[pos]; System.arraycopy(oid, 0, value, 0, pos); return value; } return oid; } /** * A ManagedObject change is being prepared. To cancel preparation set the * deny reason to a SNMPv2/v3 error status. * * @param changeEvent * the change event object. */ public void beforePrepareMOChange(MOChangeEvent changeEvent) { } /** * A change has been prepared. Setting the deny reason of the supplied event * object will be ignored. * * @param changeEvent * the change event object. */ public void afterPrepareMOChange(MOChangeEvent changeEvent) { } /** * A ManagedObject change is being committed. To cancel the commit phase set * the deny reason to a SNMPv2/v3 error status. *

* NOTE: Canceling the commit phase must be avoided. Setting a deny reason * has only an effect if {@link MOChangeEvent#isDeniable()} returns * {@code true}. Otherwise, you will need to throw an exception. * * @param changeEvent * the change event object. */ public void beforeMOChange(MOChangeEvent changeEvent) { } /** * A change has been committed. Setting the deny reason of the supplied event * object will be ignored. * * @param changeEvent * the change event object. */ public void afterMOChange(MOChangeEvent changeEvent) { if (logger.isDebugEnabled()) { logger.debug("Managed object " + changeEvent.getChangedObject() + " changed"); } if (!isIgnoreChangeListenerEvents()) { ManagedObject managedObject = changeEvent.getChangedObject(); if (managedObject instanceof RandomAccessManagedObject) { RandomAccessManagedObject randomAccessManagedObject = (RandomAccessManagedObject) managedObject; if (randomAccessManagedObject.isVolatile()) { if (logger.isDebugEnabled()) { logger.debug("Ignored change of " + changeEvent.getChangedObject() + " because it is volatile"); } return; } Set contexts = getContexts(managedObject); if (contexts.size() > 0) { OID instanceID = changeEvent.getOID(); if (changeEvent.getOidType() != MOChangeEvent.OidType.index) { instanceID = instanceID.getSuffix(managedObject.getScope().getLowerBound()); } else if (randomAccessManagedObject.isVolatile(instanceID)) { if (logger.isDebugEnabled()) { logger.debug("Sub-instance " + instanceID + " from " + changeEvent + " is volatile"); } return; } @SuppressWarnings("unchecked") List vbs = randomAccessManagedObject.exportInstance(instanceID); final Transaction txn = environment.beginTransaction(); if (changeEvent.getModification() == null) { for (OctetString context : contexts) { Store store = createStore(txn, context); putInstanceData(txn, context, store, managedObject.getScope().getLowerBound(), instanceID, vbs); } } else { switch (changeEvent.getModification()) { case removed: for (OctetString context : contexts) { Store store = createStore(txn, context); store.delete(txn, getKey(managedObject.getScope().getLowerBound(), instanceID)); if (logger.isDebugEnabled()) { logger.debug("Removed managed object " + changeEvent.getChangedObject() + "|" + instanceID); } } break; case added: case updated: for (OctetString context : contexts) { Store store = createStore(txn, context); putInstanceData(txn, context, store, managedObject.getScope().getLowerBound(), instanceID, vbs); } break; } } txn.flush(); txn.commit(); } else { logger.warn("Managed object " + changeEvent.getChangedObject() + " is not registered to any MOServer known to " + this); } } } else if (logger.isDebugEnabled()) { logger.debug("Ignored change event "+changeEvent); } } protected Set getContexts(ManagedObject managedObject) { Set contexts = new HashSet<>(); for (MOServer moServer : moServers) { contexts.addAll(Arrays.asList(moServer.getRegisteredContexts(managedObject))); } return contexts; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy