diff --git a/container/features/pom.xml b/container/features/pom.xml index 10031cfcff3f..d69628739031 100644 --- a/container/features/pom.xml +++ b/container/features/pom.xml @@ -1011,6 +1011,13 @@ org.opennms.features.notifications.shell ${project.version} + + org.opennms.features + mib-compiler-rest + ${project.version} + pom + provided + diff --git a/container/features/src/main/resources/features.xml b/container/features/src/main/resources/features.xml index 8ec4f1d5a1fa..7df40f73f98a 100644 --- a/container/features/src/main/resources/features.xml +++ b/container/features/src/main/resources/features.xml @@ -2135,5 +2135,15 @@ mvn:org.apache.httpcomponents/httpclient-osgi/${httpclientVersion} mvn:org.apache.httpcomponents/httpasyncclient-osgi/${httpasyncclientVersion} + + +
+ REST interface for the OpenNMS MIB Compiler. +
+ mvn:org.opennms.features.mib-compiler-rest/org.opennms.features.mib-compiler-rest.parser/${project.version} + mvn:org.opennms.features.mib-compiler-rest/org.opennms.features.mib-compiler-rest.api/${project.version} +
diff --git a/container/karaf/src/main/filtered-resources/etc/org.apache.karaf.features.cfg b/container/karaf/src/main/filtered-resources/etc/org.apache.karaf.features.cfg index f9d5ca9f3090..0176528ab12f 100644 --- a/container/karaf/src/main/filtered-resources/etc/org.apache.karaf.features.cfg +++ b/container/karaf/src/main/filtered-resources/etc/org.apache.karaf.features.cfg @@ -130,6 +130,7 @@ featuresBoot = ( \ opennms-config-management, \ opennms-scv-rest, \ scv-shell, \ + mib-compiler-rest, \ opennms-karaf-health # Ensure that the 'opennms-karaf-health' feature is installed *last* diff --git a/features/mib-compiler-rest/api/pom.xml b/features/mib-compiler-rest/api/pom.xml new file mode 100644 index 000000000000..978729c0a0d1 --- /dev/null +++ b/features/mib-compiler-rest/api/pom.xml @@ -0,0 +1,90 @@ + + + 4.0.0 + + org.opennms.features.mib-compiler-rest + org.opennms.features + 36.0.0-SNAPSHOT + + + org.opennms.features.mib-compiler-rest + org.opennms.features.mib-compiler-rest.api + bundle + OpenNMS :: Features :: Mib Compiler Rest :: API + + + + UTF-8 + + + + + org.apache.felix + maven-bundle-plugin + true + + + JavaSE-1.8 + ${project.artifactId} + ${project.version} + * + + + + + + + + com.google.guava + guava + + + javax.ws.rs + javax.ws.rs-api + + + + org.apache.cxf + cxf-rt-frontend-jaxrs + ${cxfVersion} + + + com.sun.xml.bind + jaxb-core + + + com.sun.xml.bind + jaxb-impl + + + + + org.apache.commons + commons-lang3 + + + org.opennms + opennms-web-api + + + + junit + junit + test + + + org.mockito + mockito-core + test + + + org.opennms.features.mib-compiler-rest + org.opennms.features.mib-compiler-rest.parser + 36.0.0-SNAPSHOT + compile + + + + \ No newline at end of file diff --git a/features/mib-compiler-rest/api/src/main/java/org/opennms/features/mibcompiler/rest/api/MibCompilerRestService.java b/features/mib-compiler-rest/api/src/main/java/org/opennms/features/mibcompiler/rest/api/MibCompilerRestService.java new file mode 100644 index 000000000000..a690e38c6739 --- /dev/null +++ b/features/mib-compiler-rest/api/src/main/java/org/opennms/features/mibcompiler/rest/api/MibCompilerRestService.java @@ -0,0 +1,33 @@ +package org.opennms.features.mibcompiler.rest.api; + +import org.apache.cxf.jaxrs.ext.multipart.Attachment; +import org.apache.cxf.jaxrs.ext.multipart.Multipart; +import org.opennms.features.mibcompiler.rest.api.model.CompileMibRequest; + +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; +import java.util.List; + +@Path("/mib-compiler") +public interface MibCompilerRestService { + + + @POST + @Path("/upload") + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces("application/json") + Response uploadMibFiles(@Multipart("upload") List attachments, + @Context SecurityContext securityContext) throws Exception; + + @POST + @Path("/compile-mib") + @Consumes({MediaType.APPLICATION_JSON}) + @Produces("application/json") + Response compilePendingMib(CompileMibRequest compileMibRequest, @Context SecurityContext securityContext) throws Exception; +} diff --git a/features/mib-compiler-rest/api/src/main/java/org/opennms/features/mibcompiler/rest/api/impl/MibCompilerRestServiceImpl.java b/features/mib-compiler-rest/api/src/main/java/org/opennms/features/mibcompiler/rest/api/impl/MibCompilerRestServiceImpl.java new file mode 100644 index 000000000000..b1fbd6df23d0 --- /dev/null +++ b/features/mib-compiler-rest/api/src/main/java/org/opennms/features/mibcompiler/rest/api/impl/MibCompilerRestServiceImpl.java @@ -0,0 +1,229 @@ +package org.opennms.features.mibcompiler.rest.api.impl; + +import org.apache.cxf.jaxrs.ext.multipart.Attachment; +import org.opennms.features.mibcompiler.rest.api.MibCompilerRestService; +import org.opennms.features.mibcompiler.rest.api.model.CompileMibRequest; +import org.opennms.features.mibcompiler.rest.api.model.CompileMibResult; +import org.opennms.features.mibcompiler.rest.api.service.MibCompilerFileService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; +import java.io.File; +import java.io.InputStream; +import java.util.*; + + +public class MibCompilerRestServiceImpl implements MibCompilerRestService { + private static final Logger LOG = LoggerFactory.getLogger(MibCompilerRestServiceImpl.class); + + + private final MibCompilerFileService mibCompilerFileService; + + public MibCompilerRestServiceImpl(MibCompilerFileService mibCompilerFileService) { + this.mibCompilerFileService = mibCompilerFileService; + } + + + @Override + public Response uploadMibFiles(final List attachments, final SecurityContext securityContext) { + final Map fileMap = new LinkedHashMap<>(); + for (final Attachment attachment : attachments) { + final String originalFilename = safeFilename(attachment); + final String baseName = MibCompilerFileService.stripPathAndExtension(originalFilename); + + if (baseName == null || baseName.isBlank()) { + LOG.warn("Skipping attachment with invalid filename: {}", originalFilename); + continue; + } + + if (fileMap.containsKey(baseName)) { + final String existingFilename = safeFilename(fileMap.get(baseName)); + LOG.warn("Duplicate basename detected: '{}' and '{}' resolve to same name '{}'. Keeping first file.", + existingFilename, originalFilename, baseName); + continue; + } + + fileMap.put(baseName, attachment); + } + + final List> successList = new ArrayList<>(); + final List> errorList = new ArrayList<>(); + + for (final Map.Entry entry : fileMap.entrySet()) { + final String baseName = entry.getKey(); + final Attachment attachment = entry.getValue(); + final String originalFilename = safeFilename(attachment); + + try { + if (mibCompilerFileService.baseNameExistsInPendingOrCompiled(baseName)) { + errorList.add(Map.of( + "filename", originalFilename, + "basename", baseName, + "error", "A MIB with the same base name already exists in pending/ or compiled/." + )); + continue; + } + + final String ext = MibCompilerFileService.normalizeExtension( + getExtensionOrDefault(originalFilename, MibCompilerFileService.DEFAULT_MIB_EXTENSION), + MibCompilerFileService.DEFAULT_MIB_EXTENSION + ); + + try (InputStream in = attachment.getObject(InputStream.class)) { + final File saved = mibCompilerFileService.saveToPending(baseName, ext, in); + successList.add(Map.of( + "filename", originalFilename, + "savedAs", saved.getName(), + "success", true + )); + } + } catch (Exception e) { + String message = e.getMessage(); + if (message == null || message.isBlank()) { + message = "Unexpected error while processing MIB file."; + } + String detailedError = e.getClass().getSimpleName() + ": " + message; + errorList.add(Map.of( + "filename", originalFilename, + "basename", baseName, + "error", detailedError, + "exception", e.getClass().getName() + )); + } + } + + return Response.ok(Map.of("success", successList, "errors", errorList)).build(); + } + + @Override + public Response compilePendingMib(CompileMibRequest request, SecurityContext securityContext) throws Exception { + final String name = request != null ? request.getName() : null; + + final CompileMibResult result = mibCompilerFileService.compilePendingByBaseName(name); + + switch (result.getStatus()) { + + case SUCCESS: { + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", result.getMessage()); + response.put("mibName", safeName(name)); + response.put("compiledFile", fileNameOnly(result.getCompiledFile())); + + return Response.ok(response).build(); + } + + case NOT_FOUND: { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", result.getMessage()); + response.put("mibName", safeName(name)); + + return Response.status(Response.Status.NOT_FOUND) + .entity(response) + .build(); + } + + case INVALID_REQUEST: { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", result.getMessage()); + response.put("mibName", safeName(name)); + + return Response.status(Response.Status.BAD_REQUEST) + .entity(response) + .build(); + } + + case MISSING_DEPENDENCIES: + case CONFLICT: { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", result.getMessage()); + response.put("mibName", safeName(name)); + response.put("missingDependencies", emptyToNull(result.getMissingDependencies())); + + return Response.status(Response.Status.CONFLICT) + .entity(response) + .build(); + } + + case VALIDATION_FAILED: { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", result.getMessage()); + response.put("mibName", safeName(name)); + response.put("errors", result.getFormattedErrors()); + + return Response.status(Response.Status.BAD_REQUEST) + .entity(response) + .build(); + } + + default: { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "Unexpected status: " + result.getStatus()); + response.put("mibName", safeName(name)); + + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(response) + .build(); + } + } + } + + private static String safeName(String name) { + // keep response stable; avoid returning nulls when possible + return name == null ? "" : name; + } + + private static String fileNameOnly(File f) { + return f == null ? null : f.getName(); + } + + private static List emptyToNull(List l) { + return (l == null || l.isEmpty()) ? null : l; + } + + + private static String safeFilename(final Attachment attachment) { + if (attachment == null || attachment.getContentDisposition() == null) { + return null; + } + return attachment.getContentDisposition().getParameter("filename"); + } + + private static String stripPathAndExtension(final String filename) { + if (filename == null) return null; + + // Strip any path + String justName = filename; + int slash = justName.lastIndexOf('/'); + int backslash = justName.lastIndexOf('\\'); + int idx = Math.max(slash, backslash); + if (idx >= 0 && idx + 1 < justName.length()) { + justName = justName.substring(idx + 1); + } + + justName = justName.trim(); + if (justName.isEmpty()) return null; + + // Strip extension + int dot = justName.lastIndexOf('.'); + if (dot > 0) { + return justName.substring(0, dot); + } + return justName; + } + + private static String getExtensionOrDefault(final String filename, final String defaultExt) { + if (filename == null) return defaultExt; + int dot = filename.lastIndexOf('.'); + if (dot > 0 && dot < filename.length() - 1) { + return filename.substring(dot); // includes the "." + } + return defaultExt; + } +} diff --git a/features/mib-compiler-rest/api/src/main/java/org/opennms/features/mibcompiler/rest/api/model/CompileMibRequest.java b/features/mib-compiler-rest/api/src/main/java/org/opennms/features/mibcompiler/rest/api/model/CompileMibRequest.java new file mode 100644 index 000000000000..cca8fbca5149 --- /dev/null +++ b/features/mib-compiler-rest/api/src/main/java/org/opennms/features/mibcompiler/rest/api/model/CompileMibRequest.java @@ -0,0 +1,20 @@ +package org.opennms.features.mibcompiler.rest.api.model; + +public class CompileMibRequest { + + private String name; + + public CompileMibRequest(){ + + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + +} diff --git a/features/mib-compiler-rest/api/src/main/java/org/opennms/features/mibcompiler/rest/api/model/CompileMibResult.java b/features/mib-compiler-rest/api/src/main/java/org/opennms/features/mibcompiler/rest/api/model/CompileMibResult.java new file mode 100644 index 000000000000..3bd54993651b --- /dev/null +++ b/features/mib-compiler-rest/api/src/main/java/org/opennms/features/mibcompiler/rest/api/model/CompileMibResult.java @@ -0,0 +1,96 @@ +package org.opennms.features.mibcompiler.rest.api.model; + +import java.io.File; +import java.util.Collections; +import java.util.List; + +public class CompileMibResult { + + public enum Status { + SUCCESS, + NOT_FOUND, + INVALID_REQUEST, + VALIDATION_FAILED, + MISSING_DEPENDENCIES, + CONFLICT + } + + private final Status status; + private final String message; + private final File pendingFile; + private final File compiledFile; + private final List missingDependencies; + private final String formattedErrors; + + private CompileMibResult(Status status, + String message, + File pendingFile, + File compiledFile, + List missingDependencies, + String formattedErrors) { + this.status = status; + this.message = message; + this.pendingFile = pendingFile; + this.compiledFile = compiledFile; + this.missingDependencies = missingDependencies == null ? Collections.emptyList() : missingDependencies; + this.formattedErrors = formattedErrors; + } + + public static CompileMibResult success(File pendingFile, File compiledFile) { + return new CompileMibResult(Status.SUCCESS, "Compiled successfully.", pendingFile, compiledFile, + Collections.emptyList(), null); + } + + public static CompileMibResult notFound(String message) { + return new CompileMibResult(Status.NOT_FOUND, message, null, null, + Collections.emptyList(), null); + } + + public static CompileMibResult invalidRequest(String message) { + return new CompileMibResult(Status.INVALID_REQUEST, message, null, null, + Collections.emptyList(), null); + } + + public static CompileMibResult validationFailed(String message, String formattedErrors) { + return new CompileMibResult(Status.VALIDATION_FAILED, message, null, null, + Collections.emptyList(), formattedErrors); + } + + public static CompileMibResult missingDependencies(String message, List missingDependencies) { + return new CompileMibResult(Status.MISSING_DEPENDENCIES, message, null, null, + missingDependencies, null); + } + + public static CompileMibResult conflict(String message) { + return new CompileMibResult(Status.CONFLICT, message, null, null, + Collections.emptyList(), null); + } + + public Status getStatus() { + return status; + } + + public String getMessage() { + return message; + } + + public File getPendingFile() { + return pendingFile; + } + + public File getCompiledFile() { + return compiledFile; + } + + public List getMissingDependencies() { + return missingDependencies; + } + + public String getFormattedErrors() { + return formattedErrors; + } + + public boolean isSuccess() { + return status == Status.SUCCESS; + } +} diff --git a/features/mib-compiler-rest/api/src/main/java/org/opennms/features/mibcompiler/rest/api/service/MibCompilerFileService.java b/features/mib-compiler-rest/api/src/main/java/org/opennms/features/mibcompiler/rest/api/service/MibCompilerFileService.java new file mode 100644 index 000000000000..e8585a9d3dbf --- /dev/null +++ b/features/mib-compiler-rest/api/src/main/java/org/opennms/features/mibcompiler/rest/api/service/MibCompilerFileService.java @@ -0,0 +1,240 @@ +package org.opennms.features.mibcompiler.rest.api.service; + +import org.opennms.features.mibcompiler.api.MibParser; +import org.opennms.features.mibcompiler.rest.api.model.CompileMibResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +public class MibCompilerFileService { + + private static final Logger LOG = LoggerFactory.getLogger(MibCompilerFileService.class); + + private final MibParser mibParser; + + public static final String DEFAULT_MIB_EXTENSION = ".mib"; + + private static final String SHARE_MIBS_DIR = "share" + File.separatorChar + "mibs"; + private static final String PENDING_DIR = "pending"; + private static final String COMPILED_DIR = "compiled"; + + public MibCompilerFileService(MibParser mibParser) { + this.mibParser = mibParser; + } + + public File getMibsRootDir() { + return new File(getHome(), SHARE_MIBS_DIR); + } + + public File getPendingDir() { + return new File(getMibsRootDir(), PENDING_DIR); + } + + public File getCompiledDir() { + return new File(getMibsRootDir(), COMPILED_DIR); + } + + public void ensurePendingDirExists() { + ensureDirExists(getPendingDir()); + } + + public void ensureCompiledDirExists() { + ensureDirExists(getCompiledDir()); + } + + public boolean baseNameExistsInPendingOrCompiled(final String baseName) throws Exception { + return baseNameExists(getPendingDir(), baseName) || baseNameExists(getCompiledDir(), baseName); + } + + /** + * Save a file to pending with a normalized name: {baseName}{extension} + * Example: baseName="IF-MIB", extension=".mib" -> IF-MIB.mib + */ + public File saveToPending(final String baseName, + final String extension, + final InputStream content) throws Exception { + + if (baseName == null || baseName.isBlank()) { + throw new IllegalArgumentException("baseName must not be blank."); + } + if (content == null) { + throw new IllegalArgumentException("content must not be null."); + } + + ensurePendingDirExists(); + final String ext = normalizeExtension(extension, DEFAULT_MIB_EXTENSION); + final File target = new File(getPendingDir(), baseName + ext); + + try (FileOutputStream out = new FileOutputStream(target)) { + content.transferTo(out); + } + return target; + } + + /** + * Compile (parse/validate) a file that already exists in the pending directory, then move it + * to the compiled directory, forcing a ".mib" extension. + * + * - The file must exist in pending directory. + * - Caller provides a base name; extension is optional. + * - There should only be one file in pending with that base name. + * - Dependencies should already be compiled (present in compiled dir). + */ + public CompileMibResult compilePendingByBaseName(final String baseName) throws Exception { + if (baseName == null || baseName.isBlank()) { + return CompileMibResult.invalidRequest("baseName must not be blank."); + } + + ensurePendingDirExists(); + ensureCompiledDirExists(); + + final String normalizedBaseName = stripPathAndExtension(baseName); + if (normalizedBaseName == null || normalizedBaseName.isBlank()) { + return CompileMibResult.invalidRequest("baseName must not be blank."); + } + + final File pendingFile = findSingleByBaseName(getPendingDir(), normalizedBaseName); + if (pendingFile == null) { + return CompileMibResult.notFound("No pending file found with base name '" + normalizedBaseName + "'."); + } + + // Parse/validate + final boolean ok = mibParser.parseMib(pendingFile); + if (!ok) { + final List missing = safeList(mibParser.getMissingDependencies()); + if (!missing.isEmpty()) { + return CompileMibResult.missingDependencies("Missing dependencies: " + missing, missing); + } + final String errors = mibParser.getFormattedErrors(); + return CompileMibResult.validationFailed("MIB validation failed.", errors); + } + + // Move to compiled and force .mib extension + final File destFile = new File(getCompiledDir(), normalizedBaseName + DEFAULT_MIB_EXTENSION); + if (destFile.exists()) { + return CompileMibResult.conflict("Compiled file already exists: " + destFile.getName()); + } + + Files.move(pendingFile.toPath(), destFile.toPath(), StandardCopyOption.ATOMIC_MOVE); + + return CompileMibResult.success(pendingFile, destFile); + } + + private static List safeList(final List l) { + return l == null ? Collections.emptyList() : l; + } + + private static File findSingleByBaseName(final File dir, final String baseName) throws Exception { + if (dir == null || !dir.exists() || !dir.isDirectory()) { + return null; + } + + final List matches = new ArrayList(); + try (var stream = Files.list(dir.toPath())) { + stream.filter(Files::isRegularFile).forEach(p -> { + final String bn = stripPathAndExtension(p.getFileName().toString()); + if (baseName.equals(bn)) { + matches.add(p); + } + }); + } + + matches.sort(Comparator.comparing(p -> p.getFileName().toString())); + + if (matches.isEmpty()) { + return null; + } + if (matches.size() > 1) { + throw new IllegalStateException("Multiple pending files found with base name '" + baseName + "': " + matches); + } + return matches.get(0).toFile(); + } + + private static void ensureDirExists(final File dir) { + if (dir.exists()) { + if (!dir.isDirectory()) { + throw new IllegalStateException("Path exists but is not a directory: " + dir.getAbsolutePath()); + } + return; + } + if (!dir.mkdirs()) { + throw new IllegalStateException("Failed to create directory: " + dir.getAbsolutePath()); + } + } + + /** + * Checks if any file in {@code dir} has the given basename (basename comparison, not full filename). + */ + private static boolean baseNameExists(final File dir, final String baseName) throws Exception { + if (dir == null || !dir.exists() || !dir.isDirectory()) { + return false; + } + try (var stream = Files.list(dir.toPath())) { + return stream.anyMatch(p -> baseName.equals(stripPathAndExtension(p.getFileName().toString()))); + } + } + + /** + * Removes any path segments and strips the extension. + */ + public static String stripPathAndExtension(final String filename) { + if (filename == null) return null; + + // Strip any path component + String justName = filename; + int slash = justName.lastIndexOf('/'); + int backslash = justName.lastIndexOf('\\'); + int idx = Math.max(slash, backslash); + if (idx >= 0 && idx + 1 < justName.length()) { + justName = justName.substring(idx + 1); + } + + justName = justName.trim(); + if (justName.isEmpty()) return null; + + // Strip extension + int dot = justName.lastIndexOf('.'); + if (dot > 0) { + return justName.substring(0, dot); + } + return justName; + } + + public static String normalizeExtension(final String extension, final String defaultExt) { + String ext = extension; + if (ext == null || ext.isBlank()) { + ext = defaultExt; + } + ext = ext.trim(); + if (!ext.startsWith(".")) { + ext = "." + ext; + } + return ext; + } + + public static String getHome() { + String home = System.getProperty("opennms.home"); + if (home == null) { + LOG.debug("The 'opennms.home' property was not set, falling back to /opt/opennms. This should really only happen in unit tests."); + home = File.separator + "opt" + File.separator + "opennms"; + } + // Remove the trailing slash if necessary + // + if (home.endsWith("/") || home.endsWith(File.separator)) + home = home.substring(0, home.length() - 1); + + return home; + } + + +} diff --git a/features/mib-compiler-rest/api/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/features/mib-compiler-rest/api/src/main/resources/OSGI-INF/blueprint/blueprint.xml new file mode 100644 index 000000000000..88d60364f726 --- /dev/null +++ b/features/mib-compiler-rest/api/src/main/resources/OSGI-INF/blueprint/blueprint.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/features/mib-compiler-rest/parser/pom.xml b/features/mib-compiler-rest/parser/pom.xml new file mode 100644 index 000000000000..2bfe697e7537 --- /dev/null +++ b/features/mib-compiler-rest/parser/pom.xml @@ -0,0 +1,117 @@ + + + + org.opennms.features.mib-compiler-rest + org.opennms.features + 36.0.0-SNAPSHOT + + 4.0.0 + org.opennms.features.mib-compiler-rest + org.opennms.features.mib-compiler-rest.parser + bundle + OpenNMS :: Features :: Mib Compiler Rest :: Parser + + Standalong MIB-parser based on JSMILib + + org.opennms.features.mib-parser + org.opennms.features + UTF-8 + 0.14 + + + + + + org.apache.felix + maven-bundle-plugin + true + + + JavaSE-1.8 + ${project.artifactId} + ${project.version} + !${bundle.namespace}.internal.*,${bundle.namespace}.*;version="${project.version}" + ${bundle.namespace}.internal.* + jsmiparser* + true + + + + + + + + + org.opennms.dependencies + spring-dependencies + provided + pom + + + org.opennms.core + org.opennms.core.lib + provided + + + org.opennms + opennms-config + provided + + + org.opennms.features.config.service + org.opennms.features.config.service.impl + + + + + org.opennms + opennms-dao + provided + + + org.opennms.features.config.service + org.opennms.features.config.service.api + + + org.opennms.features.config.service + org.opennms.features.config.service.impl + + + + + org.jsmiparser + jsmiparser-api + ${jsmiparser.version} + provided + + + org.opennms.features + org.opennms.features.name-cutter + + + commons-lang + commons-lang + + + + + + commons-beanutils + commons-beanutils + + + + + + junit + junit + test + + + org.opennms.core.test-api + org.opennms.core.test-api.lib + test + + + + \ No newline at end of file diff --git a/features/mib-compiler-rest/parser/src/main/java/org/opennms/features/mibcompiler/api/MibParser.java b/features/mib-compiler-rest/parser/src/main/java/org/opennms/features/mibcompiler/api/MibParser.java new file mode 100644 index 000000000000..6e482e89e996 --- /dev/null +++ b/features/mib-compiler-rest/parser/src/main/java/org/opennms/features/mibcompiler/api/MibParser.java @@ -0,0 +1,96 @@ +/* + * Licensed to The OpenNMS Group, Inc (TOG) under one or more + * contributor license agreements. See the LICENSE.md file + * distributed with this work for additional information + * regarding copyright ownership. + * + * TOG licenses this file to You under the GNU Affero General + * Public License Version 3 (the "License") or (at your option) + * any later version. You may not use this file except in + * compliance with the License. You may obtain a copy of the + * License at: + * + * https://www.gnu.org/licenses/agpl-3.0.txt + * + * 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.opennms.features.mibcompiler.api; + +import java.io.File; +import java.util.List; + +import org.opennms.netmgt.config.datacollection.DatacollectionGroup; +import org.opennms.netmgt.model.PrefabGraph; +import org.opennms.netmgt.xml.eventconf.Events; + +/** + * The Interface MibParser. + * + * @author Alejandro Galue + */ +public interface MibParser { + + /** + * Sets the MIB directory. + * + * @param mibDirectory the MIB directory + */ + void setMibDirectory(File mibDirectory); + + /** + * Parses the MIB. + * + * @param mibFile the MIB file + * @return true, if successful + */ + boolean parseMib(File mibFile); + + /** + * Gets the formatted errors. + * + * @return the formatted errors + */ + String getFormattedErrors(); + + /** + * Gets the missing dependencies. + * + * @return the missing dependencies + */ + List getMissingDependencies(); + + /** + * Gets the MIB name. + * + * @return the MIB name. + */ + String getMibName(); + + /** + * Gets the event list. + * + * @param ueibase the UEI base + * @return the event list + */ + Events getEvents(String ueibase); + + /** + * Gets the data collection. + * + * @return the data collection group + */ + DatacollectionGroup getDataCollection(); + + /** + * Gets the prefab graph templates. + * + * @return the prefab graph templates. + */ + List getPrefabGraphs(); + +} diff --git a/features/mib-compiler-rest/parser/src/main/java/org/opennms/features/mibcompiler/services/JsmiMibParser.java b/features/mib-compiler-rest/parser/src/main/java/org/opennms/features/mibcompiler/services/JsmiMibParser.java new file mode 100644 index 000000000000..8941b393379e --- /dev/null +++ b/features/mib-compiler-rest/parser/src/main/java/org/opennms/features/mibcompiler/services/JsmiMibParser.java @@ -0,0 +1,779 @@ +/* + * Licensed to The OpenNMS Group, Inc (TOG) under one or more + * contributor license agreements. See the LICENSE.md file + * distributed with this work for additional information + * regarding copyright ownership. + * + * TOG licenses this file to You under the GNU Affero General + * Public License Version 3 (the "License") or (at your option) + * any later version. You may not use this file except in + * compliance with the License. You may obtain a copy of the + * License at: + * + * https://www.gnu.org/licenses/agpl-3.0.txt + * + * 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.opennms.features.mibcompiler.services; + +import java.io.File; +import java.io.Serializable; +import java.math.BigInteger; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.jsmiparser.parser.SmiDefaultParser; +import org.jsmiparser.smi.Notification; +import org.jsmiparser.smi.SmiMib; +import org.jsmiparser.smi.SmiModule; +import org.jsmiparser.smi.SmiNamedNumber; +import org.jsmiparser.smi.SmiNotificationType; +import org.jsmiparser.smi.SmiPrimitiveType; +import org.jsmiparser.smi.SmiRow; +import org.jsmiparser.smi.SmiTrapType; +import org.jsmiparser.smi.SmiType; +import org.jsmiparser.smi.SmiVariable; +import org.opennms.features.mibcompiler.api.MibParser; +import org.opennms.features.namecutter.NameCutter; +import org.opennms.netmgt.collection.support.IndexStorageStrategy; +import org.opennms.netmgt.config.datacollection.DatacollectionGroup; +import org.opennms.netmgt.config.datacollection.Group; +import org.opennms.netmgt.config.datacollection.MibObj; +import org.opennms.netmgt.config.datacollection.PersistenceSelectorStrategy; +import org.opennms.netmgt.config.datacollection.ResourceType; +import org.opennms.netmgt.config.datacollection.StorageStrategy; +import org.opennms.netmgt.model.OnmsSeverity; +import org.opennms.netmgt.model.PrefabGraph; +import org.opennms.netmgt.xml.eventconf.Decode; +import org.opennms.netmgt.xml.eventconf.Event; +import org.opennms.netmgt.xml.eventconf.Events; +import org.opennms.netmgt.xml.eventconf.LogDestType; +import org.opennms.netmgt.xml.eventconf.Logmsg; +import org.opennms.netmgt.xml.eventconf.Mask; +import org.opennms.netmgt.xml.eventconf.Maskelement; +import org.opennms.netmgt.xml.eventconf.Varbindsdecode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * JSMIParser implementation of the interface MibParser. + * + * @author Alejandro Galue + */ +@SuppressWarnings("serial") +public class JsmiMibParser implements MibParser, Serializable { + + /** The Constant LOG. */ + private static final Logger LOG = LoggerFactory.getLogger(JsmiMibParser.class); + + /** The Constant MIB_SUFFIXES. */ + private static final String[] MIB_SUFFIXES = new String[] { "", ".txt", ".mib", ".my" }; + + /** The Constant TRAP_OID_PATTERN. */ + private static final Pattern TRAP_OID_PATTERN = Pattern.compile("(.*)\\.(\\d+)$"); + + /** The MIB directory. */ + private File mibDirectory; + + /** The parser. */ + private SmiDefaultParser parser; + + /** The module. */ + private SmiModule module; + + /** The error handler. */ + private OnmsProblemEventHandler errorHandler; + + /** The missing dependencies. */ + private List missingDependencies = new ArrayList<>(); + + private static final String MISSING_DESCR = "MIB Object is missing the description field"; + + /** + * Instantiates a new JLIBSMI MIB parser. + */ + public JsmiMibParser() { + parser = new SmiDefaultParser(); + errorHandler = new OnmsProblemEventHandler(parser); + } + + /* (non-Javadoc) + * @see org.opennms.features.vaadin.mibcompiler.MibParser#setMibDirectory(java.io.File) + */ + @Override + public void setMibDirectory(File mibDirectory) { + this.mibDirectory = mibDirectory; + } + + /* (non-Javadoc) + * @see org.opennms.features.vaadin.mibcompiler.MibParser#parseMib(java.io.File) + */ + @Override + public boolean parseMib(File mibFile) { + // Validate MIB Directory + if (mibDirectory == null || !mibDirectory.isDirectory()) { + errorHandler.addError("MIB directory has not been set."); + return false; + } + + // Reset error handler and dependencies tracker + missingDependencies.clear(); + + // Set UP the MIB Queue MIB to be parsed + List queue = new ArrayList<>(); + parser.getFileParserPhase().setInputUrls(queue); + + // Create a cache of filenames to do case-insensitive lookups + final Map mibDirectoryFiles = new HashMap(); + for (final File file : mibDirectory.listFiles()) { + mibDirectoryFiles.put(file.getName().toLowerCase(), file); + } + + // Parse MIB + LOG.debug("Parsing {}", mibFile.getAbsolutePath()); + SmiMib mib = null; + addFileToQueue(queue, mibFile); + while (true) { + errorHandler.reset(); + try { + mib = parser.parse(); + } catch (Exception e) { + LOG.error("Can't compile {}", mibFile, e); + errorHandler.addError(e.getMessage()); + } + if (errorHandler.isOk()) { + break; + } else { + List dependencies = errorHandler.getDependencies(); + if (dependencies.isEmpty()) // No dependencies, everything is fine. + break; + missingDependencies.addAll(dependencies); + if (!addDependencyToQueue(queue, mibDirectoryFiles)) + break; + } + } + if (errorHandler.isNotOk() || mib == null) // There are still non-dependency related problems. + return false; + + // Extracting the module from compiled MIB. + LOG.info("The MIB {} has been parsed successfully.", mibFile.getAbsolutePath()); + module = getModule(mib, mibFile); + return module != null; + } + + /* (non-Javadoc) + * @see org.opennms.features.vaadin.mibcompiler.MibParser#getFormattedErrors() + */ + @Override + public String getFormattedErrors() { + return errorHandler.getMessages(); + } + + /* (non-Javadoc) + * @see org.opennms.features.vaadin.mibcompiler.MibParser#getMissingDependencies() + */ + @Override + public List getMissingDependencies() { + return missingDependencies; + } + + /* (non-Javadoc) + * @see org.opennms.features.vaadin.mibcompiler.services.MibParser#getMibName() + */ + @Override + public String getMibName() { + return module.getId(); + } + + /* (non-Javadoc) + * @see org.opennms.features.vaadin.mibcompiler.services.MibParser#getEvents(java.lang.String) + */ + @Override + public Events getEvents(String ueibase) { + if (module == null) { + return null; + } + LOG.info("Generating events for {} using the following UEI Base: {}", module.getId(), ueibase); + try { + return convertMibToEvents(module, ueibase); + } catch (Throwable e) { + String errors = e.getMessage(); + if (errors == null || errors.trim().equals("")) + errors = "An unknown error accured when generating events objects from the MIB " + module.getId(); + LOG.error("Event parsing error: {}", errors, e); + errorHandler.addError(errors); + return null; + } + } + + /* (non-Javadoc) + * @see org.opennms.features.vaadin.mibcompiler.api.MibParser#getDataCollection() + */ + @Override + public DatacollectionGroup getDataCollection() { + if (module == null) { + return null; + } + LOG.info("Generating data collection configuration for {}", module.getId()); + DatacollectionGroup dcGroup = new DatacollectionGroup(); + dcGroup.setName(module.getId()); + NameCutter cutter = new NameCutter(); + try { + for (SmiVariable v : module.getVariables()) { + String groupName = getGroupName(v); + String resourceType = getResourceType(v); + Group group = getGroup(dcGroup, groupName, resourceType); + String typeName = getMetricType(v.getType()); // FIXME what if it is not a primitive type, like in ENTITY-SENSOR-MIB ? + if (typeName != null) { + String alias = cutter.trimByCamelCase(v.getId(), 19); // RRDtool DS size restriction. + MibObj mibObj = new MibObj(); + mibObj.setOid('.' + v.getOidStr()); + mibObj.setInstance(resourceType == null ? "0" : resourceType); + mibObj.setAlias(alias); + mibObj.setType(typeName); + group.addMibObj(mibObj); + if (typeName.equals("string") && resourceType != null) { + for (ResourceType rs : dcGroup.getResourceTypes()) { + if (rs.getName().equals(resourceType) && rs.getResourceLabel().equals("${index}")) { + rs.setResourceLabel("${" + v.getId() + "} (${index})"); + } + } + } + } + } + } catch (Throwable e) { + String errors = e.getMessage(); + if (errors == null || errors.trim().equals("")) + errors = "An unknown error accured when generating data collection objects from the MIB " + module.getId(); + LOG.error("Data Collection parsing error: {}", errors, e); + errorHandler.addError(errors); + return null; + } + return dcGroup; + } + + + /* (non-Javadoc) + * @see org.opennms.features.vaadin.mibcompiler.api.MibParser#getPrefabGraphs() + */ + @Override + public List getPrefabGraphs() { + if (module == null) { + return null; + } + final String color = System.getProperty("org.opennms.snmp.mib-compiler.default-graph-template.color", "#00ccff"); + List graphs = new ArrayList<>(); + LOG.info("Generating graph templates for {}", module.getId()); + NameCutter cutter = new NameCutter(); + String name = ""; + try { + for (SmiVariable v : module.getVariables()) { + String groupName = getGroupName(v); + String resourceType = getResourceType(v); + if (resourceType == null) + resourceType = "nodeSnmp"; + String typeName = getMetricType(v.getType()); + if (v.getId().contains("Index")) { // Treat SNMP Indexes as strings. + typeName = "string"; + } + int order = 1; + if (typeName != null && !typeName.toLowerCase().contains("string")) { + name = groupName + '.' + v.getId(); + String title = getMibName() + "::" + groupName + "::" + v.getId(); + String alias = cutter.trimByCamelCase(v.getId(), 19); // RRDtool DS size restriction. + String descr = MISSING_DESCR; + if (v.getDescription() != null) { // missing descriptions are a source of pain; don't NPE, just work. + descr = v.getDescription().replaceAll("[\n\r]", "").replaceAll("\\s+", " "); + } + final StringBuilder sb = new StringBuilder(); + sb.append("--title=\"").append(title).append("\" \\\n"); + sb.append(" DEF:var={rrd1}:").append(alias).append(":AVERAGE \\\n"); + sb.append(" LINE1:var").append(color).append(":\"").append(v.getId()).append("\" \\\n"); + sb.append(" GPRINT:var:AVERAGE:\"Avg\\\\: %8.2lf %s\" \\\n"); + sb.append(" GPRINT:var:MIN:\"Min\\\\: %8.2lf %s\" \\\n"); + sb.append(" GPRINT:var:MAX:\"Max\\\\: %8.2lf %s\\\\n\""); + sb.append("\n\n"); + PrefabGraph graph = new PrefabGraph(name, title, new String[] { alias }, sb.toString(), new String[0], new String[0], order++, new String[] { resourceType }, descr, null, null, new String[0]); + graphs.add(graph); + } + } + } catch (Throwable e) { + String errors = e.getMessage(); + if (errors == null || errors.trim().equals("")) + errors = "An unknown error accured when generating graph templates from the MIB " + module.getId() + " at " + name + "."; // log the name so we know where to look when graph generation fails + LOG.error("Graph templates parsing error: {}", errors, e); + errorHandler.addError(errors); + return null; + } + return graphs; + } + + /** + * Gets the group name. + * + * @param var the SMI Variable + * @return the group name + */ + private String getGroupName(SmiVariable var) { + if (var.getNode().getParent().getSingleValue() instanceof SmiRow) { + return var.getNode().getParent().getParent().getSingleValue().getId(); + } + return var.getNode().getParent().getSingleValue().getId(); + } + + /** + * Gets the resource type. + * + * @param var the SMI Variable + * @return the resource type + */ + private String getResourceType(SmiVariable var) { + if (var.getNode().getParent().getSingleValue() instanceof SmiRow) { + return var.getNode().getParent().getSingleValue().getId(); + } + return null; + } + + /** + * Adds a file to the queue. + * + * @param queue the queue + * @param mibFile the MIB file + */ + private void addFileToQueue(List queue, File mibFile) { + try { + URL url = mibFile.toURI().toURL(); + if (!queue.contains(url)) { + LOG.debug("Adding {} to queue ", url); + queue.add(url); + } + } catch (Exception e) { + LOG.warn("Can't generate URL from {}", mibFile.getAbsolutePath()); + } + } + + /** + * Adds the dependency to the queue. + * + * @param queue the queue + * @param mibDirectoryFiles the mib directory files + * @return true, if successful + */ + private boolean addDependencyToQueue(final List queue, final Map mibDirectoryFiles) { + final List dependencies = new ArrayList(missingDependencies); + boolean ok = true; + for (String dependency : dependencies) { + boolean found = false; + for (String suffix : MIB_SUFFIXES) { + final String fileName = (dependency+suffix).toLowerCase(); + if (mibDirectoryFiles.containsKey(fileName)) { + File f = mibDirectoryFiles.get(fileName); + LOG.debug("Checking dependency file {}", f.getAbsolutePath()); + if (f.exists()) { + LOG.info("Adding dependency file {}", f.getAbsolutePath()); + addFileToQueue(queue, f); + missingDependencies.remove(dependency); + found = true; + break; + } + } + } + if (!found) { + LOG.warn("Couldn't find dependency {} on {}", dependency, mibDirectory); + ok = false; + } + } + return ok; + } + + /** + * Gets the module. + * + * @param mibObject the MIB object + * @param mibFile the MIB file + * @return the module + */ + private SmiModule getModule(SmiMib mibObject, File mibFile) { + for (SmiModule m : mibObject.getModules()) { + URL source = null; + try { + source = new URL(m.getIdToken().getLocation().getSource()); + } catch (Exception e) {} + if (source != null) { + try { + File srcFile = new File(source.toURI()); + if (srcFile.getAbsolutePath().equals(mibFile.getAbsolutePath())) { + return m; + } + } catch (Exception e) {} + } + } + LOG.error("Can't find the MIB module for " + mibFile); + errorHandler.addError("Can't find the MIB module for " + mibFile); + return null; + } + + /* + * Data Collection processing methods + */ + + /** + * Gets the metric type. + *

This should be consistent with NumericAttributeType and StringAttributeType.

+ *

For this reason the valid types are: counter, gauge, timeticks, integer, octetstring, string.

+ *

Any derivative is also valid, for example: Counter32, Integer64, etc...

+ * + * @param smiType the type + * @return the type + */ + private String getMetricType(final SmiType smiType) { + if(Objects.isNull(smiType)){ + return null; + } + if(!Objects.isNull(smiType.getId()) && smiType.getId().equalsIgnoreCase("CounterBasedGauge64")){ + return "gauge"; + } + final SmiPrimitiveType type = smiType.getPrimitiveType(); + if (Objects.isNull(type)) { + return null; + } + if (type.equals(SmiPrimitiveType.ENUM)) // ENUM are just informational elements. + return "string"; + if (type.equals(SmiPrimitiveType.OBJECT_IDENTIFIER)) // ObjectIdentifier will be treated as strings. + return "string"; + if (type.equals(SmiPrimitiveType.UNSIGNED_32)) // Unsigned32 will be treated as integer. + return "integer"; + if (type.equals(SmiPrimitiveType.IP_ADDRESS)) // IpAddress will be treated as strings. + return "string"; + if (type.equals(SmiPrimitiveType.BITS)) // BITS are extension of octet string. + return "octetstring"; + return type.toString().replaceAll("_", "").toLowerCase(); + } + + /** + * Gets the group. + * + * @param data the data collection group object + * @param groupName the group name + * @param resourceType the resource type + * @return the group + */ + protected Group getGroup(DatacollectionGroup data, String groupName, String resourceType) { + for (Group group : data.getGroups()) { + if (group.getName().equals(groupName)) + return group; + } + Group group = new Group(); + group.setName(groupName); + group.setIfType(resourceType == null ? "ignore" : "all"); + if (resourceType != null) { + ResourceType type = new ResourceType(); + type.setName(resourceType); + type.setLabel(resourceType); + type.setResourceLabel("${index}"); + type.setPersistenceSelectorStrategy(new PersistenceSelectorStrategy("org.opennms.netmgt.collection.support.PersistAllSelectorStrategy")); // To avoid requires opennms-services + type.setStorageStrategy(new StorageStrategy(IndexStorageStrategy.class.getName())); + data.addResourceType(type); + } + data.addGroup(group); + return group; + } + + /* + * Event processing methods + * + */ + + /** + * Convert MIB to events. + * + * @param module the module object + * @param ueibase the UEI base + * @return the events + */ + protected Events convertMibToEvents(SmiModule module, String ueibase) { + Events events = new Events(); + for (SmiNotificationType trap : module.getNotificationTypes()) { + events.addEvent(getTrapEvent(trap, ueibase)); + } + for (SmiTrapType trap : module.getTrapTypes()) { + events.addEvent(getTrapEvent(trap, ueibase)); + } + return events; + } + + /** + * Gets the trap event. + * + * @param trap the trap object + * @param ueibase the UEI base + * @return the trap event + */ + protected Event getTrapEvent(Notification trap, String ueibase) { + // Build default severity + String severity = OnmsSeverity.INDETERMINATE.toString(); + severity = severity.substring(0, 1).toUpperCase() + severity.substring(1).toLowerCase(); + // Set the event's UEI, event-label, logmsg, severity, and descr + Event evt = new Event(); + evt.setUei(getTrapEventUEI(trap, ueibase)); + evt.setEventLabel(getTrapEventLabel(trap)); + evt.setLogmsg(getTrapEventLogmsg(trap)); + evt.setSeverity(severity); + evt.setDescr(getTrapEventDescr(trap)); + List decode = getTrapVarbindsDecode(trap); + if (!decode.isEmpty()) { + evt.setVarbindsdecodes(decode); + } + evt.setMask(new Mask()); + // The "ID" mask element (trap enterprise) + addMaskElement(evt, "id", getTrapEnterprise(trap)); + // The "generic" mask element: hard-wired to enterprise-specific(6) + addMaskElement(evt, "generic", "6"); + // The "specific" mask element (trap specific-type) + addMaskElement(evt, "specific", getTrapSpecificType(trap)); + return evt; + } + + /** + * Gets the trap event UEI. + * + * @param trap the trap object + * @param ueibase the UEI base + * @return the trap event UEI + */ + protected String getTrapEventUEI(Notification trap, String ueibase) { + final StringBuilder buf = new StringBuilder(ueibase); + if (! ueibase.endsWith("/")) { + buf.append("/"); + } + buf.append(trap.getId()); + return buf.toString(); + } + + /** + * Gets the trap event label. + * + * @param trap the trap object + * @return the trap event label + */ + protected String getTrapEventLabel(Notification trap) { + final StringBuilder buf = new StringBuilder(); + buf.append(trap.getModule().getId()); + buf.append(" defined trap event: "); + buf.append(trap.getId()); + return buf.toString(); + } + + /** + * Gets the trap event LogMsg. + * + * @param trap the trap object + * @return the trap event LogMsg + */ + protected Logmsg getTrapEventLogmsg(Notification trap) { + Logmsg msg = new Logmsg(); + msg.setDest(LogDestType.LOGNDISPLAY); + final StringBuilder dbuf = new StringBuilder(); + dbuf.append("

"); + dbuf.append("\n"); + dbuf.append("\t").append(trap.getId()).append(" trap received\n"); + int vbNum = 1; + for (SmiVariable var : trap.getObjects()) { + dbuf.append("\t").append(var.getId()).append("=%parm[#").append(vbNum).append("]%\n"); + vbNum++; + } + if (dbuf.charAt(dbuf.length() - 1) == '\n') { + dbuf.deleteCharAt(dbuf.length() - 1); // delete the \n at the end + } + dbuf.append("

\n\t"); + msg.setContent(dbuf.toString()); + return msg; + } + + /** + * Gets the trap event description. + * + * @param trap the trap object + * @return the trap event description + */ + protected String getTrapEventDescr(Notification trap) { + String description = trap.getDescription(); + if (description == null) { + LOG.warn("The trap {} doesn't have a description field", trap.getOidStr()); + } + // FIXME There a lot of detail here (like removing the last \n) that can go away when we don't need to match mib2opennms exactly + final String descrEndingNewlines = description == null ? "No Description." : description.replaceAll("^", "\n

").replaceAll("$", "

\n"); + final StringBuffer dbuf = new StringBuffer(descrEndingNewlines); + if (dbuf.charAt(dbuf.length() - 1) == '\n') { + dbuf.deleteCharAt(dbuf.length() - 1); // delete the \n at the end + } + dbuf.append(""); + dbuf.append("\n"); + int vbNum = 1; + for (SmiVariable var : trap.getObjects()) { + dbuf.append("\t\n"); + vbNum++; + } + if (dbuf.charAt(dbuf.length() - 1) == '\n') { + dbuf.deleteCharAt(dbuf.length() - 1); // delete the \n at the end + } + dbuf.append("
\n\n\t").append(var.getId()); + dbuf.append("\n\t%parm[#").append(vbNum).append("]%;

"); + SmiPrimitiveType type = var.getType().getPrimitiveType(); + if (type.equals(SmiPrimitiveType.ENUM)) { + SortedMap map = new TreeMap(); + SmiType t = var.getType(); + while (t.getEnumValues() == null) { + t = t.getBaseType(); + } + List enumValues = t.getEnumValues(); + if (enumValues != null) { + for (SmiNamedNumber v : enumValues) { + map.put(v.getValue(), v.getId()); + } + } else { + // This is theoretically impossible, but in case of another bug in JSMIParser, better than an NPE. + map.put(new BigInteger("0"), "Unable to derive list of possible values."); + } + dbuf.append("\n"); + for (Entry entry : map.entrySet()) { + dbuf.append("\t\t").append(entry.getValue()).append("(").append(entry.getKey()).append(")\n"); + } + dbuf.append("\t"); + } + dbuf.append("

\n\t"); + return dbuf.toString(); + } + + /** + * Gets the trap varbinds decode. + * + * @param trap the trap object + * @return the trap varbinds decode + */ + protected List getTrapVarbindsDecode(Notification trap) { + Map decode = new LinkedHashMap(); + int vbNum = 1; + for (SmiVariable var : trap.getObjects()) { + String parmName = "parm[#" + vbNum + "]"; + SmiPrimitiveType type = var.getType().getPrimitiveType(); + if (type.equals(SmiPrimitiveType.ENUM)) { + SortedMap map = new TreeMap(); + SmiType t = var.getType(); + while (t.getEnumValues() == null) { + t = t.getBaseType(); + } + List enumValues = t.getEnumValues(); + if (enumValues != null) { + for (SmiNamedNumber v : enumValues) { + map.put(v.getValue(), v.getId()); + } + for (Entry entry : map.entrySet()) { + if (!decode.containsKey(parmName)) { + Varbindsdecode newVarbind = new Varbindsdecode(); + newVarbind.setParmid(parmName); + decode.put(newVarbind.getParmid(), newVarbind); + } + Decode d = new Decode(); + d.setVarbinddecodedstring(entry.getValue()); + d.setVarbindvalue(entry.getKey().toString()); + decode.get(parmName).addDecode(d); + } + } + } + vbNum++; + } + return new ArrayList(decode.values()); + } + + /** + * Gets the trap enterprise. + * + * @param trap the trap object + * @return the trap enterprise + */ + private String getTrapEnterprise(Notification trap) { + String trapOid = getMatcherForOid(getTrapOid(trap)).group(1); + + /* RFC3584 sec 3.2 (1) bullet 2 sub-bullet 1 states: + * + * "If the next-to-last sub-identifier of the snmpTrapOID value + * is zero, then the SNMPv1 enterprise SHALL be the SNMPv2 + * snmpTrapOID value with the last 2 sub-identifiers removed..." + * + * Issue SPC-592 boils down to the fact that we were not doing the above. + * + */ + + if (trapOid.endsWith(".0")) { + trapOid = trapOid.substring(0, trapOid.length() - 2); + } + return trapOid; + } + + /** + * Gets the trap specific type. + * + * @param trap the trap object + * @return the trap specific type + */ + private String getTrapSpecificType(Notification trap) { + return getMatcherForOid(getTrapOid(trap)).group(2); + } + + /** + * Gets the matcher for OID. + * + * @param trapOid the trap OID + * @return the matcher for OID + */ + private Matcher getMatcherForOid(String trapOid) { + Matcher m = TRAP_OID_PATTERN.matcher(trapOid); + if (!m.matches()) { + throw new IllegalStateException("Could not match the trap OID '" + trapOid + "' against '" + m.pattern().pattern() + "'"); + } + return m; + } + + /** + * Gets the trap OID. + * + * @param trap the trap object + * @return the trap OID + */ + private String getTrapOid(Notification trap) { + return '.' + trap.getOidStr(); + } + + /** + * Adds the mask element. + * + * @param event the event object + * @param name the name + * @param value the value + */ + private void addMaskElement(Event event, String name, String value) { + if (event.getMask() == null) { + throw new IllegalStateException("Event mask is not present, must have been set before this method was called"); + } + Maskelement me = new Maskelement(); + me.setMename(name); + me.addMevalue(value); + event.getMask().addMaskelement(me); + } +} diff --git a/features/mib-compiler-rest/parser/src/main/java/org/opennms/features/mibcompiler/services/OnmsProblemEventHandler.java b/features/mib-compiler-rest/parser/src/main/java/org/opennms/features/mibcompiler/services/OnmsProblemEventHandler.java new file mode 100644 index 000000000000..ef6ecb91915d --- /dev/null +++ b/features/mib-compiler-rest/parser/src/main/java/org/opennms/features/mibcompiler/services/OnmsProblemEventHandler.java @@ -0,0 +1,334 @@ +/* + * Licensed to The OpenNMS Group, Inc (TOG) under one or more + * contributor license agreements. See the LICENSE.md file + * distributed with this work for additional information + * regarding copyright ownership. + * + * TOG licenses this file to You under the GNU Affero General + * Public License Version 3 (the "License") or (at your option) + * any later version. You may not use this file except in + * compliance with the License. You may obtain a copy of the + * License at: + * + * https://www.gnu.org/licenses/agpl-3.0.txt + * + * 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.opennms.features.mibcompiler.services; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.jsmiparser.parser.SmiDefaultParser; +import org.jsmiparser.util.location.Location; +import org.jsmiparser.util.problem.DefaultProblemReporterFactory; +import org.jsmiparser.util.problem.ProblemEvent; +import org.jsmiparser.util.problem.ProblemEventHandler; +import org.jsmiparser.util.problem.ProblemReporterFactory; +import org.jsmiparser.util.problem.annotations.ProblemSeverity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The Implementation of the ProblemEventHandler interface for OpenNMS. + * + * @author Alejandro Galue + */ +public class OnmsProblemEventHandler implements ProblemEventHandler { + + /** The Constant LOG. */ + private static final Logger LOG = LoggerFactory.getLogger(OnmsProblemEventHandler.class); + + /** The Constant FILE_PREFIX. */ + private static final String FILE_PREFIX_LINUX = "file://"; + + /** The Constant FILE_PREFIX. */ + private static final String FILE_PREFIX_WINDOWS = "file:///"; + + /** The Constant DEPENDENCY_PATERN. */ + private static final Pattern DEPENDENCY_PATERN = Pattern.compile("Cannot find module ([^,]+)", Pattern.MULTILINE); + + /** The severity counters. */ + private int[] m_severityCounters = new int[ProblemSeverity.values().length]; + + /** The total counter. */ + private int m_totalCounter; + + /** The output stream. */ + private ByteArrayOutputStream m_outputStream = new ByteArrayOutputStream(); + + /** The print stream. */ + private PrintStream m_out; + + /** + * The Class Source. + */ + private static class Source { + public File file; + public int row; + public int column; + } + + /** + * Instantiates a new OpenNMS problem event handler. + * + * @param parser the parser + */ + public OnmsProblemEventHandler(SmiDefaultParser parser) { + m_out = new PrintStream(m_outputStream); + ProblemReporterFactory problemReporterFactory = new DefaultProblemReporterFactory(getClass().getClassLoader(), this); + parser.setProblemReporterFactory(problemReporterFactory); + } + + /* (non-Javadoc) + * @see org.jsmiparser.util.problem.ProblemEventHandler#handle(org.jsmiparser.util.problem.ProblemEvent) + */ + @Override + public void handle(ProblemEvent event) { + m_severityCounters[event.getSeverity().ordinal()]++; + m_totalCounter++; + print(m_out, event.getSeverity().toString(), event.getLocation(), event.getLocalizedMessage()); + } + + /* (non-Javadoc) + * @see org.jsmiparser.util.problem.ProblemEventHandler#isOk() + */ + @Override + public boolean isOk() { + for (int i = 0; i < m_severityCounters.length; i++) { + if (i >= ProblemSeverity.ERROR.ordinal()) { + int severityCounter = m_severityCounters[i]; + if (severityCounter > 0) { + return false; + } + } + } + return true; + } + + /* (non-Javadoc) + * @see org.jsmiparser.util.problem.ProblemEventHandler#isNotOk() + */ + @Override + public boolean isNotOk() { + return !isOk(); + } + + /* (non-Javadoc) + * @see org.jsmiparser.util.problem.ProblemEventHandler#getSeverityCount(org.jsmiparser.util.problem.annotations.ProblemSeverity) + */ + @Override + public int getSeverityCount(ProblemSeverity severity) { + return m_severityCounters[severity.ordinal()]; + } + + /* (non-Javadoc) + * @see org.jsmiparser.util.problem.ProblemEventHandler#getTotalCount() + */ + @Override + public int getTotalCount() { + return m_totalCounter; + } + + /** + * Gets the prefix. + *

The URL prefix depending on the host Operating System

+ * + * @return the prefix + */ + private String getPrefix() { + return File.separatorChar == '\\' ? FILE_PREFIX_WINDOWS : FILE_PREFIX_LINUX; + } + + /** + * Gets the MIB from source. + * + * @param source the source + * @return the MIB from source + */ + private String getMibFromSource(final String source) { + return getMibFromSource(source, File.separatorChar); + } + + /** + * Gets the MIB from source. + * + * @param source the source + * @param separatorChar the separatoe character + * @return the MIB from source + */ + String getMibFromSource(final String source, final char separatorChar) { + final String arr[] = source.split(":"); + + if (separatorChar == '\\') { + if (arr.length == 5) { + return arr[4]; + } + } else { + if (arr.length == 4) { + return arr[3]; + } + } + + return null; + } + + /** + * Prints the error message. + * + * @param stream the stream + * @param severity the severity + * @param location the location + * @param localizedMessage the localized message + */ + private void print(final PrintStream stream, final String severity, final Location location, final String localizedMessage) { + LOG.debug("[{}] Location: {}, Message: {}", severity, location, localizedMessage); + int n = localizedMessage.indexOf(getPrefix()); + if (n > 0) { + final String source = localizedMessage.substring(n).replaceAll(getPrefix(), ""); + final String mibFromSource = getMibFromSource(source); + + final String message; + + if (mibFromSource == null) { + message = localizedMessage; + } else { + message = localizedMessage.substring(0, n) + getMibFromSource(source); + } + + processMessage(stream, severity, source, message); + } else { + if (location == null) { + stream.println(severity + ": " + localizedMessage); + } else { + final String source = location.toString().replaceAll(getPrefix(), ""); + final String message = localizedMessage; + processMessage(stream, severity, source, message); + } + } + } + + /** + * Gets the source data. + *

Analyzes the source string and build the data source depending on the host Operating System

+ * + * @param strSource the string source + * @return the source data + */ + private Source getSourceData(String strSource) { + String[] data = strSource.split(":"); + Source src = new Source(); + int rowIdx = 1; + int colIdx = 2; + if (File.separatorChar == '\\') { // Windows + src.file = new File(data[0] + ':' + data[1]); + rowIdx = 2; + colIdx = 3; + } else { // Linux + src.file = new File(data[0]); + } + try { + src.row = Integer.parseInt(data[rowIdx]); + } catch (Exception e) { + src.row = -1; + } + try { + src.column = Integer.parseInt(data[colIdx]); + } catch (Exception e) { + src.column = -1; + } + return src; + } + + /** + * Process the error message. + * + * @param stream the stream + * @param severity the severity + * @param source the location source + * @param message the message + */ + // TODO This implementation might be expensive. + private void processMessage(final PrintStream stream, final String severity, final String source, final String message) { + final Source src = getSourceData(source); + if (src.row != -1 && src.column != -1) { + stream.println(severity + ": " + message + ", Source: " + src.file.getName() + ", Row: " + src.row + ", Col: " + src.column); + } else { + stream.println(severity + ": " + message + ", Source: " + src.file.getName()); + } + try { + if (!src.file.exists()) { + LOG.warn("File {} doesn't exist", src.file); + return; + } + final FileInputStream fs = new FileInputStream(src.file); + final BufferedReader br = new BufferedReader(new InputStreamReader(fs)); + for (int i = 1; i < src.row; i++) + br.readLine(); + stream.println(br.readLine()); + br.close(); + stream.println(String.format("%" + src.column + "s", "^")); + } catch (Exception e) { + LOG.warn("Can't retrieve line {} from file {}", src.row, src.file); + } + } + + /** + * Reset. + */ + public void reset() { + m_outputStream.reset(); + m_severityCounters = new int[ProblemSeverity.values().length]; + m_totalCounter = 0; + } + + /** + * Gets the dependencies. + * + * @return the dependencies + */ + public List getDependencies() { + List dependencies = new ArrayList<>(); + if (m_outputStream.size() > 0) { + Matcher m = DEPENDENCY_PATERN.matcher(m_outputStream.toString()); + while (m.find()) { + final String dep = m.group(1); + if (!dependencies.contains(dep)) + dependencies.add(dep); + } + } + return dependencies; + } + + /** + * Gets the messages. + * + * @return the messages + */ + public String getMessages() { + return m_outputStream.size() > 0 ? m_outputStream.toString() : null; + } + + /** + * Adds a new error message. + * + * @param errorMessage the error message + */ + public void addError(String errorMessage) { + m_out.println(errorMessage); + } + +} diff --git a/features/mib-compiler-rest/parser/src/main/java/org/opennms/features/mibcompiler/services/PrefabGraphDumper.java b/features/mib-compiler-rest/parser/src/main/java/org/opennms/features/mibcompiler/services/PrefabGraphDumper.java new file mode 100644 index 000000000000..da607d15111c --- /dev/null +++ b/features/mib-compiler-rest/parser/src/main/java/org/opennms/features/mibcompiler/services/PrefabGraphDumper.java @@ -0,0 +1,63 @@ +/* + * Licensed to The OpenNMS Group, Inc (TOG) under one or more + * contributor license agreements. See the LICENSE.md file + * distributed with this work for additional information + * regarding copyright ownership. + * + * TOG licenses this file to You under the GNU Affero General + * Public License Version 3 (the "License") or (at your option) + * any later version. You may not use this file except in + * compliance with the License. You may obtain a copy of the + * License at: + * + * https://www.gnu.org/licenses/agpl-3.0.txt + * + * 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.opennms.features.mibcompiler.services; + +import java.io.IOException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang.StringUtils; +import org.opennms.netmgt.model.PrefabGraph; + +/** + * The Class PrefabGraphDumper. + * + * @author Alejandro Galue + */ +public class PrefabGraphDumper { + + /** + * Dump. + *

This only cover the main variables from the PrefabGraph.

+ * + * @param graphs the graphs + * @param writer the writer + * @throws IOException Signals that an I/O exception has occurred. + */ + public void dump(List graphs, Writer writer) throws IOException { + List templates = new ArrayList<>(); + final StringBuilder sb = new StringBuilder(); + for (PrefabGraph graph : graphs) { + String name = "report." + graph.getName(); + templates.add(graph.getName()); + sb.append(name).append(".name=").append(graph.getTitle()).append("\n"); + sb.append(name).append(".columns=").append(StringUtils.join(graph.getColumns(), ",")).append("\n"); + sb.append(name).append(".type=").append(StringUtils.join(graph.getTypes(), ",")).append("\n"); + sb.append(name).append(".description=").append(graph.getDescription()).append("\n"); + sb.append(name).append(".command=").append(graph.getCommand()); + } + writer.write("reports=" + StringUtils.join(templates, ", \\\n") + "\n\n"); + writer.write(sb.toString()); + } + +} diff --git a/features/mib-compiler-rest/parser/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/features/mib-compiler-rest/parser/src/main/resources/OSGI-INF/blueprint/blueprint.xml new file mode 100644 index 000000000000..9ab61de06f96 --- /dev/null +++ b/features/mib-compiler-rest/parser/src/main/resources/OSGI-INF/blueprint/blueprint.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/features/mib-compiler-rest/pom.xml b/features/mib-compiler-rest/pom.xml new file mode 100644 index 000000000000..6ba3cf6dbc34 --- /dev/null +++ b/features/mib-compiler-rest/pom.xml @@ -0,0 +1,22 @@ + + + + org.opennms.features + org.opennms + 36.0.0-SNAPSHOT + + 4.0.0 + + org.opennms.features + org.opennms.features.mib-compiler-rest + pom + OpenNMS :: Features :: Mib Compiler Rest + + + parser + api + + + \ No newline at end of file diff --git a/features/pom.xml b/features/pom.xml index 4a34a6132b5c..e3625e5b358f 100644 --- a/features/pom.xml +++ b/features/pom.xml @@ -195,5 +195,6 @@ grpc elastic + mib-compiler-rest