diff --git a/features/mib-compiler/rest/pom.xml b/features/mib-compiler/rest/pom.xml index 45db9f4b6898..099624b91e79 100644 --- a/features/mib-compiler/rest/pom.xml +++ b/features/mib-compiler/rest/pom.xml @@ -49,7 +49,27 @@ ${project.version} provided + + org.opennms + opennms-dao + provided + + + org.opennms.core.test-api + org.opennms.core.test-api.lib + test + + + org.opennms.core.test-api + org.opennms.core.test-api.db + test + + + org.opennms.core.test-api + org.opennms.core.test-api.services + test + junit junit diff --git a/features/mib-compiler/rest/src/main/java/org/opennms/features/mibcompiler/rest/MibCompilerRestService.java b/features/mib-compiler/rest/src/main/java/org/opennms/features/mibcompiler/rest/MibCompilerRestService.java index e17ee1646804..5e2ff4bab43d 100644 --- a/features/mib-compiler/rest/src/main/java/org/opennms/features/mibcompiler/rest/MibCompilerRestService.java +++ b/features/mib-compiler/rest/src/main/java/org/opennms/features/mibcompiler/rest/MibCompilerRestService.java @@ -21,11 +21,16 @@ */ package org.opennms.features.mibcompiler.rest; +import org.opennms.features.mibcompiler.rest.model.MibCompilerGenerateEventsRequest; + +import javax.ws.rs.Produces; import javax.ws.rs.Consumes; +import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; -import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; +import javax.ws.rs.PathParam; +import javax.ws.rs.DELETE; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; @@ -36,10 +41,45 @@ public interface MibCompilerRestService { @POST @Path("/upload") @Consumes(MediaType.APPLICATION_OCTET_STREAM) - Response uploadMib(byte[] mibContent, @QueryParam("filename") String filename); + @Produces(MediaType.APPLICATION_JSON) + Response uploadMib(byte[] mibContent, @QueryParam("filename") String filename) throws Exception; @POST @Path("/compile") @Consumes(MediaType.APPLICATION_JSON) - Response compileMib(@QueryParam("name") String name); + @Produces(MediaType.APPLICATION_JSON) + Response compileMib(@QueryParam("name") String name) throws Exception; + + @GET + @Path("/files") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + Response listPendingAndCompiledFiles() throws Exception; + + @DELETE + @Path("/files/{location}/{fileName:.+}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + Response deleteFile(@PathParam("location") String location, + @PathParam("fileName") String fileName) throws Exception; + + @GET + @Path("/files/{location}/{fileName:.+}/text") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + Response getFileText(@PathParam("location") String location, + @PathParam("fileName") String fileName) throws Exception; + + @POST + @Path("/files/pending/text") + @Consumes(MediaType.APPLICATION_OCTET_STREAM) + @Produces(MediaType.APPLICATION_JSON) + Response setFileText(@QueryParam("fileName") String fileName, + byte[] mibContent) throws Exception; + + @POST + @Path("/generate-events") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + Response generateEvents(MibCompilerGenerateEventsRequest request) throws Exception; } diff --git a/features/mib-compiler/rest/src/main/java/org/opennms/features/mibcompiler/rest/internal/MibCompilerRestServiceImpl.java b/features/mib-compiler/rest/src/main/java/org/opennms/features/mibcompiler/rest/internal/MibCompilerRestServiceImpl.java index b28234647e9e..0744bb7428b0 100644 --- a/features/mib-compiler/rest/src/main/java/org/opennms/features/mibcompiler/rest/internal/MibCompilerRestServiceImpl.java +++ b/features/mib-compiler/rest/src/main/java/org/opennms/features/mibcompiler/rest/internal/MibCompilerRestServiceImpl.java @@ -21,36 +21,568 @@ */ package org.opennms.features.mibcompiler.rest.internal; +import org.apache.commons.lang.StringUtils; import org.opennms.features.mibcompiler.api.MibParser; import org.opennms.features.mibcompiler.rest.MibCompilerRestService; +import org.opennms.features.mibcompiler.rest.model.MibCompilerFileText; +import org.opennms.features.mibcompiler.rest.model.MibCompilerGenerateEventsRequest; +import org.opennms.netmgt.config.api.EventConfDao; +import org.opennms.netmgt.dao.api.EventConfEventDao; +import org.opennms.netmgt.dao.api.EventConfSourceDao; +import org.opennms.netmgt.dao.support.EventConfServiceHelper; +import org.opennms.netmgt.model.EventConfSource; +import org.opennms.netmgt.model.events.EventConfSourceMetadataDto; +import org.opennms.netmgt.xml.eventconf.Events; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.transaction.support.TransactionOperations; +import javax.annotation.PreDestroy; import javax.ws.rs.core.Response; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.List; +import java.util.LinkedHashMap; +import java.util.Date; +import java.util.Optional; +import java.util.concurrent.ExecutorService; public class MibCompilerRestServiceImpl implements MibCompilerRestService { private static final Logger LOG = LoggerFactory.getLogger(MibCompilerRestServiceImpl.class); + private static final String UNKNOWN_FILENAME = "unknown"; + private static final int MAX_FILENAME_LENGTH = 255; + private final MibParser mibParser; + private final EventConfSourceDao eventConfSourceDao; + private final EventConfEventDao eventConfEventDao; + private final EventConfDao eventConfDao; + private final TransactionOperations operations; + + private final ExecutorService eventConfExecutor = + EventConfServiceHelper.createEventConfExecutor("load-eventConf-%d"); - public MibCompilerRestServiceImpl(MibParser mibParser) { - this.mibParser = mibParser; + public MibCompilerRestServiceImpl( + final MibParser mibParser, EventConfSourceDao eventConfSourceDao, EventConfEventDao eventConfEventDao, EventConfDao eventConfDao, TransactionOperations operations) { + this.mibParser = Objects.requireNonNull(mibParser, "mibParser must not be null"); + this.eventConfSourceDao = eventConfSourceDao; + this.eventConfEventDao = eventConfEventDao; + this.eventConfDao = eventConfDao; + this.operations = operations; + + this.mibParser.setMibDirectory(MibCompilerServiceUtil.getCompiledDir()); } @Override - public Response uploadMib(byte[] mibContent, String filename) { - // TODO: implement MIB upload to pending directory - return Response.status(Response.Status.SERVICE_UNAVAILABLE) - .entity("{\"error\": \"Not yet implemented\"}") - .build(); + public Response uploadMib(final byte[] mibContent, final String filename) throws Exception { + final List> successList = new ArrayList<>(); + final List> errorList = new ArrayList<>(); + + final String originalFilename = safeFilename(filename); + final String baseName = MibCompilerServiceUtil.stripPathAndExtension(originalFilename); + + if (isBlank(baseName)) { + LOG.warn("Skipping upload with invalid filename: {}", originalFilename); + + errorList.add(error(originalFilename, baseName, + "Invalid filename; cannot derive base name.")); + + return buildResponse(Response.Status.BAD_REQUEST, successList, errorList); + } + + if (mibContent == null || mibContent.length == 0) { + errorList.add(error(originalFilename, baseName, "Empty MIB content.")); + return buildResponse(Response.Status.BAD_REQUEST, successList, errorList); + } + + if (MibCompilerServiceUtil.baseNameExistsInPendingOrCompiled(baseName)) { + errorList.add(error(originalFilename, baseName, + "A MIB with the same base name already exists in pending/ or compiled/.")); + return buildResponse(Response.Status.CONFLICT, successList, errorList); + } + + final String ext = MibCompilerServiceUtil.normalizeExtension( + getExtensionOrDefault(originalFilename), + MibCompilerServiceUtil.DEFAULT_MIB_EXTENSION + ); + + try (InputStream in = new ByteArrayInputStream(mibContent)) { + final File saved = MibCompilerServiceUtil.saveToPending(baseName, ext, in); + + final Map success = new LinkedHashMap<>(); + success.put("filename", originalFilename); + success.put("savedAs", saved.getName()); + success.put("success", Boolean.TRUE); + successList.add(success); + + return buildResponse(Response.Status.CREATED, successList, errorList); + + } catch (Exception e) { + LOG.warn("Failed to save uploaded MIB file '{}' (basename='{}') to pending.", originalFilename, baseName, e); + + errorList.add(errorWithException(originalFilename, baseName, e)); + return buildResponse(Response.Status.INTERNAL_SERVER_ERROR, successList, errorList); + } + } + + @Override + public Response compileMib(final String name) throws Exception { + // 1) Validate request + final String baseName = MibCompilerServiceUtil.stripPathAndExtension(safeName(name)); + if (isBlank(baseName)) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "baseName must not be blank."); + response.put("mibName", safeName(name)); + return Response.status(Response.Status.BAD_REQUEST).entity(response).build(); + } + + final File pendingFile; + try { + pendingFile = MibCompilerServiceUtil.findPendingByBaseName(baseName); + } catch (IllegalStateException e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", e.getMessage()); + response.put("mibName", safeName(name)); + return Response.status(Response.Status.CONFLICT).entity(response).build(); + } + + if (pendingFile == null) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "No pending file found with base name '" + baseName + "'."); + response.put("mibName", safeName(name)); + return Response.status(Response.Status.NOT_FOUND).entity(response).build(); + } + + final boolean parsed = mibParser.parseMib(pendingFile); + if (!parsed) { + final var missingDeps = mibParser.getMissingDependencies(); + if (missingDeps != null && !missingDeps.isEmpty()) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "Missing dependencies: " + missingDeps); + response.put("mibName", safeName(name)); + response.put("missingDependencies", missingDeps); + return Response.status(Response.Status.CONFLICT).entity(response).build(); + } + + final String errors = mibParser.getFormattedErrors(); + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "MIB validation failed."); + response.put("mibName", safeName(name)); + response.put("errors", errors); + return Response.status(Response.Status.BAD_REQUEST).entity(response).build(); + } + + final File compiledFile; + try { + compiledFile = MibCompilerServiceUtil.movePendingToCompiled(pendingFile, baseName); + } catch (IllegalStateException e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", e.getMessage()); + response.put("mibName", safeName(name)); + return Response.status(Response.Status.CONFLICT).entity(response).build(); + } + + // 5) Success response + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "MIB compiled successfully."); + response.put("mibName", safeName(name)); + response.put("compiledFile", fileNameOnly(compiledFile)); + return Response.ok(response).build(); + } + @Override + public Response listPendingAndCompiledFiles() { + LOG.debug("REST request: list mib compiler files"); + + try { + final var files = MibCompilerServiceUtil.listPendingAndCompiledFiles(); + return Response.ok(files).build(); + } catch (java.io.IOException e) { + LOG.error("I/O error while listing mib compiler files", e); + return Response.status(Response.Status.SERVICE_UNAVAILABLE) // 503 + .entity("Unable to read mib directories.") + .build(); + } catch (Exception e) { + LOG.error("Unexpected error while listing mib compiler files", e); + return Response.serverError() + .entity("Unexpected error while listing mib compiler files.") + .build(); + } + } + + @Override + public Response deleteFile(String location, String fileName) { + LOG.debug("REST request: delete mib compiler file: location={}, fileName={}", location, fileName); + + try { + validateLocationAndFileName(location, fileName); + final boolean deleted = MibCompilerServiceUtil.deleteFile(location, fileName); + + if (!deleted) { + LOG.info("Mib compiler file not found for delete: location={}, fileName={}", location, fileName); + return Response.status(Response.Status.NOT_FOUND).build(); + } + + LOG.info("Mib compiler file deleted: location={}, fileName={}", location, fileName); + return Response.noContent().build(); + } catch (IllegalArgumentException e) { + LOG.warn("Delete mib compiler file failed (bad request): location={}, fileName={}, msg={}", + location, fileName, e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST) + .entity(e.getMessage()) + .build(); + } catch (IllegalStateException e) { + LOG.warn("Delete mib compiler file failed (conflict): location={}, fileName={}, msg={}", + location, fileName, e.getMessage(), e); + return Response.status(Response.Status.CONFLICT) + .entity(e.getMessage()) + .build(); + } catch (java.io.IOException e) { + LOG.error("Delete mib compiler file failed (I/O): location={}, fileName={}", location, fileName, e); + return Response.status(Response.Status.SERVICE_UNAVAILABLE) // 503 + .entity("Unable to delete file.") + .build(); + } catch (Exception e) { + LOG.error("Delete mib compiler file failed (unexpected): location={}, fileName={}", location, fileName, e); + return Response.serverError() + .entity("Unexpected error while deleting file.") + .build(); + } + } + + @Override + public Response getFileText(String location, String fileName) throws Exception { + try { + validateLocationAndFileName(location, fileName); + + final String contents = MibCompilerServiceUtil.readTextFile(location, fileName); + if (contents == null) { + LOG.info("Mib compiler file not found for text read: location={}, fileName={}", location, fileName); + return Response.status(Response.Status.NOT_FOUND).build(); + } + + final var dto = new MibCompilerFileText(fileName, location.toLowerCase(), contents); + return Response.ok(dto).build(); + } catch (IllegalArgumentException e) { + LOG.warn("Get file text failed (bad request): location={}, fileName={}, msg={}", location, fileName, e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST) + .entity(e.getMessage()) + .build(); + } catch (IllegalStateException e) { + LOG.warn("Get file text failed (conflict): location={}, fileName={}, msg={}", location, fileName, e.getMessage(), e); + return Response.status(Response.Status.CONFLICT) + .entity(e.getMessage()) + .build(); + } catch (java.io.IOException e) { + LOG.error("Get file text failed (I/O): location={}, fileName={}", location, fileName, e); + return Response.status(Response.Status.SERVICE_UNAVAILABLE) + .entity("Unable to read file.") + .build(); + } catch (Exception e) { + LOG.error("Get file text failed (unexpected): location={}, fileName={}", location, fileName, e); + return Response.serverError() + .entity("Unexpected error while reading file.") + .build(); + } } + @Override - public Response compileMib(String name) { - // TODO: implement MIB compilation - return Response.status(Response.Status.SERVICE_UNAVAILABLE) - .entity("{\"error\": \"Not yet implemented\"}") + public Response setFileText(final String fileName, final byte[] mibContent) { + final String location = "pending"; + LOG.debug("REST request: set mib compiler file text: location={}, fileName={}", location, fileName); + + try { + + validateLocationAndFileName(location, fileName); + + if (mibContent == null) { + LOG.warn("Set file text rejected: null mibContent (location={}, fileName={})", location, fileName); + return Response.status(Response.Status.BAD_REQUEST) + .entity("mibContent must not be null.") + .build(); + } + + MibCompilerServiceUtil.writeBinaryFile(location, fileName, mibContent); + + LOG.info("Set file text: updated successfully (location={}, fileName={})", location, fileName); + + return Response.ok("Text updated successfully").build(); + + } catch (IllegalArgumentException e) { + LOG.warn("Set file text failed (bad request): location={}, fileName={}, msg={}", location, fileName, e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST) + .entity(e.getMessage()) + .build(); + } catch (IllegalStateException e) { + LOG.warn("Set file text failed (conflict): location={}, fileName={}, msg={}", location, fileName, e.getMessage(), e); + return Response.status(Response.Status.CONFLICT) + .entity(e.getMessage()) + .build(); + } catch (java.io.IOException e) { + LOG.error("Set file text failed (I/O): location={}, fileName={}", location, fileName, e); + return Response.status(Response.Status.SERVICE_UNAVAILABLE) + .entity("Unable to write file.") + .build(); + } catch (Exception e) { + LOG.error("Set file text failed (unexpected): location={}, fileName={}", location, fileName, e); + return Response.serverError() + .entity("Unexpected error while writing file.") + .build(); + } + } + @Override + public Response generateEvents(final MibCompilerGenerateEventsRequest request) { + if (request == null) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("Request must not be null.") + .build(); + } + final String location = "compiled"; + String fileName = request.getName(); + LOG.debug("REST request: generate events: location={}, file={}", location, request != null ? request.getName() : null); + + try { + + if (fileName == null || fileName.trim().isEmpty()) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("mibFileName must not be null/empty.") + .build(); + } + + validateLocationAndFileName(location, fileName); + + final String ueiBase = request.getUeiBase(); + if (ueiBase == null || ueiBase.trim().isEmpty()) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("ueiBase must not be null/empty.") + .build(); + } + final boolean exists = MibCompilerServiceUtil.exists(location, fileName); + if (!exists) { + LOG.info("Generate events: file not found (location={}, fileName={})", location, fileName); + return Response.status(Response.Status.NOT_FOUND) + .entity("MIB file not found in compiled directory: " + fileName) + .build(); + } + + final File mibFile = MibCompilerServiceUtil.getFile(location, fileName); + + LOG.info("Parsing MIB before generating events (fileName={}, location={})", fileName, location); + + if (!mibParser.parseMib(mibFile)) { + final List dependencies = mibParser.getMissingDependencies(); + if (dependencies != null && !dependencies.isEmpty()) { + LOG.warn("Generate events rejected: missing dependencies (fileName={}, deps={})", fileName, dependencies); + return Response.status(Response.Status.CONFLICT) + .entity("Dependencies required: " + dependencies) + .build(); + } + + final String formattedErrors = mibParser.getFormattedErrors(); + LOG.warn("Generate events rejected: MIB parse errors (fileName={}, errors={})", fileName, formattedErrors); + return Response.status(Response.Status.BAD_REQUEST) + .entity(formattedErrors != null ? formattedErrors : "Problem found when compiling the MIB.") + .build(); + } + + final Events events = mibParser.getEvents(ueiBase); + if (events == null) { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity("MIB parsed successfully but event generation returned null.") + .build(); + } + + return operations.execute(status -> { + try { + final Date now = new Date(); + final int maxFileOrder = Optional.ofNullable(eventConfSourceDao.findMaxFileOrder()).orElse(0); + final int nextFileOrder = maxFileOrder + 1; + final EventConfSourceMetadataDto meta = + buildMetadata(fileName, events, nextFileOrder, now); + + final EventConfSource source = EventConfServiceHelper.createOrUpdateSource(eventConfSourceDao, meta); + + eventConfEventDao.deleteBySourceId(source.getId()); + EventConfServiceHelper.saveEvents(eventConfEventDao, source, events, meta.getUsername(), meta.getNow()); + EventConfServiceHelper.reloadEventsFromDBAsync(eventConfEventDao, eventConfDao, eventConfExecutor); + + final Map resp = new LinkedHashMap<>(); + resp.put("success", true); + resp.put("message", "Events generated successfully."); + resp.put("mibFile", fileName); + resp.put("sourceId", source.getId()); + return Response.ok(resp).build(); + } catch (Exception e) { + status.setRollbackOnly(); + LOG.error("Generate events failed during DB transaction", e); + return Response.serverError() + .entity("Unexpected error while generating events.") + .build(); + } + }); + + } catch (IllegalArgumentException e) { + LOG.warn("Generate events failed (bad request): location={}, fileName={}, msg={}", location, fileName, e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build(); + } catch (Exception e) { + LOG.error("Generate events failed (unexpected): location={}, fileName={}", location, fileName, e); + return Response.serverError() + .entity("Unexpected error while generating events.") + .build(); + } + } + + + private static Response buildResponse(final Response.Status status, + final List> successList, + final List> errorList) { + final Map payload = new LinkedHashMap<>(); + payload.put("success", successList); + payload.put("errors", errorList); + return Response.status(status).entity(payload).build(); + } + + private static Map error(final String filename, final String basename, final String message) { + final Map e = new LinkedHashMap<>(); + e.put("filename", filename); + e.put("basename", basename); + e.put("error", message); + return e; + } + + private static Map errorWithException(final String filename, final String basename, final Exception ex) { + final Map e = error(filename, basename, toDetailedErrorMessage(ex)); + e.put("exception", ex.getClass().getName()); + return e; + } + + private static boolean isBlank(final String s) { + if (s == null) return true; + for (int i = 0; i < s.length(); i++) { + if (!Character.isWhitespace(s.charAt(i))) { + return false; + } + } + return true; + } + + static String safeFilename(final String filename) { + if (filename == null) { + return UNKNOWN_FILENAME; + } + + String name = filename.trim(); + if (name.isEmpty()) { + return UNKNOWN_FILENAME; + } + + name = name.replace('\\', '/'); + final int lastSlash = name.lastIndexOf('/'); + if (lastSlash >= 0) { + name = name.substring(lastSlash + 1); + } + + name = name.trim(); + if (name.isEmpty() || ".".equals(name) || "..".equals(name)) { + return UNKNOWN_FILENAME; + } + + name = name.replaceAll("[\\p{Cntrl}\\\\/:*?\"<>|]+", "_"); + + name = name.replaceAll("^[\\s.]+", ""); + name = name.replaceAll("[\\s.]+$", ""); + + if (name.isEmpty()) { + return UNKNOWN_FILENAME; + } + + if (name.length() > MAX_FILENAME_LENGTH) { + name = name.substring(0, MAX_FILENAME_LENGTH); + } + + return name; + } + + private static String getExtensionOrDefault(final String filename) { + if (isBlank(filename)) { + return MibCompilerServiceUtil.DEFAULT_MIB_EXTENSION; + } + + final int dot = filename.lastIndexOf('.'); + if (dot > 0 && dot < filename.length() - 1) { + return filename.substring(dot); + } + return MibCompilerServiceUtil.DEFAULT_MIB_EXTENSION; + } + + private static String toDetailedErrorMessage(final Exception e) { + String message = e.getMessage(); + if (isBlank(message)) { + message = "Unexpected error while processing MIB file."; + } + return e.getClass().getSimpleName() + ": " + message; + } + + private static String safeName(String name) { + return name == null ? "" : name; + } + + private static String fileNameOnly(File f) { + return f == null ? null : f.getName(); + } + + private static void validateLocationAndFileName(final String location, final String fileName) { + if (location == null || location.isBlank()) { + LOG.warn("Request rejected: blank location (fileName={})", fileName); + throw new IllegalArgumentException("location must not be blank."); + } + + if (fileName == null || fileName.isBlank()) { + LOG.warn("Request rejected: blank fileName (location={})", location); + throw new IllegalArgumentException("fileName must not be blank."); + } + + if (fileName.contains("/") || fileName.contains("\\") || fileName.contains("..")) { + LOG.warn("Request rejected: invalid fileName={} (location={})", fileName, location); + throw new IllegalArgumentException("Invalid fileName."); + } + } + + private EventConfSourceMetadataDto buildMetadata(String fileName, Events events, int fileOrder, + Date now) { + + final String baseName = StringUtils.substringBeforeLast(fileName, "."); + final String base = StringUtils.isBlank(baseName) ? fileName : baseName; + + final String eventsFileName = base + ".events"; + + return new EventConfSourceMetadataDto.Builder() + .filename(eventsFileName) + .eventCount(events.getEvents().size()) + .fileOrder(fileOrder) + .username("system-generated") + .now(now) + .vendor(base) + .description("") .build(); } -} + + @PreDestroy + public void shutdown() { + eventConfExecutor.shutdown(); + } + +} \ No newline at end of file diff --git a/features/mib-compiler/rest/src/main/java/org/opennms/features/mibcompiler/rest/internal/MibCompilerServiceUtil.java b/features/mib-compiler/rest/src/main/java/org/opennms/features/mibcompiler/rest/internal/MibCompilerServiceUtil.java new file mode 100644 index 000000000000..95fe0769ec80 --- /dev/null +++ b/features/mib-compiler/rest/src/main/java/org/opennms/features/mibcompiler/rest/internal/MibCompilerServiceUtil.java @@ -0,0 +1,399 @@ +/* + * 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.rest.internal; + +import org.opennms.core.utils.ConfigFileConstants; +import org.opennms.features.mibcompiler.rest.model.MibCompilerFileInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +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; +import java.util.stream.Stream; + +public class MibCompilerServiceUtil { + + private static final Logger LOG = LoggerFactory.getLogger(MibCompilerServiceUtil.class); + + 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"; + + private static final File MIBS_ROOT_DIR = new File(ConfigFileConstants.getHome(), SHARE_MIBS_DIR); + + /** The Constant MIBS_PENDING_DIR. */ + private static final File MIBS_PENDING_DIR = new File(MIBS_ROOT_DIR, PENDING_DIR); + + /** The Constant MIBS_COMPILED_DIR. */ + private static final File MIBS_COMPILED_DIR = new File(MIBS_ROOT_DIR, COMPILED_DIR); + + /** Expose compiled dir for consumers that need to configure parsers, etc. */ + public static File getCompiledDir() { + return MIBS_COMPILED_DIR; + } + + public static boolean baseNameExistsInPendingOrCompiled(final String baseName) throws Exception { + final boolean existsPending = baseNameExists(MIBS_PENDING_DIR, baseName); + final boolean existsCompiled = baseNameExists(MIBS_COMPILED_DIR, baseName); + final boolean exists = existsPending || existsCompiled; + + LOG.debug("baseNameExistsInPendingOrCompiled(baseName={}): pending={}, compiled={}, exists={}", + baseName, existsPending, existsCompiled, exists); + + return exists; + } + + /** + * Save a file to pending with a normalized name: {baseName}{extension} + * Example: baseName="IF-MIB", extension=".mib" -> IF-MIB.mib + */ + public static File saveToPending(final String baseName, + final String extension, + final InputStream content) throws Exception { + + if (isBlank(baseName)) { + LOG.warn("saveToPending called with blank baseName"); + throw new IllegalArgumentException("baseName must not be blank."); + } + if (content == null) { + LOG.warn("saveToPending(baseName={}) called with null content", baseName); + throw new IllegalArgumentException("content must not be null."); + } + + ensureDirExists(MIBS_PENDING_DIR); + final String ext = normalizeExtension(extension, DEFAULT_MIB_EXTENSION); + final File target = new File(MIBS_PENDING_DIR, baseName + ext); + + LOG.info("Saving MIB to pending: baseName={}, extension={}, target={}", + baseName, ext, target.getAbsolutePath()); + + try (FileOutputStream out = new FileOutputStream(target)) { + copy(content, out); + } + + LOG.debug("Saved pending MIB: {}", target.getAbsolutePath()); + return target; + } + + /** + * Find a single pending file by base name (no parsing/validation here). + * @return the pending File or null if not found + * @throws IllegalStateException if multiple matches exist + */ + public static File findPendingByBaseName(final String baseName) throws Exception { + if (isBlank(baseName)) { + throw new IllegalArgumentException("baseName must not be blank."); + } + + ensureDirExists(MIBS_PENDING_DIR); + + final String normalizedBaseName = stripPathAndExtension(baseName); + if (isBlank(normalizedBaseName)) { + throw new IllegalArgumentException("baseName must not be blank."); + } + + return findSingleByBaseName(MIBS_PENDING_DIR, normalizedBaseName); + } + + /** + * Move a pending file to compiled directory, forcing ".mib" extension. + * This assumes validation/parsing has already happened elsewhere. + */ + public static File movePendingToCompiled(final File pendingFile, final String baseName) throws Exception { + if (pendingFile == null) { + throw new IllegalArgumentException("pendingFile must not be null."); + } + if (isBlank(baseName)) { + throw new IllegalArgumentException("baseName must not be blank."); + } + + ensureDirExists(MIBS_COMPILED_DIR); + + final String normalizedBaseName = stripPathAndExtension(baseName); + if (isBlank(normalizedBaseName)) { + throw new IllegalArgumentException("baseName must not be blank."); + } + + final File destFile = new File(MIBS_COMPILED_DIR, normalizedBaseName + DEFAULT_MIB_EXTENSION); + if (destFile.exists()) { + LOG.warn("Compilation conflict: destination file already exists: {}", destFile.getAbsolutePath()); + throw new IllegalStateException("Compiled file already exists: " + destFile.getName()); + } + + LOG.info("Moving compiled MIB from pending to compiled: from={}, to={}", + pendingFile.getAbsolutePath(), destFile.getAbsolutePath()); + + Files.move(pendingFile.toPath(), destFile.toPath(), StandardCopyOption.ATOMIC_MOVE); + + LOG.info("Moved MIB to compiled: baseName={}, compiledFile={}", + normalizedBaseName, destFile.getAbsolutePath()); + + return destFile; + } + + public static List listPendingAndCompiledFiles() throws IOException { + ensureDirExists(MIBS_PENDING_DIR); + ensureDirExists(MIBS_COMPILED_DIR); + + final List results = new ArrayList(); + results.addAll(listFilesInDir(MIBS_PENDING_DIR, MibCompilerFileInfo.Location.PENDING)); + results.addAll(listFilesInDir(MIBS_COMPILED_DIR, MibCompilerFileInfo.Location.COMPILED)); + + results.sort(Comparator + .comparing(MibCompilerFileInfo::getLocation) + .thenComparing(MibCompilerFileInfo::getFileName)); + + LOG.debug("Listed mib compiler files: total={}, pendingDir={}, compiledDir={}", + results.size(), MIBS_PENDING_DIR.getAbsolutePath(), MIBS_COMPILED_DIR.getAbsolutePath()); + + return results; + } + + public static boolean deleteFile(final String location, final String fileName) throws IOException { + if (isBlank(location)) { + throw new IllegalArgumentException("location must not be blank."); + } + if (isBlank(fileName)) { + throw new IllegalArgumentException("fileName must not be blank."); + } + + if (fileName.contains("/") || fileName.contains("\\") || fileName.contains("..")) { + throw new IllegalArgumentException("Invalid fileName."); + } + + final File dir = resolveLocationDir(location); + ensureDirExists(dir); + + final Path target = new File(dir, fileName).toPath(); + + if (!Files.exists(target)) { + return false; // not found + } + if (!Files.isRegularFile(target)) { + throw new IllegalStateException("Target is not a regular file: " + fileName); + } + + Files.delete(target); + LOG.info("Deleted MIB file: location={}, fileName={}", location, fileName); + return true; + } + + public static String readTextFile(final String location, final String fileName) throws IOException { + final File dir = resolveLocationDir(location); + ensureDirExists(dir); + + final Path target = new File(dir, fileName).toPath(); + + if (!Files.exists(target)) { + return null; + } + if (!Files.isRegularFile(target)) { + throw new IllegalStateException("Target is not a regular file: " + fileName); + } + + byte[] bytes = Files.readAllBytes(target); + return new String(bytes, StandardCharsets.UTF_8); + } + + public static void writeBinaryFile(final String location, final String fileName, final byte[] contents) throws IOException { + final File dir = resolveLocationDir(location); + ensureDirExists(dir); + + final Path target = new File(dir, fileName).toPath(); + + if (!Files.exists(target)) { + throw new IllegalArgumentException("File does not exists: " + target); + } + if (!Files.isRegularFile(target)) { + throw new IllegalArgumentException("Target is not a regular file: " + fileName); + } + + Files.write(target, contents); + } + + /** Helper for REST: does file exist at location/fileName? */ + public static boolean exists(final String location, final String fileName) throws IOException { + final File dir = resolveLocationDir(location); + ensureDirExists(dir); + final Path target = new File(dir, fileName).toPath(); + return Files.exists(target) && Files.isRegularFile(target); + } + + /** Helper for REST: get File handle for location/fileName (no existence guarantee). */ + public static File getFile(final String location, final String fileName) { + final File dir = resolveLocationDir(location); + return new File(dir, fileName); + } + + private static File resolveLocationDir(final String location) { + if (isBlank(location)) { + throw new IllegalArgumentException("location must not be blank."); + } + switch (location.toLowerCase()) { + case "pending": + return MIBS_PENDING_DIR; + case "compiled": + return MIBS_COMPILED_DIR; + default: + throw new IllegalArgumentException("Invalid location: " + location + ". Must be 'pending' or 'compiled'."); + } + } + + private static List listFilesInDir(File dir, MibCompilerFileInfo.Location location) throws IOException { + if (dir == null || !dir.exists() || !dir.isDirectory()) { + return Collections.emptyList(); + } + + final List out = new ArrayList(); + try (Stream stream = Files.list(dir.toPath())) { + stream.filter(Files::isRegularFile) + .sorted(Comparator.comparing(p -> p.getFileName().toString())) + .forEach(p -> out.add(new MibCompilerFileInfo( + p.getFileName().toString(), + location + ))); + } + return out; + } + + private static File findSingleByBaseName(final File dir, final String baseName) throws Exception { + if (dir == null || !dir.exists() || !dir.isDirectory()) { + LOG.debug("findSingleByBaseName: directory missing or not a directory: dir={}, baseName={}", + dir == null ? null : dir.getAbsolutePath(), baseName); + return null; + } + + final List matches = new ArrayList(); + try (Stream 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()) { + LOG.debug("findSingleByBaseName: no matches: dir={}, baseName={}", dir.getAbsolutePath(), baseName); + return null; + } + if (matches.size() > 1) { + LOG.error("findSingleByBaseName: multiple matches: dir={}, baseName={}, matches={}", + dir.getAbsolutePath(), baseName, matches); + throw new IllegalStateException("Multiple pending files found with base name '" + baseName + "': " + matches); + } + + LOG.debug("findSingleByBaseName: single match found: {}", matches.get(0)); + return matches.get(0).toFile(); + } + + private static void ensureDirExists(final File dir) { + if (dir.exists()) { + if (!dir.isDirectory()) { + LOG.error("Path exists but is not a directory: {}", dir.getAbsolutePath()); + throw new IllegalStateException("Path exists but is not a directory: " + dir.getAbsolutePath()); + } + return; + } + if (!dir.mkdirs()) { + LOG.error("Failed to create directory: {}", dir.getAbsolutePath()); + throw new IllegalStateException("Failed to create directory: " + dir.getAbsolutePath()); + } + LOG.debug("Created directory: {}", dir.getAbsolutePath()); + } + + private static boolean baseNameExists(final File dir, final String baseName) throws Exception { + if (dir == null || !dir.exists() || !dir.isDirectory()) { + LOG.debug("baseNameExists: dir missing or not a directory: dir={}, baseName={}", + dir == null ? null : dir.getAbsolutePath(), baseName); + return false; + } + try (Stream stream = Files.list(dir.toPath())) { + final boolean found = stream.anyMatch(p -> baseName.equals(stripPathAndExtension(p.getFileName().toString()))); + LOG.debug("baseNameExists: dir={}, baseName={}, found={}", dir.getAbsolutePath(), baseName, found); + return found; + } + } + + public static String stripPathAndExtension(final String filename) { + if (filename == null) return null; + + 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; + + 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 (isBlank(ext)) { + ext = defaultExt; + } + ext = ext.trim(); + if (!ext.startsWith(".")) { + ext = "." + ext; + } + return ext; + } + + private static boolean isBlank(String s) { + return s == null || s.trim().isEmpty(); + } + + private static long copy(InputStream in, OutputStream out) throws IOException { + byte[] buffer = new byte[8192]; + long total = 0; + int read; + while ((read = in.read(buffer)) != -1) { + out.write(buffer, 0, read); + total += read; + } + return total; + } +} \ No newline at end of file diff --git a/features/mib-compiler/rest/src/main/java/org/opennms/features/mibcompiler/rest/model/CompileMibResult.java b/features/mib-compiler/rest/src/main/java/org/opennms/features/mibcompiler/rest/model/CompileMibResult.java new file mode 100644 index 000000000000..6975fb848c7f --- /dev/null +++ b/features/mib-compiler/rest/src/main/java/org/opennms/features/mibcompiler/rest/model/CompileMibResult.java @@ -0,0 +1,116 @@ +/* + * 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.rest.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/src/main/java/org/opennms/features/mibcompiler/rest/model/MibCompilerFileInfo.java b/features/mib-compiler/rest/src/main/java/org/opennms/features/mibcompiler/rest/model/MibCompilerFileInfo.java new file mode 100644 index 000000000000..a37bb2ef7e28 --- /dev/null +++ b/features/mib-compiler/rest/src/main/java/org/opennms/features/mibcompiler/rest/model/MibCompilerFileInfo.java @@ -0,0 +1,51 @@ +/* + * 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.rest.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class MibCompilerFileInfo { + public enum Location { + PENDING, + COMPILED + } + + private final String fileName; + private final Location location; + + @JsonCreator + public MibCompilerFileInfo( + @JsonProperty("fileName") String fileName, + @JsonProperty("location") Location location) { + this.fileName = fileName; + this.location = location; + } + + public String getFileName() { + return fileName; + } + + public Location getLocation() { + return location; + } +} diff --git a/features/mib-compiler/rest/src/main/java/org/opennms/features/mibcompiler/rest/model/MibCompilerFileText.java b/features/mib-compiler/rest/src/main/java/org/opennms/features/mibcompiler/rest/model/MibCompilerFileText.java new file mode 100644 index 000000000000..0627d4b09dde --- /dev/null +++ b/features/mib-compiler/rest/src/main/java/org/opennms/features/mibcompiler/rest/model/MibCompilerFileText.java @@ -0,0 +1,53 @@ +/* + * 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.rest.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class MibCompilerFileText { + private final String name; + private final String location; + private final String contents; + + @JsonCreator + public MibCompilerFileText( + @JsonProperty("name") String name, + @JsonProperty("location") String location, + @JsonProperty("contents") String contents) { + this.name = name; + this.location = location; + this.contents = contents; + } + + public String getName() { + return name; + } + + public String getLocation() { + return location; + } + + public String getContents() { + return contents; + } +} diff --git a/features/mib-compiler/rest/src/main/java/org/opennms/features/mibcompiler/rest/model/MibCompilerGenerateEventsRequest.java b/features/mib-compiler/rest/src/main/java/org/opennms/features/mibcompiler/rest/model/MibCompilerGenerateEventsRequest.java new file mode 100644 index 000000000000..5ad20497ce38 --- /dev/null +++ b/features/mib-compiler/rest/src/main/java/org/opennms/features/mibcompiler/rest/model/MibCompilerGenerateEventsRequest.java @@ -0,0 +1,50 @@ +/* + * 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.rest.model; + +public class MibCompilerGenerateEventsRequest { + + private String name; + private String ueiBase; + + public MibCompilerGenerateEventsRequest() { + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUeiBase() { + return ueiBase; + } + + public void setUeiBase(String ueiBase) { + this.ueiBase = ueiBase; + } + + + +} diff --git a/features/mib-compiler/rest/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/features/mib-compiler/rest/src/main/resources/OSGI-INF/blueprint/blueprint.xml index 6d8d8bebad67..f59b72a000e0 100644 --- a/features/mib-compiler/rest/src/main/resources/OSGI-INF/blueprint/blueprint.xml +++ b/features/mib-compiler/rest/src/main/resources/OSGI-INF/blueprint/blueprint.xml @@ -6,9 +6,18 @@ "> + + + + + + + + + diff --git a/features/mib-compiler/rest/src/test/java/org/opennms/features/mibcompiler/rest/internal/MibCompilerRestServiceImplIT.java b/features/mib-compiler/rest/src/test/java/org/opennms/features/mibcompiler/rest/internal/MibCompilerRestServiceImplIT.java new file mode 100644 index 000000000000..b293bf6c861a --- /dev/null +++ b/features/mib-compiler/rest/src/test/java/org/opennms/features/mibcompiler/rest/internal/MibCompilerRestServiceImplIT.java @@ -0,0 +1,307 @@ +/* + * 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.rest.internal; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.opennms.features.mibcompiler.api.MibParser; +import org.opennms.features.mibcompiler.rest.model.MibCompilerFileInfo; +import org.opennms.features.mibcompiler.rest.model.MibCompilerFileText; +import org.opennms.features.mibcompiler.rest.model.MibCompilerGenerateEventsRequest; +import org.opennms.netmgt.config.api.EventConfDao; +import org.opennms.netmgt.dao.api.EventConfEventDao; +import org.opennms.netmgt.dao.api.EventConfSourceDao; +import org.springframework.transaction.support.TransactionOperations; + +import javax.ws.rs.core.Response; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.List; +import java.util.Map; + +import static org.apache.activemq.util.IOHelper.deleteChildren; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class MibCompilerRestServiceImplIT { + + private File pendingDir; + private File compiledDir; + private static File classHome; + + private MibParser parser; + private EventConfSourceDao eventConfSourceDao; + private EventConfEventDao eventConfEventDao; + private EventConfDao eventConfDao; + private TransactionOperations operations; + + private MibCompilerRestServiceImpl service; + + @BeforeClass + public static void beforeClass() throws Exception { + classHome = Files.createTempDirectory("mibcompiler-rest-it-home-").toFile(); + System.setProperty("opennms.home", classHome.getAbsolutePath()); + } + + @AfterClass + public static void afterClass() { + System.clearProperty("opennms.home"); + deleteRecursively(classHome); + } + + @Before + public void setUp() throws Exception { + File mibsRoot = new File(classHome, "share/mibs"); + pendingDir = new File(mibsRoot, "pending"); + compiledDir = new File(mibsRoot, "compiled"); + + pendingDir.mkdirs(); + compiledDir.mkdirs(); + + deleteChildren(pendingDir); + deleteChildren(compiledDir); + + // Keep references to mocks so tests (especially generateEvents) can stub them. + parser = mock(MibParser.class); + eventConfSourceDao = mock(EventConfSourceDao.class); + eventConfEventDao = mock(EventConfEventDao.class); + eventConfDao = mock(EventConfDao.class); + operations = mock(TransactionOperations.class); + + service = new MibCompilerRestServiceImpl(parser, eventConfSourceDao, eventConfEventDao, eventConfDao, operations); + } + + @After + public void tearDown() { + deleteRecursively(pendingDir); + deleteRecursively(compiledDir); + } + + @Test + public void listPendingAndCompiledFiles_shouldReturn200AndIncludePendingAndCompiled() throws Exception { + Files.writeString(new File(pendingDir, "p1.mib").toPath(), "pending", StandardCharsets.UTF_8); + Files.writeString(new File(compiledDir, "c1.mib").toPath(), "compiled", StandardCharsets.UTF_8); + + Response r = service.listPendingAndCompiledFiles(); + + assertEquals(200, r.getStatus()); + assertNotNull(r.getEntity()); + + @SuppressWarnings("unchecked") + List list = (List) r.getEntity(); + + assertEquals(2, list.size()); + } + + @Test + public void listPendingAndCompiledFiles_shouldReturn200AndEmptyListWhenNoFiles() { + Response r = service.listPendingAndCompiledFiles(); + + assertEquals(200, r.getStatus()); + assertNotNull(r.getEntity()); + + @SuppressWarnings("unchecked") + List list = (List) r.getEntity(); + + assertTrue(list.isEmpty()); + } + + @Test + public void deleteFile_shouldReturn204WhenPendingFileDeleted() throws Exception { + File f = new File(pendingDir, "to-delete.txt"); + Files.writeString(f.toPath(), "x", StandardCharsets.UTF_8); + assertTrue(f.exists()); + + Response r = service.deleteFile("pending", "to-delete.txt"); + + assertEquals(204, r.getStatus()); + assertFalse("file should be deleted", f.exists()); + } + + @Test + public void deleteFile_shouldReturn404WhenFileMissing() { + Response r = service.deleteFile("pending", "missing.txt"); + assertEquals(404, r.getStatus()); + } + + @Test + public void deleteFile_shouldReturn400ForInvalidFileNameTraversal() { + Response r = service.deleteFile("pending", "../evil.txt"); + assertEquals(400, r.getStatus()); + } + + @Test + public void getFileText_shouldReturn200AndFileTextForPending() throws Exception { + Files.writeString(new File(pendingDir, "file1.txt").toPath(), + "opennms\nmibcompiler testing", StandardCharsets.UTF_8); + + Response r = service.getFileText("pending", "file1.txt"); + + assertEquals(200, r.getStatus()); + assertNotNull(r.getEntity()); + assertTrue(r.getEntity() instanceof MibCompilerFileText); + + MibCompilerFileText body = (MibCompilerFileText) r.getEntity(); + assertEquals("file1.txt", body.getName()); + assertEquals("pending", body.getLocation()); + assertEquals("opennms\nmibcompiler testing", body.getContents()); + } + + @Test + public void getFileText_shouldReturn404WhenMissing() throws Exception { + Response r = service.getFileText("compiled", "missing.mib"); + assertEquals(404, r.getStatus()); + } + + @Test + public void getFileText_shouldReturn404ForInvalidFileName() throws Exception { + Response r = service.getFileText("pending", "..\\evil.txt"); + assertEquals(400, r.getStatus()); + } + + @Test + public void setFileText_shouldReturn200AndOverwritePendingFile() throws Exception { + Files.writeString(new File(pendingDir, "edit.txt").toPath(), + "old", StandardCharsets.UTF_8); + + byte[] content = "new\ntext".getBytes(StandardCharsets.UTF_8); + + Response r = service.setFileText("edit.txt", content); + + assertEquals(200, r.getStatus()); + assertNotNull(r.getEntity()); + + // Verify content changed on disk + String updated = Files.readString(new File(pendingDir, "edit.txt").toPath(), StandardCharsets.UTF_8); + assertEquals("new\ntext", updated); + } + + @Test + public void setFileText_shouldReturn400WhenMibContentIsNull() { + Response r = service.setFileText("edit.txt", null); + assertEquals(400, r.getStatus()); + } + + @Test + public void uploadMib_shouldReturn201AndSaveToPending() throws Exception { + byte[] content = "SOME MIB CONTENT".getBytes(StandardCharsets.UTF_8); + + Response r = service.uploadMib(content, "IF-MIB.mib"); + + assertEquals(201, r.getStatus()); + assertNotNull(r.getEntity()); + assertTrue(r.getEntity() instanceof Map); + + @SuppressWarnings("unchecked") + Map payload = (Map) r.getEntity(); + + assertTrue(payload.containsKey("success")); + assertTrue(payload.containsKey("errors")); + + File[] pendingFiles = pendingDir.listFiles(); + assertNotNull(pendingFiles); + assertEquals(1, pendingFiles.length); + assertEquals("IF-MIB.mib", pendingFiles[0].getName()); + } + + @Test + public void uploadMib_shouldReturn400ForEmptyContent() throws Exception { + Response r = service.uploadMib(new byte[0], "IF-MIB.mib"); + assertEquals(400, r.getStatus()); + } + + @Test + public void uploadMib_shouldReturn409WhenBaseNameAlreadyExistsInPending() throws Exception { + Files.writeString(new File(pendingDir, "IF-MIB.mib").toPath(), "existing", StandardCharsets.UTF_8); + Response r = service.uploadMib("new".getBytes(StandardCharsets.UTF_8), "IF-MIB.txt"); + assertEquals(409, r.getStatus()); + } + + + @Test + public void generateEvents_shouldReturn409WhenMissingDependencies() throws Exception { + Files.writeString(new File(compiledDir, "D.mib").toPath(), "dummy", StandardCharsets.UTF_8); + + when(parser.parseMib(any(File.class))).thenReturn(false); + when(parser.getMissingDependencies()).thenReturn(List.of("SNMPv2-SMI")); + + MibCompilerGenerateEventsRequest req = new MibCompilerGenerateEventsRequest(); + req.setName("D.mib"); + req.setUeiBase("uei.opennms.org/test"); + + Response r = service.generateEvents(req); + assertEquals(409, r.getStatus()); + } + + @Test + public void generateEvents_shouldReturn400WhenParseFailsWithErrors() throws Exception { + Files.writeString(new File(compiledDir, "D.mib").toPath(), "dummy", StandardCharsets.UTF_8); + + when(parser.parseMib(any(File.class))).thenReturn(false); + when(parser.getMissingDependencies()).thenReturn(List.of()); + when(parser.getFormattedErrors()).thenReturn("parse error"); + + MibCompilerGenerateEventsRequest req = new MibCompilerGenerateEventsRequest(); + req.setName("D.mib"); + req.setUeiBase("uei.opennms.org/test"); + + Response r = service.generateEvents(req); + assertEquals(400, r.getStatus()); + assertEquals("parse error", r.getEntity()); + } + + @Test + public void generateEvents_shouldReturn500WhenEventsNull() throws Exception { + Files.writeString(new File(compiledDir, "D.mib").toPath(), "dummy", StandardCharsets.UTF_8); + + when(parser.parseMib(any(File.class))).thenReturn(true); + when(parser.getEvents(anyString())).thenReturn(null); + + MibCompilerGenerateEventsRequest req = new MibCompilerGenerateEventsRequest(); + req.setName("D.mib"); + req.setUeiBase("uei.opennms.org/test"); + + Response r = service.generateEvents(req); + assertEquals(500, r.getStatus()); + } + + private static void deleteRecursively(File f) { + if (f == null || !f.exists()) return; + File[] kids = f.listFiles(); + if (kids != null) { + for (File k : kids) { + deleteRecursively(k); + } + } + f.delete(); + } +} \ No newline at end of file diff --git a/opennms-dao/src/main/java/org/opennms/netmgt/dao/support/EventConfServiceHelper.java b/opennms-dao/src/main/java/org/opennms/netmgt/dao/support/EventConfServiceHelper.java index 40752184bf0c..fe5c52bcb7d6 100644 --- a/opennms-dao/src/main/java/org/opennms/netmgt/dao/support/EventConfServiceHelper.java +++ b/opennms-dao/src/main/java/org/opennms/netmgt/dao/support/EventConfServiceHelper.java @@ -25,10 +25,13 @@ import org.opennms.core.xml.JaxbUtils; import org.opennms.netmgt.config.api.EventConfDao; import org.opennms.netmgt.dao.api.EventConfEventDao; +import org.opennms.netmgt.dao.api.EventConfSourceDao; import org.opennms.netmgt.model.EventConfEvent; import org.opennms.netmgt.model.EventConfSource; import org.opennms.netmgt.model.OnmsSeverity; +import org.opennms.netmgt.model.events.EventConfSourceMetadataDto; import org.opennms.netmgt.xml.eventconf.Event; +import org.opennms.netmgt.xml.eventconf.Events; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -75,6 +78,31 @@ public static Long saveEvent(EventConfEventDao eventConfEventDao, EventConfSourc return eventConfEventDao.save(eventConfEvent); } + + public static EventConfSource createOrUpdateSource(EventConfSourceDao eventConfSourceDao,final EventConfSourceMetadataDto eventConfSourceMetadataDto) { + EventConfSource source = eventConfSourceDao.findByName(eventConfSourceMetadataDto.getFilename()); + if (source == null) { + source = new EventConfSource(); + source.setCreatedTime(eventConfSourceMetadataDto.getNow()); + source.setFileOrder(eventConfSourceMetadataDto.getFileOrder()); + } + source.setName(eventConfSourceMetadataDto.getFilename()); + source.setEventCount(eventConfSourceMetadataDto.getEventCount()); + source.setEnabled(true); + source.setUploadedBy(eventConfSourceMetadataDto.getUsername()); + source.setLastModified(eventConfSourceMetadataDto.getNow()); + source.setVendor(eventConfSourceMetadataDto.getVendor()); + source.setDescription(eventConfSourceMetadataDto.getDescription()); + eventConfSourceDao.saveOrUpdate(source); + return eventConfSourceDao.get(source.getId()); + } + + public static void saveEvents(EventConfEventDao eventConfEventDao,EventConfSource source, Events events, String username, Date now) { + List eventEntities = EventConfServiceHelper.createEventConfEventEntities( + source, events.getEvents(), username, now); + eventConfEventDao.saveAll(eventEntities); + } + /** * Reloads all enabled events from the database into memory synchronously. *