package com.thinkbiganalytics.feedmgr.service.template; /*- * #%L * thinkbig-feed-manager-controller * %% * Copyright (C) 2017 ThinkBig Analytics * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% */ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.thinkbiganalytics.feedmgr.nifi.cache.NifiFlowCache; import com.thinkbiganalytics.feedmgr.nifi.NifiTemplateParser; import com.thinkbiganalytics.feedmgr.nifi.PropertyExpressionResolver; import com.thinkbiganalytics.feedmgr.rest.ImportComponent; import com.thinkbiganalytics.feedmgr.rest.ImportSection; import com.thinkbiganalytics.feedmgr.rest.ImportType; import com.thinkbiganalytics.feedmgr.rest.model.ImportComponentOption; import com.thinkbiganalytics.feedmgr.rest.model.ImportOptions; import com.thinkbiganalytics.feedmgr.rest.model.ImportTemplateOptions; import com.thinkbiganalytics.feedmgr.rest.model.RegisteredTemplate; import com.thinkbiganalytics.feedmgr.rest.model.RegisteredTemplateRequest; import com.thinkbiganalytics.feedmgr.rest.model.ReusableTemplateConnectionInfo; import com.thinkbiganalytics.feedmgr.rest.model.UploadProgress; import com.thinkbiganalytics.feedmgr.rest.model.UploadProgressMessage; import com.thinkbiganalytics.feedmgr.rest.support.SystemNamingService; import com.thinkbiganalytics.feedmgr.security.FeedServicesAccessControl; import com.thinkbiganalytics.feedmgr.service.MetadataService; import com.thinkbiganalytics.feedmgr.service.UploadProgressService; import com.thinkbiganalytics.feedmgr.support.ZipFileUtil; import com.thinkbiganalytics.feedmgr.util.ImportUtil; import com.thinkbiganalytics.json.ObjectMapperSerializer; import com.thinkbiganalytics.metadata.api.MetadataAccess; import com.thinkbiganalytics.metadata.api.template.security.TemplateAccessControl; import com.thinkbiganalytics.nifi.feedmgr.ReusableTemplateCreationCallback; import com.thinkbiganalytics.nifi.feedmgr.TemplateCreationHelper; import com.thinkbiganalytics.nifi.rest.client.LegacyNifiRestClient; import com.thinkbiganalytics.nifi.rest.client.NifiClientRuntimeException; import com.thinkbiganalytics.nifi.rest.client.NifiComponentNotFoundException; import com.thinkbiganalytics.nifi.rest.model.NiFiComponentErrors; import com.thinkbiganalytics.nifi.rest.model.NiFiPropertyDescriptorTransform; import com.thinkbiganalytics.nifi.rest.model.NifiError; import com.thinkbiganalytics.nifi.rest.model.NifiProcessGroup; import com.thinkbiganalytics.nifi.rest.model.NifiProperty; import com.thinkbiganalytics.nifi.rest.support.NifiConnectionUtil; import com.thinkbiganalytics.nifi.rest.support.NifiProcessUtil; import com.thinkbiganalytics.nifi.rest.support.NifiPropertyUtil; import com.thinkbiganalytics.security.AccessController; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.nifi.web.api.dto.ProcessGroupDTO; import org.apache.nifi.web.api.dto.ProcessorDTO; import org.apache.nifi.web.api.dto.TemplateDTO; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.xml.sax.SAXException; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.StringWriter; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; import javax.inject.Inject; import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPathExpressionException; /** * Export or Import a template */ public class ExportImportTemplateService { public static final Logger log = LoggerFactory.getLogger(ExportImportTemplateService.class); public static final String NIFI_CONNECTING_REUSABLE_TEMPLATE_XML_FILE = "nifiConnectingReusableTemplate"; public static final String NIFI_TEMPLATE_XML_FILE = "nifiTemplate.xml"; public static final String TEMPLATE_JSON_FILE = "template.json"; @Autowired PropertyExpressionResolver propertyExpressionResolver; @Autowired MetadataService metadataService; @Inject MetadataAccess metadataAccess; @Autowired LegacyNifiRestClient nifiRestClient; @Inject NifiFlowCache nifiFlowCache; @Inject private AccessController accessController; @Inject private RegisteredTemplateService registeredTemplateService; @Inject private NiFiPropertyDescriptorTransform propertyDescriptorTransform; @Inject private UploadProgressService uploadProgressService; //Export Methods public ExportTemplate exportTemplate(String templateId) { this.accessController.checkPermission(AccessController.SERVICES, FeedServicesAccessControl.EXPORT_TEMPLATES); RegisteredTemplate template = registeredTemplateService.findRegisteredTemplate(new RegisteredTemplateRequest.Builder().templateId(templateId).nifiTemplateId(templateId).includeSensitiveProperties(true).build()); if (template != null) { List<String> connectingReusableTemplates = new ArrayList<>(); Set<String> connectedTemplateIds = new HashSet<>(); //if this template uses any reusable templates then export those reusable ones as well if (template.usesReusableTemplate()) { List<ReusableTemplateConnectionInfo> reusableTemplateConnectionInfos = template.getReusableTemplateConnections(); for (ReusableTemplateConnectionInfo reusableTemplateConnectionInfo : reusableTemplateConnectionInfos) { String inputName = reusableTemplateConnectionInfo.getReusableTemplateInputPortName(); //find the template that has the input port name? Map<String, String> map = nifiRestClient.getTemplatesAsXmlMatchingInputPortName(inputName); if (map != null && !map.isEmpty()) { //get the first one?? for (Map.Entry<String, String> entry : map.entrySet()) { String portTemplateId = entry.getKey(); if (!connectedTemplateIds.contains(portTemplateId)) { connectedTemplateIds.add(portTemplateId); connectingReusableTemplates.add(entry.getValue()); } } } } } String templateXml = null; try { if (template != null) { try { templateXml = nifiRestClient.getTemplateXml(template.getNifiTemplateId()); } catch (NifiClientRuntimeException e) { TemplateDTO templateDTO = nifiRestClient.getTemplateByName(template.getTemplateName()); if (templateDTO != null) { templateXml = nifiRestClient.getTemplateXml(templateDTO.getId()); } } } } catch (Exception e) { throw new UnsupportedOperationException("Unable to find Nifi Template for " + templateId); } //create a zip file with the template and xml byte[] zipFile = zip(template, templateXml, connectingReusableTemplates); return new ExportTemplate(SystemNamingService.generateSystemName(template.getTemplateName()) + ".template.zip", zipFile); } else { throw new UnsupportedOperationException("Unable to find Template for " + templateId); } } private byte[] zip(RegisteredTemplate template, String nifiTemplateXml, List<String> reusableTemplateXmls) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); try (ZipOutputStream zos = new ZipOutputStream(baos)) { ZipEntry entry = new ZipEntry(NIFI_TEMPLATE_XML_FILE); zos.putNextEntry(entry); zos.write(nifiTemplateXml.getBytes()); zos.closeEntry(); int reusableTemplateNumber = 0; for (String reusableTemplateXml : reusableTemplateXmls) { entry = new ZipEntry(String.format("%s_%s.xml", NIFI_CONNECTING_REUSABLE_TEMPLATE_XML_FILE, reusableTemplateNumber++)); zos.putNextEntry(entry); zos.write(reusableTemplateXml.getBytes()); zos.closeEntry(); } entry = new ZipEntry(TEMPLATE_JSON_FILE); zos.putNextEntry(entry); String json = ObjectMapperSerializer.serialize(template); zos.write(json.getBytes()); zos.closeEntry(); } catch (IOException ioe) { throw new RuntimeException(ioe); } return baos.toByteArray(); } //Validation Methods /** * Is the file a valid file for importing * * @param fileName the name of the file * @return true if valid, false if not valid */ private boolean isValidFileImport(String fileName) { return fileName.endsWith(".zip") || fileName.endsWith(".xml"); } //validate public ImportTemplate validateTemplateForImport(final String fileName, byte[] content, ImportOptions importOptions) { this.accessController.checkPermission(AccessController.SERVICES, FeedServicesAccessControl.IMPORT_TEMPLATES); InputStream inputStream = new ByteArrayInputStream(content); UploadProgressMessage overallStatusMessage = uploadProgressService.addUploadStatus(importOptions.getUploadKey(), "Validating template for import"); UploadProgressMessage statusMessage = overallStatusMessage; ImportTemplateOptions options = new ImportTemplateOptions(); options.setUploadKey(importOptions.getUploadKey()); ImportTemplate template = null; if (!isValidFileImport(fileName)) { statusMessage.complete(false); throw new UnsupportedOperationException("Unable to import " + fileName + ". The file must be a zip file or a Nifi Template xml file"); } try { if (fileName.endsWith(".zip")) { template = openZip(fileName, inputStream); template.setValid(true); Set<ImportComponentOption> componentOptions = ImportUtil.inspectZipComponents(content, ImportType.TEMPLATE); options.setImportComponentOptions(importOptions.getImportComponentOptions()); options.addOptionsIfNotExists(componentOptions); template.setImportOptions(options); validateReusableTemplate(template, options); validateRegisteredTemplate(template, options); if(template.isValid()) { validateNiFiTemplateImport(template); } } else { template = getNewNiFiTemplateImport(fileName, inputStream); template.setImportOptions(options); //deal with reusable templates?? validateNiFiTemplateImport(template); } } catch (Exception e) { statusMessage.update("An Error occurred " + e.getMessage(), false); throw new UnsupportedOperationException("Error importing template " + fileName + ". " + e.getMessage()); } overallStatusMessage.update("Validated template for import ", template.isValid()); return template; } /** * Validate the NiFi template is valid. This method will validate the {@link ImportTemplate#nifiTemplateXml} * * @param template the template data to validate before importing */ private void validateNiFiTemplateImport(ImportTemplate template) throws IOException { ImportOptions options = template.getImportOptions(); ImportComponentOption nifiTemplateOption = options.findImportComponentOption(ImportComponent.NIFI_TEMPLATE); //if the options of the TEMPLATE_DATA are marked to import and overwrite this should be as well ImportComponentOption templateData = options.findImportComponentOption(ImportComponent.TEMPLATE_DATA); if (templateData.isUserAcknowledged()) { nifiTemplateOption.setUserAcknowledged(true); } if (templateData.isShouldImport()) { nifiTemplateOption.setShouldImport(true); } if (templateData.isOverwrite()) { nifiTemplateOption.setOverwrite(true); } if (nifiTemplateOption.isShouldImport()) { UploadProgressMessage statusMessage = uploadProgressService.addUploadStatus(options.getUploadKey(), "Validating the NiFi template"); String templateName = null; TemplateDTO dto = null; try { templateName = NifiTemplateParser.getTemplateName(template.getNifiTemplateXml()); template.setTemplateName(templateName); dto = nifiRestClient.getTemplateByName(templateName); if (dto != null) { template.setNifiTemplateId(dto.getId()); //if the template incoming is an XML template and it already exists, or if its a zip file and it exists and the user has not acknowledge to overwrite then error out if ((!options.isUserAcknowledged(ImportComponent.NIFI_TEMPLATE) || options.isUserAcknowledged(ImportComponent.NIFI_TEMPLATE) && !template.isZipFile() )&& !options.isImportAndOverwrite(ImportComponent.NIFI_TEMPLATE) && !options .isContinueIfExists(ImportComponent.NIFI_TEMPLATE)) { template.setValid(false); String msg = "Unable to import Template " + templateName + ". It already exists in NiFi."; template.getImportOptions().addErrorMessage(ImportComponent.NIFI_TEMPLATE, msg); statusMessage.update("Validation Error: Unable to import Template " + templateName + ". It already exists in NiFi."); statusMessage.complete(false); } else { statusMessage.update("Validated the NiFi template. "); statusMessage.complete(true); } } else { statusMessage.update("Validated the NiFi template. The template " + templateName + " will be created in NiFi"); statusMessage.complete(true); } } catch (ParserConfigurationException | XPathExpressionException | SAXException e) { template.setValid(false); template.getTemplateResults().addError(NifiError.SEVERITY.WARN, "The xml file you are trying to import is not a valid NiFi template. Please try again. " + e.getMessage(), ""); statusMessage.complete(false); } nifiTemplateOption.setValidForImport(!nifiTemplateOption.hasErrorMessages()); } completeSection(options, ImportSection.Section.VALIDATE_NIFI_TEMPLATE); } /** * Validate the Registered Template is valid for importing * * @param template the template data to validate before importing * @param options user options about what/how it should be imported */ private void validateRegisteredTemplate(ImportTemplate template, ImportOptions options) { ImportComponentOption registeredTemplateOption = options.findImportComponentOption(ImportComponent.TEMPLATE_DATA); //validate template boolean validForImport = true; if (!registeredTemplateOption.isUserAcknowledged()) { template.setValid(false); template.getImportOptions().addErrorMessage(ImportComponent.TEMPLATE_DATA, "A template exists. Do you want to import it?"); } if (registeredTemplateOption.isUserAcknowledged() && registeredTemplateOption.isShouldImport()) { UploadProgressMessage statusMessage = uploadProgressService.addUploadStatus(options.getUploadKey(), "Validating feed template for import"); RegisteredTemplate registeredTemplate = template.getTemplateToImport(); //validate unique //1 find if the template exists in the system running as a service account RegisteredTemplate existingTemplate = registeredTemplateService.findRegisteredTemplate(new RegisteredTemplateRequest.Builder().templateName(registeredTemplate.getTemplateName()).isFeedEdit(true).build()); if (existingTemplate != null) { if (options.stopProcessingAlreadyExists(ImportComponent.TEMPLATE_DATA)) { template.setValid(false); String msg = "Unable to import the template " + registeredTemplate.getTemplateName() + " It is already registered."; template.getImportOptions().addErrorMessage(ImportComponent.TEMPLATE_DATA, msg); statusMessage.update("Validation error: The template " + registeredTemplate.getTemplateName() + " is already registered", false); } else { //skip importing if user doesnt want it. if (!registeredTemplateOption.isOverwrite()) { validForImport = false; } statusMessage.complete(true); } registeredTemplate.setId(existingTemplate.getId()); //validate entity access if (accessController.isEntityAccessControlled() && registeredTemplateOption.isOverwrite()) { //requery as the currently logged in user statusMessage = uploadProgressService.addUploadStatus(options.getUploadKey(), "Validating template entity access"); existingTemplate = registeredTemplateService.findRegisteredTemplate(RegisteredTemplateRequest.requestByTemplateName(registeredTemplate.getTemplateName())); //ensure the user can Edit this template boolean valid = existingTemplate != null && existingTemplate.getAllowedActions().hasAction(TemplateAccessControl.EDIT_TEMPLATE.getSystemName()); if(!valid){ template.setValid(false); validForImport = false; statusMessage.update("Access Denied: You do not have edit access for the template " + registeredTemplate.getTemplateName(), false); template.getImportOptions().addErrorMessage(ImportComponent.TEMPLATE_DATA, "Access Denied: You do not have edit access for the template "); } else { statusMessage.complete(valid); } } } else { //template doesnt exist.. it is new ensure the user is allowed to create templates boolean editAccess = accessController.hasPermission(AccessController.SERVICES,FeedServicesAccessControl.EDIT_TEMPLATES); if(!editAccess) { statusMessage.update("Access Denied: You are not allowed to create the template " + registeredTemplate.getTemplateName(), false); template.getImportOptions().addErrorMessage(ImportComponent.TEMPLATE_DATA, "Access Denied: You are not allowed to create the template "); } registeredTemplate.setId(null); statusMessage.complete(editAccess); } validForImport &= !registeredTemplateOption.hasErrorMessages(); if (validForImport) { boolean isValid = validateTemplateProperties(template, options); if (template.isValid()) { template.setValid(isValid); } validForImport &= isValid; } registeredTemplateOption.setValidForImport(validForImport); } completeSection(options, ImportSection.Section.VALIDATE_REGISTERED_TEMPLATE); } /** * Validate any reusable templates as part of a zip file upload are valid for importing * * @param template the template data to validate before importing * @param options user options about what/how it should be imported */ private void validateReusableTemplate(ImportTemplate template, ImportTemplateOptions options) throws Exception { //validate the reusable template if (template.hasConnectingReusableTemplate()) { ImportComponentOption reusableTemplateOption = options.findImportComponentOption(ImportComponent.REUSABLE_TEMPLATE); UploadProgressMessage reusableTemplateStatusMessage = uploadProgressService.addUploadStatus(options.getUploadKey(), "Validating Reusable Template. "); if (reusableTemplateOption.isShouldImport()) { boolean validForImport = true; //for each of the connecting template for (String reusableTemplateXml : template.getNifiConnectingReusableTemplateXmls()) { String templateName = NifiTemplateParser.getTemplateName(reusableTemplateXml); UploadProgressMessage statusMessage = uploadProgressService.addUploadStatus(options.getUploadKey(), "Validating Reusable Template. " + templateName); TemplateDTO dto = nifiRestClient.getTemplateByName(templateName); //if there is a match and it has not been acknowledged by the user to overwrite or not, error out if (dto != null && !reusableTemplateOption.isUserAcknowledged()) { //error out it exists template.getImportOptions().addErrorMessage(ImportComponent.REUSABLE_TEMPLATE, "A reusable template with the same name " + templateName + " exists."); template.setValid(false); statusMessage.update("Reusable template, " + templateName + ", already exists.", false); validForImport = false; } else if (dto != null && reusableTemplateOption.isUserAcknowledged() && !reusableTemplateOption.isOverwrite()) { validForImport = false; //user has asked to not import the template. uploadProgressService.removeMessage(options.getUploadKey(), statusMessage); } else { uploadProgressService.removeMessage(options.getUploadKey(), statusMessage); //statusMessage.update("Validated Reusable Template", true); validForImport &= true; } } reusableTemplateOption.setValidForImport(validForImport); reusableTemplateStatusMessage.update("Validated Reusable Templates ", !reusableTemplateOption.hasErrorMessages()); } else if (!reusableTemplateOption.isUserAcknowledged()) { template.getImportOptions().addErrorMessage(ImportComponent.REUSABLE_TEMPLATE, "The file " + template.getFileName() + " has a reusable template to import."); template.setValid(false); reusableTemplateStatusMessage.update("A reusable template was found. Additional input needed.", false); } else { reusableTemplateStatusMessage.update("Reusable template found in import, but it is not marked for importing", true); } } completeSection(options, ImportSection.Section.VALIDATE_REUSABLE_TEMPLATE); } /** * Validate the Template doesnt have any sensitive properties needing additional user input before importing * * @param importTemplate the template data to validate before importing * @param importOptions user options about what/how it should be imported * @return true if valid, false if not */ private boolean validateTemplateProperties(ImportTemplate importTemplate, ImportOptions importOptions) { RegisteredTemplate template = importTemplate.getTemplateToImport(); //detect any sensitive properties and prompt for input before proceeding List<NifiProperty> sensitiveProperties = template.getSensitiveProperties(); ImportUtil.addToImportOptionsSensitiveProperties(importOptions, sensitiveProperties, ImportComponent.TEMPLATE_DATA); boolean valid = ImportUtil.applyImportPropertiesToTemplate(template, importTemplate, ImportComponent.TEMPLATE_DATA); if (!valid) { importTemplate.getImportOptions().addErrorMessage(ImportComponent.TEMPLATE_DATA, "Additional properties are required for the Template"); } importOptions.findImportComponentOption(ImportComponent.TEMPLATE_DATA).setAnalyzed(true); return valid; } /** * Validate the sensitive properties are set for the reusable templates. * * @param importTemplate the template data to validate before importing * @param importOptions user options about what/how it should be imported * @return true if valid, false if not */ private boolean analyzeReusableTemplateForImport(ImportTemplate importTemplate, ImportOptions importOptions) throws Exception { boolean valid = true; if (importTemplate.hasConnectingReusableTemplate()) { log.info("analyzing reusable templates in file"); for (String reusableTemplateXml : importTemplate.getNifiConnectingReusableTemplateXmls()) { //create a temp instance of this template to assess if it has any sensitive properties String templateName = NifiTemplateParser.getTemplateName(importTemplate.getNifiTemplateXml()); String reusableTemplateName = templateName; templateName += "_" + System.currentTimeMillis(); String templateXml = NifiTemplateParser.updateTemplateName(importTemplate.getNifiTemplateXml(), templateName); TemplateDTO temporaryTemplate = nifiRestClient.importTemplate(templateName, templateXml); log.info("created temp reusable template {} ", temporaryTemplate.getName() + ", for " + reusableTemplateName); TemplateCreationHelper helper = new TemplateCreationHelper(nifiRestClient); ProcessGroupDTO flow = helper.createTemporaryTemplateFlow(temporaryTemplate.getId()); nifiRestClient.deleteTemplate(temporaryTemplate.getId()); List<NifiProperty> sensitiveProperties = NifiPropertyUtil.getProperties(flow, propertyDescriptorTransform).stream().filter(nifiProperty -> nifiProperty.isSensitive()).collect(Collectors.toList()); if (sensitiveProperties != null && !sensitiveProperties.isEmpty()) { ImportUtil.addToImportOptionsSensitiveProperties(importOptions, sensitiveProperties, ImportComponent.REUSABLE_TEMPLATE); importTemplate.getImportOptions().addErrorMessage(ImportComponent.REUSABLE_TEMPLATE, "Additional Properties are required for input on the reusable template"); valid = false; } } importOptions.findImportComponentOption(ImportComponent.REUSABLE_TEMPLATE).setAnalyzed(true); } return valid; } private void completeSection(ImportOptions options, ImportSection.Section section) { UploadProgress progress = uploadProgressService.getUploadStatus(options.getUploadKey()); progress.completeSection(section.name()); } /// IMPORT methods /** * Import a xml or zip file. * * @param fileName the name of the file * @param content the file * @param importOptions user options about what/how it should be imported * @return the template data to import along with status/messages/error information if it was valid and if was successfully imported */ public ImportTemplate importTemplate(final String fileName, final byte[] content, ImportTemplateOptions importOptions) { // return metadataAccess.commit(() -> { this.accessController.checkPermission(AccessController.SERVICES, FeedServicesAccessControl.IMPORT_TEMPLATES); ImportTemplate template = null; if (!isValidFileImport(fileName)) { throw new UnsupportedOperationException("Unable to import " + fileName + ". The file must be a zip file or a Nifi Template xml file"); } try { if (fileName.endsWith(".zip")) { UploadProgress progress = uploadProgressService.getUploadStatus(importOptions.getUploadKey()); progress.setSections(ImportSection.sectionsForImportAsString(ImportType.TEMPLATE)); template = validateAndImportZip(fileName, content, importOptions); //dont allow exported reusable flows to become registered templates } else if (fileName.endsWith(".xml")) { UploadProgress progress = uploadProgressService.getUploadStatus(importOptions.getUploadKey()); progress.setSections(ImportSection.sectionsForImportAsString(ImportType.TEMPLATE_XML)); template = importNifiTemplate(fileName, content, importOptions, true); } } catch (IOException e) { throw new UnsupportedOperationException("Error importing template " + fileName + ". " + e.getMessage()); } template.setImportOptions(importOptions); return template; // }); } /** * Imports the data and various components based upon the supplied {@link ImportTemplate#importOptions}. * * Note. This method will not call any validation routines. It will just import. * If you want to validate before to ensure the import will be correct call this.importTemplate(final String fileName, final byte[] content, ImportTemplateOptions importOptions) * * @param importTemplate the template data to validate before importing * @return the template data to validate before importing */ public ImportTemplate importZip(ImportTemplate importTemplate) throws Exception { this.accessController.checkPermission(AccessController.SERVICES, FeedServicesAccessControl.IMPORT_TEMPLATES); ImportTemplateOptions importOptions = importTemplate.getImportOptions(); log.info("Importing Zip file template {}, overwrite: {}, reusableFlow: {}", importTemplate.getFileName(), importOptions.isImportAndOverwrite(ImportComponent.TEMPLATE_DATA), importOptions.isImport(ImportComponent.REUSABLE_TEMPLATE)); if (importTemplate.isValid()) { // UploadProgressMessage startingStatusMessage = uploadProgressService.addUploadStatus(importOptions.getUploadKey(), "Starting import of the template"); // startingStatusMessage.complete(true); UploadProgressMessage statusMessage = null; //Get information about the import RegisteredTemplate template = importTemplate.getTemplateToImport(); // validateTemplateProperties(template, importTemplate, importOptions); //1 ensure this template doesnt already exist importTemplate.setTemplateName(template.getTemplateName()); RegisteredTemplate existingTemplate = registeredTemplateService.findRegisteredTemplate(RegisteredTemplateRequest.requestByTemplateName(template.getTemplateName())); if (existingTemplate != null) { template.setId(existingTemplate.getId()); } else { template.setId(null); } List<ImportTemplate> connectingTemplates = importReusableTemplate(importTemplate, importOptions); if (importOptions.findImportComponentOption(ImportComponent.REUSABLE_TEMPLATE).hasErrorMessages()) { //return if invalid return importTemplate; } NiFiTemplateImport niFiTemplateImport = importNiFiXmlTemplate(importTemplate, importOptions); String oldTemplateXml = niFiTemplateImport.getOldTemplateXml(); TemplateDTO dto = niFiTemplateImport.getDto(); template.setNifiTemplateId(dto.getId()); importTemplate.setNifiTemplateId(dto.getId()); ImportComponentOption registeredTemplateImport = importOptions.findImportComponentOption(ImportComponent.TEMPLATE_DATA); if (existingTemplate != null) { importTemplate.setTemplateId(existingTemplate.getId()); } if (registeredTemplateImport.isShouldImport() && registeredTemplateImport.isValidForImport()) { NifiProcessGroup newTemplateInstance = createTemplateInstance(importTemplate, importOptions); if (newTemplateInstance.isSuccess()) { importTemplate.setSuccess(true); RegisteredTemplate savedTemplate = registerTemplate(importTemplate, importOptions); if (savedTemplate != null) { template = savedTemplate; } } if (!importTemplate.isSuccess()) { //ROLLBACK.. errrors statusMessage = uploadProgressService.addUploadStatus(importOptions.getUploadKey(), "Errors were found registering the template. Attempting to rollback."); rollbackTemplateImportInNifi(importTemplate, dto, oldTemplateXml); //also restore existing registered template with metadata //restore old registered template if (existingTemplate != null) { try { metadataService.registerTemplate(existingTemplate); } catch (Exception e) { Throwable root = ExceptionUtils.getRootCause(e); String msg = root != null ? root.getMessage() : e.getMessage(); importTemplate.getTemplateResults() .addError(NifiError.SEVERITY.WARN, "Error while restoring the template " + template.getTemplateName() + " in the Kylo metadata. " + msg, ""); } } statusMessage.update("Errors were found registering the template. Rolled back to previous version.", true); } //remove the temporary Process Group we created removeTemporaryProcessGroup(importTemplate); } else { importTemplate.setSuccess(true); } //if we also imported the reusable template make sure that is all running properly for (ImportTemplate connectingTemplate : connectingTemplates) { //enable it nifiRestClient.markConnectionPortsAsRunning(connectingTemplate.getTemplateResults().getProcessGroupEntity()); } completeSection(importOptions, ImportSection.Section.IMPORT_REGISTERED_TEMPLATE); } return importTemplate; } /** * Import a template string into NiFi as a template to use */ private NiFiTemplateImport importNiFiXmlTemplate(ImportTemplate template, ImportTemplateOptions importOptions) throws IOException { TemplateDTO dto = null; String templateName = null; String oldTemplateXml = null; ImportComponentOption nifiTemplateImport = importOptions.findImportComponentOption(ImportComponent.NIFI_TEMPLATE); if (nifiTemplateImport.isValidForImport()) { try { templateName = NifiTemplateParser.getTemplateName(template.getNifiTemplateXml()); template.setTemplateName(templateName); dto = nifiRestClient.getTemplateByName(templateName); if (dto != null) { oldTemplateXml = nifiRestClient.getTemplateXml(dto.getId()); template.setNifiTemplateId(dto.getId()); if (importOptions.isImportAndOverwrite(ImportComponent.NIFI_TEMPLATE) && nifiTemplateImport.isValidForImport()) { nifiRestClient.deleteTemplate(dto.getId()); } else if(!template.isZipFile()){ //if its not a zip file we need to error out if the user has decided not to overwrite when it already exists uploadProgressService.addUploadStatus(importOptions.getUploadKey(),"The template "+templateName+" already exists in NiFi. Please choose the option to replace the template and try again.",true,false); template.setValid(false); } } } catch (ParserConfigurationException | XPathExpressionException | SAXException e) { throw new UnsupportedOperationException("The xml file you are trying to import is not a valid NiFi template. Please try again. " + e.getMessage()); } boolean register = (dto == null || (importOptions.isImportAndOverwrite(ImportComponent.NIFI_TEMPLATE) && nifiTemplateImport.isValidForImport())); //attempt to import the xml into NiFi if its new, or if the user said to overwrite if (register) { UploadProgressMessage statusMessage = uploadProgressService.addUploadStatus(importOptions.getUploadKey(), "Importing " + templateName + " into NiFi"); log.info("Attempting to import Nifi Template: {} for file {}", templateName, template.getFileName()); dto = nifiRestClient.importTemplate(template.getTemplateName(), template.getNifiTemplateXml()); template.setNifiTemplateId(dto.getId()); statusMessage.update("Imported " + templateName + " into NiFi", true); } } completeSection(importOptions, ImportSection.Section.IMPORT_NIFI_TEMPLATE); return new NiFiTemplateImport(oldTemplateXml, dto); } private ImportTemplate validateAndImportZip(String fileName, byte[] content, ImportTemplateOptions importOptions) { this.accessController.checkPermission(AccessController.SERVICES, FeedServicesAccessControl.IMPORT_TEMPLATES); ImportTemplate importTemplate = validateTemplateForImport(fileName, content, importOptions); return metadataAccess.commit(() -> importZip(importTemplate)); } private NifiProcessGroup createTemplateInstance(ImportTemplate importTemplate, ImportTemplateOptions importOptions) { UploadProgressMessage statusMessage = uploadProgressService.addUploadStatus(importOptions.getUploadKey(), "Starting to import RegisteredTemplate. Attempting to verify NiFi flow "); //Create the new instance of the template in NiFi Map<String, Object> configProperties = propertyExpressionResolver.getStaticConfigProperties(); NifiProcessGroup newTemplateInstance = nifiRestClient.createNewTemplateInstance(importTemplate.getNifiTemplateId(), configProperties, false, new NifiFlowCacheReusableTemplateCreationCallback(false)); importTemplate.setTemplateResults(newTemplateInstance); if (newTemplateInstance.isSuccess()) { statusMessage.update("Verified NiFi flow.", true); } else { statusMessage.update("Error creating NiFi template instance ", false); } completeSection(importOptions, ImportSection.Section.CREATE_NIFI_INSTANCE); return newTemplateInstance; } /** * Register the template with the metadata and save it * * @param importTemplate the template data to import * @param importOptions user options about what/how it should be imported * @return the registered template that was saved */ private RegisteredTemplate registerTemplate(ImportTemplate importTemplate, ImportTemplateOptions importOptions) { RegisteredTemplate template = importTemplate.getTemplateToImport(); ImportComponentOption registeredTemplateOption = importOptions.findImportComponentOption(ImportComponent.TEMPLATE_DATA); if (registeredTemplateOption.isValidForImport()) { UploadProgressMessage statusMessage = uploadProgressService.addUploadStatus(importOptions.getUploadKey(), "Registering template " + template.getTemplateName() + " with Kylo metadata."); try { importTemplate.setNifiTemplateId(template.getNifiTemplateId()); //register it in the system metadataService.registerTemplate(template); //get the new template template = registeredTemplateService.findRegisteredTemplate(new RegisteredTemplateRequest.Builder().templateId(template.getId()).templateName(template.getTemplateName()).build()); importTemplate.setTemplateId(template.getId()); statusMessage.update("Registered template with Kylo metadata.", true); } catch (Exception e) { importTemplate.setSuccess(false); Throwable root = ExceptionUtils.getRootCause(e); String msg = root != null ? root.getMessage() : e.getMessage(); importTemplate.getTemplateResults().addError(NifiError.SEVERITY.WARN, "Error registering the template " + template.getTemplateName() + " in the Kylo metadata. " + msg, ""); statusMessage.update("Error registering template with Kylo metadata. " + msg, false); } } return template; } private List<ImportTemplate> importReusableTemplate(ImportTemplate importTemplate, ImportTemplateOptions importOptions) throws Exception { List<ImportTemplate> connectingTemplates = new ArrayList<>(); //start the import ImportComponentOption reusableComponentOption = importOptions.findImportComponentOption(ImportComponent.REUSABLE_TEMPLATE); if (importTemplate.hasConnectingReusableTemplate() && reusableComponentOption.isShouldImport() && reusableComponentOption.isValidForImport()) { UploadProgressMessage statusMessage = uploadProgressService.addUploadStatus(importOptions.getUploadKey(), "Import Reusable Template. Starting import"); //import the reusable templates boolean valid = true; ImportTemplate lastReusableTemplate = null; log.info("Importing Zip file template {}. first importing reusable flow from zip"); for (String reusableTemplateXml : importTemplate.getNifiConnectingReusableTemplateXmls()) { String name = NifiTemplateParser.getTemplateName(reusableTemplateXml); ImportTemplate connectingTemplate = importNifiTemplateWithTemplateString(name, reusableTemplateXml, importOptions, false); lastReusableTemplate = connectingTemplate; if (!connectingTemplate.isSuccess()) { //return with exception //add in the error messages connectingTemplate.getTemplateResults().getAllErrors().stream().forEach(nifiError -> { importTemplate.getTemplateResults().addError(nifiError); }); //error out importTemplate.setSuccess(false); reusableComponentOption.getErrorMessages().add("Error importing Reusable Template"); //exit valid = false; break; } else { connectingTemplates.add(connectingTemplate); } } if (valid) { statusMessage.update("Successfully imported Reusable Templates " + (connectingTemplates.stream().map(t -> t.getTemplateName()).collect(Collectors.joining(","))), true); } else { statusMessage.update("Errors importing reusable template: Imported Reusable Template. " + lastReusableTemplate != null ? lastReusableTemplate.getTemplateName() : ""); } } completeSection(importOptions, ImportSection.Section.IMPORT_REUSABLE_TEMPLATE); return connectingTemplates; } private ImportTemplate importNifiTemplateWithTemplateString(String fileName, String xmlFile, ImportTemplateOptions importOptions, boolean xmlImport) throws IOException { byte[] content = xmlFile.getBytes("UTF-8"); return importNifiTemplate(fileName, content, importOptions, xmlImport); } /** * * @param fileName * @param xmlFile * @param importOptions * @param xmlImport * @return * @throws IOException */ private ImportTemplate importNifiTemplate(String fileName, byte[] xmlFile, ImportTemplateOptions importOptions, boolean xmlImport) throws IOException { InputStream inputStream = new ByteArrayInputStream(xmlFile); ImportTemplate importTemplate = getNewNiFiTemplateImport(fileName, inputStream); importTemplate.setImportOptions(importOptions); validateNiFiTemplateImport(importTemplate); if (importTemplate.isValid()) { UploadProgressMessage importStatusMessage = uploadProgressService.addUploadStatus(importOptions.getUploadKey(), "Importing the NiFi flow "); boolean createReusableFlow = importOptions.isImport(ImportComponent.REUSABLE_TEMPLATE); boolean overwrite = importOptions.isImportAndOverwrite(ImportComponent.NIFI_TEMPLATE); log.info("Importing XML file template {}, overwrite: {}, reusableFlow: {}", fileName, overwrite, createReusableFlow); NiFiTemplateImport niFiTemplateImport = importNiFiXmlTemplate(importTemplate, importOptions); String oldTemplateXml = niFiTemplateImport.getOldTemplateXml(); TemplateDTO dto = niFiTemplateImport.getDto(); String templateName = importTemplate.getTemplateName(); importStatusMessage.update("Importing the NiFi flow, " + templateName); log.info("validate NiFi Flow by creating a template instance in nifi Nifi Template: {} for file {}", templateName, fileName); Map<String, Object> configProperties = propertyExpressionResolver.getStaticConfigProperties(); NifiFlowCacheReusableTemplateCreationCallback reusableTemplateCreationCallback = new NifiFlowCacheReusableTemplateCreationCallback(xmlImport); if (createReusableFlow && importTemplate.isValid()) { importStatusMessage.update("Creating reusable flow instance for " + templateName); } if(importTemplate.isValid()) { NifiProcessGroup newTemplateInstance = nifiRestClient.createNewTemplateInstance(dto.getId(), configProperties, createReusableFlow, reusableTemplateCreationCallback); importTemplate.setTemplateResults(newTemplateInstance); log.info("Import finished for {}, {}... verify results", templateName, fileName); if (newTemplateInstance.isSuccess()) { log.info("SUCCESS! This template is valid Nifi Template: {} for file {}", templateName, fileName); importTemplate.setSuccess(true); if (createReusableFlow) { importStatusMessage.update("Finished creating reusable flow instance for " + templateName, true); importReusableTemplateSuccess(importTemplate); } else { importStatusMessage.update("Validated " + templateName, true); } } else { rollbackTemplateImportInNifi(importTemplate, dto, oldTemplateXml); importStatusMessage.update("An error occurred importing the template, " + templateName, false); } if (!createReusableFlow) { removeTemporaryProcessGroup(importTemplate); try { nifiRestClient.deleteProcessGroup(newTemplateInstance.getProcessGroupEntity()); } catch (NifiComponentNotFoundException e) { //its ok if its not found } log.info("Success cleanup: Successfully cleaned up Nifi"); } } log.info("Import NiFi Template for {} finished", fileName); } return importTemplate; } /** * Restore the previous Template back to Nifi */ private void rollbackTemplateImportInNifi(ImportTemplate importTemplate, TemplateDTO dto, String oldTemplateXml) throws IOException { log.error("ERROR! This template is NOT VALID Nifi Template: {} for file {}. Errors are: {} ", importTemplate.getTemplateName(), importTemplate.getFileName(), importTemplate.getTemplateResults().getAllErrors()); //delete this template importTemplate.setSuccess(false); //delete the template from NiFi nifiRestClient.deleteTemplate(dto.getId()); //restore old template if (oldTemplateXml != null) { log.info("Rollback Nifi: Attempt to restore old template xml "); nifiRestClient.importTemplate(oldTemplateXml); log.info("Rollback Nifi: restored old template xml "); } log.info("Rollback Nifi: Deleted the template: {} from Nifi ", importTemplate.getTemplateName()); } /** * Cleanup and remove the temporary process group associated with this import * * This should be called after the import to cleanup NiFi */ private void removeTemporaryProcessGroup(ImportTemplate importTemplate) { UploadProgressMessage statusMessage = uploadProgressService.addUploadStatus(importTemplate.getImportOptions().getUploadKey(), "Cleaning up the temporary process group in NiFi"); String processGroupName = importTemplate.getTemplateResults().getProcessGroupEntity().getName(); String processGroupId = importTemplate.getTemplateResults().getProcessGroupEntity().getId(); String parentProcessGroupId = importTemplate.getTemplateResults().getProcessGroupEntity().getParentGroupId(); log.info("About to cleanup the temporary process group {} ({})", processGroupName, processGroupId); try { //Now try to remove the processgroup associated with this template import ProcessGroupDTO e = null; try { e = nifiRestClient.getProcessGroup(processGroupId, false, false); } catch (NifiComponentNotFoundException notFound) { //if its not there then we already cleaned up :) } if (e != null) { try { nifiRestClient.stopAllProcessors(processGroupId, parentProcessGroupId); //remove connections nifiRestClient.removeConnectionsToProcessGroup(parentProcessGroupId, processGroupId); } catch (Exception e2) { //this is ok. we are cleaning up the template so if an error occurs due to no connections it is fine since we ultimately want to remove this temp template. } try { nifiRestClient.deleteProcessGroup(importTemplate.getTemplateResults().getProcessGroupEntity()); } catch (NifiComponentNotFoundException nfe) { //this is ok } } log.info("Successfully cleaned up Nifi and deleted the process group {} ", importTemplate.getTemplateResults().getProcessGroupEntity().getName()); statusMessage.update("Cleaned up NiFi", true); } catch (NifiClientRuntimeException e) { log.error("error attempting to cleanup and remove the temporary process group (" + processGroupId + " during the import of template " + importTemplate.getTemplateName()); importTemplate.getTemplateResults().addError(NifiError.SEVERITY.WARN, "Issues found in cleaning up the template import: " + importTemplate.getTemplateName() + ". The Process Group : " + processGroupName + " (" + processGroupId + ")" + " may need to be manually cleaned up in NiFi ", ""); statusMessage.update("Error cleaning up NiFi. The Process Group : " + processGroupName + " (" + processGroupId + ")", false); } } /** * Open the zip file and populate the {@link ImportTemplate} object with the components in the file/archive * * @param fileName the file name * @param inputStream the file * @return the template data to import */ private ImportTemplate openZip(String fileName, InputStream inputStream) throws IOException { byte[] buffer = new byte[1024]; ZipInputStream zis = new ZipInputStream(inputStream); ZipEntry zipEntry; ImportTemplate importTemplate = new ImportTemplate(fileName); while ((zipEntry = zis.getNextEntry()) != null) { String zipEntryContents = ZipFileUtil.zipEntryToString(buffer, zis, zipEntry); if (zipEntry.getName().startsWith(NIFI_TEMPLATE_XML_FILE)) { importTemplate.setNifiTemplateXml(zipEntryContents); } else if (zipEntry.getName().startsWith(TEMPLATE_JSON_FILE)) { importTemplate.setTemplateJson(zipEntryContents); } else if (zipEntry.getName().startsWith(NIFI_CONNECTING_REUSABLE_TEMPLATE_XML_FILE)) { importTemplate.addNifiConnectingReusableTemplateXml(zipEntryContents); } } zis.closeEntry(); zis.close(); if (!importTemplate.hasValidComponents()) { throw new UnsupportedOperationException( " The file you uploaded is not a valid archive. Please ensure the Zip file has been exported from the system and has 2 valid files named: " + NIFI_TEMPLATE_XML_FILE + ", and " + TEMPLATE_JSON_FILE); } importTemplate.setZipFile(true); return importTemplate; } private ImportTemplate getNewNiFiTemplateImport(String fileName, InputStream inputStream) throws IOException { ImportTemplate template = new ImportTemplate(fileName); template.setValid(true); StringWriter writer = new StringWriter(); IOUtils.copy(inputStream, writer, "UTF-8"); String xmlTemplate = writer.toString(); template.setNifiTemplateXml(xmlTemplate); return template; } /** * Called when the system imports a Reusable template either from a ZIP file or an xml file uploaded in kylo. */ private void importReusableTemplateSuccess(ImportTemplate importTemplate) { } //Internal classes public static class ImportTemplate { String fileName; String templateName; boolean success; private boolean valid; private NifiProcessGroup templateResults; private List<NiFiComponentErrors> controllerServiceErrors; private String templateId; private String nifiTemplateId; private boolean zipFile; private String nifiTemplateXml; private String templateJson; private List<String> nifiConnectingReusableTemplateXmls = new ArrayList<>(); private boolean verificationToReplaceConnectingResuableTemplateNeeded; private ImportTemplateOptions importOptions; @JsonIgnore private RegisteredTemplate templateToImport; public ImportTemplate() { } public ImportTemplate(String fileName) { this.fileName = fileName; } public ImportTemplate(String templateName, boolean success) { this.templateName = templateName; this.success = success; } public boolean isValid() { return valid; } public void setValid(boolean valid) { this.valid = valid; } @JsonIgnore public boolean hasValidComponents() { return StringUtils.isNotBlank(templateJson) && StringUtils.isNotBlank(nifiTemplateXml); } public String getTemplateName() { return templateName; } public void setTemplateName(String templateName) { this.templateName = templateName; } public boolean isSuccess() { return success; } public void setSuccess(boolean success) { this.success = success; } public String getNifiTemplateXml() { return nifiTemplateXml; } public void setNifiTemplateXml(String nifiTemplateXml) { this.nifiTemplateXml = nifiTemplateXml; } public String getTemplateJson() { return templateJson; } public void setTemplateJson(String templateJson) { this.templateJson = templateJson; } public NifiProcessGroup getTemplateResults() { if (templateResults == null) { templateResults = new NifiProcessGroup(); } return templateResults; } public void setTemplateResults(NifiProcessGroup templateResults) { this.templateResults = templateResults; inspectForControllerServiceErrors(); } public String getFileName() { return fileName; } public boolean isZipFile() { return zipFile; } public void setZipFile(boolean zipFile) { this.zipFile = zipFile; } private void inspectForControllerServiceErrors() { if (templateResults != null) { List<NiFiComponentErrors> errors = templateResults.getControllerServiceErrors(); this.controllerServiceErrors = errors; } } public List<NiFiComponentErrors> getControllerServiceErrors() { return controllerServiceErrors; } public String getTemplateId() { return templateId; } public void setTemplateId(String templateId) { this.templateId = templateId; } public String getNifiTemplateId() { return nifiTemplateId; } public void setNifiTemplateId(String nifiTemplateId) { this.nifiTemplateId = nifiTemplateId; } public List<String> getNifiConnectingReusableTemplateXmls() { return nifiConnectingReusableTemplateXmls; } public void addNifiConnectingReusableTemplateXml(String nifiConnectingReusableTemplateXml) { this.nifiConnectingReusableTemplateXmls.add(nifiConnectingReusableTemplateXml); } public boolean hasConnectingReusableTemplate() { return !nifiConnectingReusableTemplateXmls.isEmpty(); } public boolean isVerificationToReplaceConnectingResuableTemplateNeeded() { return verificationToReplaceConnectingResuableTemplateNeeded; } public void setVerificationToReplaceConnectingResuableTemplateNeeded(boolean verificationToReplaceConnectingResuableTemplateNeeded) { this.verificationToReplaceConnectingResuableTemplateNeeded = verificationToReplaceConnectingResuableTemplateNeeded; } public ImportTemplateOptions getImportOptions() { return importOptions; } public void setImportOptions(ImportTemplateOptions importOptions) { this.importOptions = importOptions; } @JsonIgnore public RegisteredTemplate getTemplateToImport() { if (templateToImport == null && StringUtils.isNotBlank(templateJson)) { templateToImport = ObjectMapperSerializer.deserialize(getTemplateJson(), RegisteredTemplate.class); } return templateToImport; } @JsonIgnore public void setTemplateToImport(RegisteredTemplate templateToImport) { this.templateToImport = templateToImport; } } public class ExportTemplate { private String fileName; private byte[] file; public ExportTemplate(String fileName, byte[] file) { this.fileName = fileName; this.file = file; } public String getFileName() { return fileName; } public byte[] getFile() { return file; } } /** * Store data about a NiFi import */ private class NiFiTemplateImport { private String oldTemplateXml; private TemplateDTO dto; public NiFiTemplateImport(String oldTemplateXml, TemplateDTO dto) { this.oldTemplateXml = oldTemplateXml; this.dto = dto; } public String getOldTemplateXml() { return oldTemplateXml; } public void setOldTemplateXml(String oldTemplateXml) { this.oldTemplateXml = oldTemplateXml; } public TemplateDTO getDto() { return dto; } public void setDto(TemplateDTO dto) { this.dto = dto; } } public class NifiFlowCacheReusableTemplateCreationCallback implements ReusableTemplateCreationCallback { private boolean isXmlImport; public NifiFlowCacheReusableTemplateCreationCallback(boolean isXmlImport) { this.isXmlImport = isXmlImport; } /** * Update the NiFi Flow Cache with the new processors information * * @param templateName the name of the template * @param processGroupDTO the group where this template resides (under the reusable_templates) group */ @Override public void beforeMarkAsRunning(String templateName, ProcessGroupDTO processGroupDTO) { //update the cache Collection<ProcessorDTO> processors = NifiProcessUtil.getProcessors(processGroupDTO); nifiFlowCache.updateProcessorIdNames(templateName, processors); nifiFlowCache.updateConnectionMap(templateName, NifiConnectionUtil.getAllConnections(processGroupDTO)); } } }