From f347869ceefd65d570fa5f9ab6d8daa6db2f4b57 Mon Sep 17 00:00:00 2001 From: "Yolanda M. Davis" Date: Wed, 31 May 2017 10:17:23 -0400 Subject: [PATCH 1/4] NIFI-3696 - initial commit of ConfigMigration and FileManager tools --- .../main/asciidoc/administration-guide.adoc | 77 +++ .../toolkit/admin/AbstractAdminTool.groovy | 48 +- .../configmigrator/ConfigMigrator.groovy | 227 +++++++ .../rules/ConfigMigrationRule.groovy | 25 + .../rules/GenericMigrationRule.groovy | 32 + .../rules/PropertyMigrationRule.groovy | 98 +++ .../rules/XmlMigrationRule.groovy | 45 ++ .../admin/filemanager/FileManagerTool.groovy | 599 ++++++++++++++++++ .../admin/nodemanager/NodeManagerTool.groovy | 50 +- .../admin/notify/NotificationTool.groovy | 9 +- .../nifi/toolkit/admin/util/AdminUtil.groovy | 41 ++ .../configmigrator/ConfigMigratorSpec.groovy | 224 +++++++ .../filemanager/FileManagerToolSpec.groovy | 511 +++++++++++++++ .../toolkit/admin/util/AdminUtilSpec.groovy | 31 + .../conf/bootstrap-notification-services.xml | 46 ++ .../src/test/resources/conf/logback.xml | 169 +++++ .../filemanager/nifi-test-archive.tar.gz | Bin 27167 -> 29306 bytes .../rules/v1_1_0/authorizations-xml.groovy | 35 + .../rules/v1_2_0/authorizations-xml.groovy | 35 + .../rules/v1_2_0/authorizers-xml.groovy | 35 + .../rules/v1_2_0/bootstrap-conf.groovy | 36 ++ ...bootstrap-notification-services-xml.groovy | 46 ++ .../resources/rules/v1_2_0/logback-xml.groovy | 70 ++ .../login-identity-providers-xml.groovy | 36 ++ .../rules/v1_2_0/nifi-properties.groovy | 33 + .../rules/v1_2_0/state-management-xml.groovy | 37 ++ .../resources/rules/v1_2_0/users-xml.groovy | 35 + .../rules/v1_2_0/zookeeper-properties.groovy | 35 + .../rules/v1_3_0/authorizations-xml.groovy | 35 + .../conf/bootstrap-notification-services.xml | 53 ++ .../test/resources/upgrade/conf/logback.xml | 170 +++++ .../src/main/assembly/dependencies.xml | 5 + .../src/main/resources/bin/file-manager.bat | 39 ++ .../src/main/resources/bin/file-manager.sh | 119 ++++ .../resources/rules/v1_2_0/logback-xml.groovy | 70 ++ .../rules/v1_2_0/nifi-properties.groovy | 35 + 36 files changed, 3124 insertions(+), 67 deletions(-) create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/configmigrator/ConfigMigrator.groovy create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/configmigrator/rules/ConfigMigrationRule.groovy create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/configmigrator/rules/GenericMigrationRule.groovy create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/configmigrator/rules/PropertyMigrationRule.groovy create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/configmigrator/rules/XmlMigrationRule.groovy create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/filemanager/FileManagerTool.groovy create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/groovy/org/apache/nifi/toolkit/admin/configmigrator/ConfigMigratorSpec.groovy create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/groovy/org/apache/nifi/toolkit/admin/filemanager/FileManagerToolSpec.groovy create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/conf/bootstrap-notification-services.xml create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/conf/logback.xml create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/rules/v1_1_0/authorizations-xml.groovy create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/rules/v1_2_0/authorizations-xml.groovy create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/rules/v1_2_0/authorizers-xml.groovy create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/rules/v1_2_0/bootstrap-conf.groovy create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/rules/v1_2_0/bootstrap-notification-services-xml.groovy create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/rules/v1_2_0/logback-xml.groovy create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/rules/v1_2_0/login-identity-providers-xml.groovy create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/rules/v1_2_0/nifi-properties.groovy create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/rules/v1_2_0/state-management-xml.groovy create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/rules/v1_2_0/users-xml.groovy create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/rules/v1_2_0/zookeeper-properties.groovy create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/rules/v1_3_0/authorizations-xml.groovy create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/upgrade/conf/bootstrap-notification-services.xml create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/upgrade/conf/logback.xml create mode 100644 nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/file-manager.bat create mode 100644 nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/file-manager.sh create mode 100644 nifi-toolkit/nifi-toolkit-assembly/src/main/resources/rules/v1_2_0/logback-xml.groovy create mode 100644 nifi-toolkit/nifi-toolkit-assembly/src/main/resources/rules/v1_2_0/nifi-properties.groovy diff --git a/nifi-docs/src/main/asciidoc/administration-guide.adoc b/nifi-docs/src/main/asciidoc/administration-guide.adoc index bc823bcc6aa4..84b1df712d5e 100644 --- a/nifi-docs/src/main/asciidoc/administration-guide.adoc +++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc @@ -1184,6 +1184,7 @@ and clustered environments. These utilities include: * Notify -- The notification tool allows administrators to send bulletins to the NiFi UI using the command line. * Node Manager -- The node manager tool allows administrators to perform a status check on a node as well as to connect, disconnect, or remove nodes that are part of a cluster. +* File Manager -- The file manager tool allows administrators to backup, install or restore a NiFi installation from backup. The admin toolkit is bundled with the nifi-toolkit and can be executed with scripts found in the _bin_ folder. @@ -1317,6 +1318,82 @@ cluster if restarted and the flow for the cluster has not changed. If the flow w the removed node should be deleted before restarting the node to allow it to obtain the cluster flow (otherwise an uninheritable flow file exception may occur). +=== File Manager + +The File Manager utility allows system administrators to take a backup of an existing NiFi installation, install a new version of NiFi +in a designated location (while migrating any previous configuration settings) or restore an installation from a previous backup. +File Manager supports NiFi version 1.0.0 and higher and is available in 'file-manager.bat' file for use on Windows machines. + +To show help: + + file-manager.sh -h + +The following are available options: + +* `-o,--operation ` File operation (install | backup | restore) +* `-b,--backupDir ` Backup NiFi Directory (used with backup or restore operation) +* `-c,--nifiCurrentDir ` Current NiFi Installation Directory (used optionally with install or restore operation) +* `-d,--nifiInstallDir ` NiFi Installation Directory (used with install or restore operation) +* `-i,--installFile ` NiFi Install File (used with install operation) +* `-r,--nifiRollbackDir ` NiFi Installation Directory (used with install or restore operation) +* `-t,--bootstrapConf ` Current NiFi Bootstrap Configuration File (used optionally) +* `-m,--moveRepositories` Allow repositories to be moved to new/restored nifi directory from existing installation, if available (used optionally with install or restore operation) +* `-x,--overwriteConfigs` Overwrite existing configuration directory with upgrade changes (used optionally with install or restore operation) +* `-v,--verbose` Verbose messaging (optional) +* `-h,--help` Print help info (optional) + +Example usage on Linux: + + # backup NiFi installation + # option -t may be provided to ensure backup of external boostrap.conf file + ./file-manager.sh + -o backup + –b /tmp/nifi_bak + –c /usr/nifi_old + -v + + # install NiFi using compressed tar file into /usr/nifi directory (should install as /usr/nifi/nifi-1.3.0). + # migrate existing configurations with location determined by external bootstrap.conf and move over repositories from nifi_old + # options -t and -c should both be provided if migration of configurations, state and repositories are required + ./file-manager.sh + -o install + –i nifi-1.3.0.tar.gz + –d /usr/nifi + –c /usr/nifi/nifi_old + -t /usr/nifi/old_conf/bootstrap.conf + -v + -m + + # restore NiFi installation from backup directory and move back repositories + # option -t may be provided to ensure bootstrap.conf is restored to the file path provided, otherwise it is placed in the + # default directory under the rollback path (e.g. /usr/nifi_old/conf) + ./file-manager.sh + -o restore + –b /tmp/nifi_bak + –r /usr/nifi_old + –c /usr/nifi + -m + -v + +=== Expected Behavior + +Backup: + +During the backup operation a backup directory is created in a designated location for an existing NiFi installation. Backups will capture all critical files +(including any internal or external configurations, libraries, scripts and documents) however it excludes backing up repositories and logs due to potential size. +If configuration/library files are external from the existing installation folder the backup operation will capture those as well. + +Install: + +During the install operation File Manager will perform installation using the designated NiFi binary file (either tar.gz or zip file) +to create a new installation or migrate an existing nifi installation to a new one. Installation can optionally move repositories (if located within the configuration +folder of the current installation) to the new installation as well as migrate configuration files to the newer installation. + +Restore: + +The restore operation allows an existing installation to revert back to a previous installation. Using an existing backup directory (created from the backup operation) +the FileManager utility will restore libraries, scripts and documents as well as revert to previous configurations. + [[clustering]] Clustering Configuration diff --git a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/AbstractAdminTool.groovy b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/AbstractAdminTool.groovy index 79c5ef1f2ba2..9d1eeae8518e 100644 --- a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/AbstractAdminTool.groovy +++ b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/AbstractAdminTool.groovy @@ -19,11 +19,7 @@ package org.apache.nifi.toolkit.admin import org.apache.nifi.toolkit.admin.util.AdminUtil import org.apache.commons.cli.HelpFormatter import org.apache.commons.cli.Options -import org.apache.commons.lang3.SystemUtils -import org.apache.nifi.toolkit.admin.util.Version -import org.apache.nifi.util.StringUtils import org.slf4j.Logger -import java.nio.file.Path import java.nio.file.Paths public abstract class AbstractAdminTool { @@ -65,45 +61,11 @@ public abstract class AbstractAdminTool { protected abstract Logger getLogger() - Properties getBootstrapConf(Path bootstrapConfFileName) { - Properties bootstrapProperties = new Properties() - File bootstrapConf = bootstrapConfFileName.toFile() - bootstrapProperties.load(new FileInputStream(bootstrapConf)) - return bootstrapProperties - } - - String getRelativeDirectory(String directory, String rootDirectory) { - if (directory.startsWith("./")) { - final String directoryUpdated = SystemUtils.IS_OS_WINDOWS ? File.separator + directory.substring(2,directory.length()) : directory.substring(1,directory.length()) - rootDirectory + directoryUpdated - } else { - directory - } - } - - Boolean supportedNiFiMinimumVersion(final String nifiConfDirName, final String nifiLibDirName, final String supportedMinimumVersion){ - final File nifiConfDir = new File(nifiConfDirName) - final File nifiLibDir = new File (nifiLibDirName) - final String versionStr = AdminUtil.getNiFiVersion(nifiConfDir,nifiLibDir) - - if(!StringUtils.isEmpty(versionStr)){ - Version version = new Version(versionStr.replace("-","."),".") - Version minVersion = new Version(supportedMinimumVersion,".") - Version.VERSION_COMPARATOR.compare(version,minVersion) >= 0 - }else{ - return false - } - - } - - Boolean supportedNiFiMinimumVersion(final String nifiCurrentDirName, final String supportedMinimumVersion){ - final String bootstrapConfFileName = Paths.get(nifiCurrentDirName,"conf","bootstrap.conf").toString() - final File bootstrapConf = new File(bootstrapConfFileName) - final Properties bootstrapProperties = getBootstrapConf(Paths.get(bootstrapConfFileName)) - final String parentPathName = bootstrapConf.getCanonicalFile().getParentFile().getParentFile().getCanonicalPath() - final String nifiConfDir = getRelativeDirectory(bootstrapProperties.getProperty("conf.dir"),parentPathName) - final String nifiLibDir = getRelativeDirectory(bootstrapProperties.getProperty("lib.dir"),parentPathName) - return supportedNiFiMinimumVersion(nifiConfDir,nifiLibDir,supportedMinimumVersion) + Boolean supportedNiFiMinimumVersion(final String nifiCurrentDirName, final String bootstrapConfFileName, final String supportedMinimumVersion){ + final Properties bootstrapProperties = AdminUtil.getBootstrapConf(Paths.get(bootstrapConfFileName)) + final String nifiConfDir = AdminUtil.getRelativeDirectory(bootstrapProperties.getProperty("conf.dir"),nifiCurrentDirName) + final String nifiLibDir = AdminUtil.getRelativeDirectory(bootstrapProperties.getProperty("lib.dir"),nifiCurrentDirName) + return AdminUtil.supportedNiFiMinimumVersion(nifiConfDir,nifiLibDir,supportedMinimumVersion) } diff --git a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/configmigrator/ConfigMigrator.groovy b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/configmigrator/ConfigMigrator.groovy new file mode 100644 index 000000000000..bafc87e499b6 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/configmigrator/ConfigMigrator.groovy @@ -0,0 +1,227 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.nifi.toolkit.admin.configmigrator + +import com.google.common.collect.Lists +import com.google.common.io.Files +import org.apache.nifi.toolkit.admin.util.AdminUtil +import org.apache.commons.cli.ParseException +import org.apache.commons.io.FileUtils +import org.apache.nifi.toolkit.admin.util.Version +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import java.nio.file.Path +import java.nio.file.Paths + +public class ConfigMigrator { + + private final static String SUPPORTED_MINIMUM_VERSION = "1.0.0" + private final String RULES_DIR = getRulesDirectory() + private final Boolean overwrite + protected Logger logger = LoggerFactory.getLogger(ConfigMigrator.class) + protected final Boolean isVerbose + + public ConfigMigrator(Boolean verbose, Boolean overwrite) { + this.overwrite = overwrite + this.isVerbose = verbose + } + + String getRulesDirectory() { + final ClassLoader cl = this.getClass().getClassLoader() + cl.getResource("rules").path.replaceAll("%20"," "); + } + + List getRulesDirectoryName(final String currentVersion, final String upgradeVersion) { + Version current = new Version(currentVersion.take(5).toString(),".") + Version upgrade = new Version(upgradeVersion.take(5).toString(),".") + File rulesDir = new File(rulesDirectory) + List rules = Lists.newArrayList(rulesDir.listFiles()) + List versions = rules.collect { new Version(it.name.substring(1,it.name.length()),"_")} + versions.sort(Version.VERSION_COMPARATOR) + List matches = versions.findAll { Version.VERSION_COMPARATOR.compare(it,upgrade) <= 0 && Version.VERSION_COMPARATOR.compare(it,current) == 1} + + if(matches.isEmpty()){ + null + }else{ + matches.sort(Version.VERSION_COMPARATOR) + List directoryNames = [] + matches.each { directoryNames.add(RULES_DIR + File.separator + "v" + it.toString()) } + return directoryNames + } + } + + Boolean supportedVersion(final File script, final String currentVersion) { + final Class ruleClass = new GroovyClassLoader(getClass().getClassLoader()).parseClass(script); + final GroovyObject ruleObject = (GroovyObject) ruleClass.newInstance(); + ruleObject.invokeMethod("supportedVersion", [currentVersion]) + } + + byte[] migrateContent(final File script, final byte[] content, final byte[] upgradeContent) { + final Class ruleClass = new GroovyClassLoader(getClass().getClassLoader()).parseClass(script); + final GroovyObject ruleObject = (GroovyObject) ruleClass.newInstance(); + ruleObject.invokeMethod("migrate", [content, upgradeContent]) + } + + String getScriptRuleName(final String fileName) { + fileName.replace(".", "-") + ".groovy" + } + + File getUpgradeFile(File upgradeDir, String fileName){ + + final File[] upgradeFiles = upgradeDir.listFiles(new FilenameFilter() { + @Override + boolean accept(File dir, String name) { + name.equals(fileName) + } + }) + + upgradeFiles.size() == 1 ? upgradeFiles[0] : new File(upgradeDir.path + File.separator + fileName) + } + + void migrate(final File nifiConfDir, final File nifiLibDir, final File nifiUpgradeConfigDir, final File nifiUpgradeLibDir, final File boostrapConf, final String nifiCurrentDir) { + + final String nifiCurrentVersion = AdminUtil.getNiFiVersion(nifiConfDir,nifiLibDir) + final String nifiUpgradeVersion = AdminUtil.getNiFiVersion(nifiUpgradeConfigDir,nifiUpgradeLibDir) + + if (nifiCurrentVersion == null) { + throw new IllegalArgumentException("Could not determine current nifi version") + } + + if (nifiUpgradeVersion == null) { + throw new IllegalArgumentException("Could not determine upgrade nifi version") + } + + final List nifiConfigFiles = Lists.newArrayList(nifiConfDir.listFiles()) + nifiConfigFiles.add(boostrapConf) + + //obtain the rule directories sorted for each version in between current version and upgrade + final List ruleDirs = getRulesDirectoryName(nifiCurrentVersion,nifiUpgradeVersion) + + //iterate through all rule scripts in each directory and apply to file + if(ruleDirs != null) { + + nifiConfigFiles.each { file -> + + if (!file.isDirectory()) { + + final String scriptName = getScriptRuleName(file.getName()) + def byte[] content = file.bytes + def upgradeFile = getUpgradeFile(nifiUpgradeConfigDir, file.name) + + ruleDirs.each { ruleDir -> + + final File script = new File(ruleDir + File.separator + scriptName) + + if (script.exists() && supportedVersion(script, nifiCurrentVersion)) { + + if (isVerbose) { + logger.info("Applying rules to {} from directory {} ", file.name, ruleDir) + } + + content = migrateContent(script, content, upgradeFile.exists() ? upgradeFile.bytes : new byte[0]) + + } else { + if (isVerbose) { + logger.info("No migration rule exists in {} for file {}. ",ruleDir,file.getName()) + } + } + + } + + //if file is external from current installation and overwrite is allowed then write to external location + //otherwise write to new/upgraded location + if (file.parentFile.parentFile!= null && file.parentFile.parentFile.toString() != nifiCurrentDir && this.overwrite) { + Files.write(content, file) + } else { + Files.write(content, upgradeFile) + } + + }else{ + if(!this.overwrite){ + FileUtils.copyDirectoryToDirectory(file, nifiUpgradeConfigDir) + } + } + } + + }else{ + if(isVerbose) { + logger.info("No upgrade rules are required for these configurations.") + } + if(!this.overwrite){ + + if(isVerbose) { + logger.info("Copying configurations over to upgrade directory") + } + + nifiConfigFiles.each { file -> + if(file.isDirectory()){ + FileUtils.copyDirectoryToDirectory(file, nifiUpgradeConfigDir) + }else { + FileUtils.copyFileToDirectory(file, nifiUpgradeConfigDir) + } + } + } + } + + } + + public void run(final String nifiCurrentDir, final String bootstrapConfFile, final String nifiUpgDirString) throws ParseException, IllegalArgumentException { + + Path bootstrapConfPath = Paths.get(bootstrapConfFile) + File bootstrapConf = Paths.get(bootstrapConfFile).toFile() + + if (!bootstrapConf.exists()) { + throw new IllegalArgumentException("NiFi Bootstrap File provided does not exist: " + bootstrapConfFile) + } + + Properties bootstrapProperties = AdminUtil.getBootstrapConf(bootstrapConfPath) + File nifiConfDir = new File(AdminUtil.getRelativeDirectory(bootstrapProperties.getProperty("conf.dir"), nifiCurrentDir)) + File nifiLibDir = new File(AdminUtil.getRelativeDirectory(bootstrapProperties.getProperty("lib.dir"), nifiCurrentDir)) + final File nifiUpgradeConfDir = Paths.get(nifiUpgDirString,"conf").toFile() + final File nifiUpgradeLibDir = Paths.get(nifiUpgDirString,"lib").toFile() + + if(AdminUtil.supportedNiFiMinimumVersion(nifiConfDir.canonicalPath, nifiLibDir.canonicalPath, SUPPORTED_MINIMUM_VERSION) && + AdminUtil.supportedNiFiMinimumVersion(nifiUpgradeConfDir.canonicalPath, nifiUpgradeLibDir.canonicalPath, SUPPORTED_MINIMUM_VERSION)) { + + if (!nifiConfDir.exists() || !nifiConfDir.isDirectory()) { + throw new IllegalArgumentException("NiFi Configuration Directory provided is not valid: " + nifiConfDir.absolutePath) + } + + if (!nifiUpgradeConfDir.exists() || !nifiUpgradeConfDir.isDirectory()) { + throw new IllegalArgumentException("Upgrade Configuration Directory provided is not valid: " + nifiUpgradeConfDir) + } + + if (isVerbose) { + logger.info("Migrating configurations from {} to {}", nifiConfDir.absolutePath, nifiUpgradeConfDir.absolutePath) + } + + migrate(nifiConfDir,nifiLibDir,nifiUpgradeConfDir,nifiUpgradeLibDir,bootstrapConf,nifiCurrentDir) + + if (isVerbose) { + logger.info("Migration completed.") + } + + }else{ + throw new UnsupportedOperationException("Config Migration Tool only supports NiFi version 1.0.0 and above") + } + + } + + +} \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/configmigrator/rules/ConfigMigrationRule.groovy b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/configmigrator/rules/ConfigMigrationRule.groovy new file mode 100644 index 000000000000..cad7baf154e8 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/configmigrator/rules/ConfigMigrationRule.groovy @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.nifi.toolkit.admin.configmigrator.rules + +interface ConfigMigrationRule { + + Boolean supportedVersion(String version) + byte[] migrate(byte[] oldContent, byte[] upgradeContent) + +} \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/configmigrator/rules/GenericMigrationRule.groovy b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/configmigrator/rules/GenericMigrationRule.groovy new file mode 100644 index 000000000000..1089467cb9fb --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/configmigrator/rules/GenericMigrationRule.groovy @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.nifi.toolkit.admin.configmigrator.rules + +abstract class GenericMigrationRule implements ConfigMigrationRule { + + @Override + Boolean supportedVersion(String version) { + return true + } + + static String getContent(String[] args){ + File file = new File(args[0]) + return file.text + } + +} diff --git a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/configmigrator/rules/PropertyMigrationRule.groovy b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/configmigrator/rules/PropertyMigrationRule.groovy new file mode 100644 index 000000000000..693665745e4d --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/configmigrator/rules/PropertyMigrationRule.groovy @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.nifi.toolkit.admin.configmigrator.rules + +/** + * PropertyMigrationRule supports the migration of older key/value (property) based files to new versions + * by adding any new variables to the older file while maintaining existing configurations. Classes that extend this rule can also filter + * any properties that are no longer needed in new installations + */ +abstract class PropertyMigrationRule extends GenericMigrationRule{ + + private final static String LICENSE_COMMENTS = "# Licensed to the Apache Software Foundation (ASF) under one or more\n" + + "# contributor license agreements. See the NOTICE file distributed with\n" + + "# this work for additional information regarding copyright ownership.\n" + + "# The ASF licenses this file to You under the Apache License, Version 2.0\n" + + "# (the \"License\"); you may not use this file except in compliance with\n" + + "# the License. You may obtain a copy of the License at\n" + + "#\n" + + "# http://www.apache.org/licenses/LICENSE-2.0\n" + + "#\n" + + "# Unless required by applicable law or agreed to in writing, software\n" + + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n" + + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" + + "# See the License for the specific language governing permissions and\n" + + "# limitations under the License." + + /** + * + * @param oldContent + * @param upgradeContent + * @return + */ + @Override + byte[] migrate(byte[] oldContent, byte[] upgradeContent) { + def properties = new SortedProperties() + def upgradeProperties = new SortedProperties() + + properties.load(new ByteArrayInputStream(oldContent)) + upgradeProperties.load(new ByteArrayInputStream(upgradeContent)) + + Enumeration keys = (Enumeration) properties.keys() + + keys.each { key -> + if(keyAllowed(key)) { + upgradeProperties.put(key, properties.get(key)) + } + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream() + upgradeProperties.store(baos,LICENSE_COMMENTS) + baos.toByteArray() + } + + /** + * Return if a key should be included in the set of properties being migrated + * @param key + * @return boolean + */ + abstract boolean keyAllowed(Object key); + + class SortedProperties extends Properties{ + + @Override + public Enumeration keys() { + + Enumeration keysEnum = super.keys(); + Vector keyList = new Vector(); + keysEnum.each { e -> keyList.add(e)} + + Collections.sort(keyList,new Comparator() { + @Override + int compare(Object o1, Object o2) { + o1.toString().compareTo(o2.toString()) + } + }) + + keyList.elements() + } + + + } + +} diff --git a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/configmigrator/rules/XmlMigrationRule.groovy b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/configmigrator/rules/XmlMigrationRule.groovy new file mode 100644 index 000000000000..de36000a5379 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/configmigrator/rules/XmlMigrationRule.groovy @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.toolkit.admin.configmigrator.rules + + +/** + * XmlMigrationRule assists classes in migrating existing xml configurations to newer versions + * by converting incoming content to Xml nodes which can then be navigated and edited as required. + */ + +abstract class XmlMigrationRule extends GenericMigrationRule{ + + @Override + byte[] migrate(byte[] oldContent, byte[] upgradeContent) { + def oldBais = new ByteArrayInputStream(oldContent) + def newBais = new ByteArrayInputStream(upgradeContent) + migrateXml(new XmlParser().parse(oldBais), new XmlParser().parse(newBais)) + } + + abstract byte[] migrateXml(Node oldXmlContent, Node newXmlContent) + + protected byte[] convertToByteArray(Node xml){ + def writer = new StringWriter() + def nodePrinter = new XmlNodePrinter(new PrintWriter(writer)) + nodePrinter.setPreserveWhitespace(true) + nodePrinter.print(xml) + return writer.toString().bytes + } + + +} diff --git a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/filemanager/FileManagerTool.groovy b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/filemanager/FileManagerTool.groovy new file mode 100644 index 000000000000..0bf5f07f3630 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/filemanager/FileManagerTool.groovy @@ -0,0 +1,599 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.nifi.toolkit.admin.filemanager + +import com.google.common.collect.Sets +import org.apache.commons.cli.CommandLine +import org.apache.commons.cli.DefaultParser +import org.apache.commons.cli.Option +import org.apache.commons.cli.Options +import org.apache.commons.cli.ParseException +import org.apache.commons.compress.archivers.ArchiveEntry +import org.apache.commons.compress.archivers.ArchiveInputStream +import org.apache.commons.compress.archivers.ArchiveStreamFactory +import org.apache.commons.compress.archivers.tar.TarArchiveEntry +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry +import org.apache.commons.compress.archivers.zip.ZipFile +import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream +import org.apache.commons.compress.utils.IOUtils +import org.apache.commons.io.FileUtils +import org.apache.commons.io.FilenameUtils +import org.apache.commons.lang3.SystemUtils +import org.apache.nifi.toolkit.admin.AbstractAdminTool +import org.apache.nifi.toolkit.admin.configmigrator.ConfigMigrator +import org.apache.nifi.toolkit.admin.util.AdminUtil +import org.apache.nifi.util.StringUtils +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.attribute.PosixFilePermission + +public class FileManagerTool extends AbstractAdminTool{ + + private static final String DEFAULT_DESCRIPTION = "This tool is used to perform backup, install and restore activities for a NiFi node. " + private static final String HELP_ARG = "help" + private static final String VERBOSE_ARG = "verbose" + private static final String OPERATION = "operation" + private static final String NIFI_CURRENT_DIR = "nifiCurrentDir" + private static final String NIFI_INSTALL_DIR = "nifiInstallDir" + private static final String NIFI_ROLLBACK_DIR = "nifiRollbackDir" + private static final String BACKUP_DIR = "backupDir" + private static final String INSTALL_FILE = "installFile" + private static final String MOVE_REPOSITORIES = "moveRepositories" + private static final String OVERWRITE_CONFIGS = "overwriteConfigs" + private static final String BOOTSTRAP_CONF = "bootstrapConf" + private boolean moveRepositories = false + private final static String SUPPORTED_MINIMUM_VERSION = "1.0.0" + private static final List POSIX_PERMISSIONS = + [PosixFilePermission.OTHERS_EXECUTE, + PosixFilePermission.OTHERS_WRITE, + PosixFilePermission.OTHERS_READ, + PosixFilePermission.GROUP_EXECUTE, + PosixFilePermission.GROUP_WRITE, + PosixFilePermission.GROUP_READ, + PosixFilePermission.OWNER_EXECUTE, + PosixFilePermission.OWNER_WRITE, + PosixFilePermission.OWNER_READ]; + + + FileManagerTool() { + header = buildHeader(DEFAULT_DESCRIPTION) + setup() + } + + @Override + protected Logger getLogger() { + LoggerFactory.getLogger(FileManagerTool.class) + } + + protected Options getOptions(){ + final Options options = new Options() + options.addOption(Option.builder("o").longOpt(OPERATION).hasArg().desc("File operation (install | backup | restore)").build()) + options.addOption(Option.builder("b").longOpt(BACKUP_DIR).hasArg().desc("Backup NiFi Directory (used with backup or restore operation)").build()) + options.addOption(Option.builder("c").longOpt(NIFI_CURRENT_DIR).hasArg().desc("Current NiFi Installation Directory (used optionally with install or restore operation)").build()) + options.addOption(Option.builder("d").longOpt(NIFI_INSTALL_DIR).hasArg().desc("NiFi Installation Directory (used with install or restore operation)").build()) + options.addOption(Option.builder("i").longOpt(INSTALL_FILE).hasArg().desc("NiFi Install File").build()) + options.addOption(Option.builder("r").longOpt(NIFI_ROLLBACK_DIR).hasArg().desc("NiFi Installation Directory (used with install or restore operation)").build()) + options.addOption(Option.builder("t").longOpt(BOOTSTRAP_CONF).hasArg().desc("Current NiFi Bootstrap Configuration File (optional)").build()) + options.addOption(Option.builder("m").longOpt(MOVE_REPOSITORIES).desc("Allow repositories to be moved to new/restored nifi directory from existing installation, if available (used optionally with install or restore operation)").build()) + options.addOption(Option.builder("x").longOpt(OVERWRITE_CONFIGS).desc("Overwrite existing configuration directory with upgrade changes (used optionally with install or restore operation)").build()) + options.addOption(Option.builder("h").longOpt(HELP_ARG).desc("Print help info (optional)").build()) + options.addOption(Option.builder("v").longOpt(VERBOSE_ARG).desc("Set mode to verbose (optional, default is false)").build()) + + options + } + + Set fromMode(final long mode) { + + Set permissions = Sets.newHashSet(); + + POSIX_PERMISSIONS.eachWithIndex{ + perm,index -> + if ((mode & (1 << index)) != 0) { + permissions.add(perm); + } + } + + return permissions; + } + + Properties getProperties(Path confFileName){ + final Properties properties = new Properties() + final File confFile = confFileName.toFile() + properties.load(new FileInputStream(confFile)) + return properties + } + + boolean valid(File nifiDir){ + if(nifiDir.isDirectory() && Files.exists(Paths.get(nifiDir.absolutePath,"bin","nifi.sh"))){ + true + }else { + false + } + } + + void move(final String srcDir, final String oldDir, final String newDir){ + + final String oldPathName = srcDir.startsWith("./") ? oldDir + File.separator + srcDir.substring(2,srcDir.length()) : oldDir + File.separator + srcDir + final String newPathName = srcDir.startsWith("./") ? newDir + File.separator + srcDir.substring(2,srcDir.length()) : newDir + File.separator + srcDir + + final Path oldPath = Paths.get(oldPathName) + final Path newPath = Paths.get(newPathName) + + if(Files.exists(oldPath)) { + Files.move(oldPath, newPath) + } + + } + + void moveRepository(final String dirName, final String installDirName){ + + if(isVerbose){ + logger.info("Moving repositories from {} to {}:",dirName,installDirName) + } + + final String bootstrapConfFileName = dirName + File.separator + "conf" + File.separator + "bootstrap.conf" + final Properties bootstrapProperties = getProperties(Paths.get(bootstrapConfFileName)) + final String nifiPropertiesFile = AdminUtil.getRelativeDirectory(bootstrapProperties.getProperty("conf.dir"),dirName) + File.separator +"nifi.properties" + final Properties nifiProperties = getProperties(Paths.get(nifiPropertiesFile)) + final String flowFileDirectory = nifiProperties.getProperty("nifi.flowfile.repository.directory") + final String contentRepositoryDir = nifiProperties.getProperty("nifi.content.repository.directory.default") + final String provenanceRepositoryDir = nifiProperties.getProperty("nifi.provenance.repository.directory.default") + final String databaseDirectory = nifiProperties.getProperty("nifi.database.directory") + + if(flowFileDirectory.startsWith("./")){ + if(isVerbose){ + logger.info("Moving flowfile repo") + } + move(flowFileDirectory,dirName,installDirName) + } + + if(contentRepositoryDir.startsWith("./")){ + if(isVerbose){ + logger.info("Moving content repo") + } + move(contentRepositoryDir,dirName,installDirName) + } + + if(provenanceRepositoryDir.startsWith("./")){ + if(isVerbose){ + logger.info("Moving provenance repo") + } + move(provenanceRepositoryDir,dirName,installDirName) + } + + if(databaseDirectory.startsWith("./")){ + if(isVerbose){ + logger.info("Moving database repo") + } + move(databaseDirectory,dirName,installDirName) + } + } + + void copyState(final String currentNiFiDirName, final String installDirName){ + + File stateDir = Paths.get(currentNiFiDirName,"state").toFile() + + if(stateDir.exists()){ + + if(Files.exists(Paths.get(installDirName,"state"))){ + Files.delete(Paths.get(installDirName,"state")) + } + + FileUtils.copyDirectoryToDirectory(stateDir, Paths.get(installDirName).toFile()) + } + + } + + protected void setPosixPermissions(final ArchiveEntry entry, final File outputFile, final ZipFile zipFile){ + int mode = 0 + + if (entry instanceof TarArchiveEntry) { + mode = ((TarArchiveEntry) entry).getMode(); + + }else if(entry instanceof ZipArchiveEntry && zipFile != null){ + mode = zipFile.getEntry(entry.name).getUnixMode(); + } + + if(mode == 0){ + mode = outputFile.isDirectory()? TarArchiveEntry.DEFAULT_DIR_MODE: TarArchiveEntry.DEFAULT_FILE_MODE + } + + Set permissions = fromMode(mode) + if(permissions.size() > 0) { + Files.setPosixFilePermissions(outputFile.toPath(), fromMode(mode)); + } + + } + + protected void setPosixPermissions(final File file,List permissions = []){ + + if (SystemUtils.IS_OS_WINDOWS) { + file?.setReadable(permissions.contains(PosixFilePermission.OWNER_READ)) + file?.setWritable(permissions.contains(PosixFilePermission.OWNER_WRITE)) + file?.setExecutable(permissions.contains(PosixFilePermission.OWNER_EXECUTE)) + } else { + Files.setPosixFilePermissions(file?.toPath(), permissions as Set) + } + + } + + void backup(String backupNiFiDirName, String currentNiFiDirName, String bootstrapConfFileName){ + + if(isVerbose){ + logger.info("Creating backup in directory:" + backupNiFiDirName) + } + + final File backupNiFiDir = new File(backupNiFiDirName) + final Properties bootstrapProperties = getProperties(Paths.get(bootstrapConfFileName)) + final File confDir = new File(AdminUtil.getRelativeDirectory(bootstrapProperties.getProperty("conf.dir"),currentNiFiDirName)) + final File libDir = new File(AdminUtil.getRelativeDirectory(bootstrapProperties.getProperty("lib.dir"),currentNiFiDirName)) + + if( backupNiFiDir.exists() && backupNiFiDir.isDirectory()){ + backupNiFiDir.deleteDir() + } + + backupNiFiDir.mkdirs() + + Files.createDirectory(Paths.get(backupNiFiDirName,"bootstrap_files")) + FileUtils.copyFileToDirectory(Paths.get(bootstrapConfFileName).toFile(),Paths.get(backupNiFiDirName,"bootstrap_files").toFile()) + FileUtils.copyDirectoryToDirectory(Paths.get(currentNiFiDirName,"lib","bootstrap").toFile(),Paths.get(backupNiFiDirName,"bootstrap_files").toFile()) + Files.createDirectories(Paths.get(backupNiFiDirName,"conf")) + Files.createDirectories(Paths.get(backupNiFiDirName,"lib")) + FileUtils.copyDirectoryToDirectory(confDir,Paths.get(backupNiFiDirName).toFile()) + FileUtils.copyDirectoryToDirectory(libDir,Paths.get(backupNiFiDirName).toFile()) + FileUtils.copyDirectoryToDirectory(Paths.get(currentNiFiDirName,"bin").toFile(),new File(backupNiFiDirName)) + FileUtils.copyDirectoryToDirectory(Paths.get(currentNiFiDirName,"docs").toFile(),new File(backupNiFiDirName)) + FileUtils.copyFileToDirectory(Paths.get(currentNiFiDirName,"LICENSE").toFile(),new File(backupNiFiDirName)) + FileUtils.copyFileToDirectory(Paths.get(currentNiFiDirName,"NOTICE").toFile(),new File(backupNiFiDirName)) + FileUtils.copyFileToDirectory(Paths.get(currentNiFiDirName,"README").toFile(),new File(backupNiFiDirName)) + + if(isVerbose){ + logger.info("Backup Complete") + } + + } + + void restore(String backupNiFiDirName, String rollbackNiFiDirName, String currentNiFiDirName, String bootstrapConfFileName){ + + if(isVerbose){ + logger.info("Restoring to directory:" + rollbackNiFiDirName) + } + + final File rollbackNiFiDir = new File(rollbackNiFiDirName) + final File rollbackNiFiLibDir = Paths.get(rollbackNiFiDirName,"lib").toFile() + final File rollbackNiFiConfDir = Paths.get(rollbackNiFiDirName,"conf").toFile() + final Properties bootstrapProperties = getProperties(Paths.get(backupNiFiDirName,"bootstrap_files","bootstrap.conf")) + final File confDir = new File(AdminUtil.getRelativeDirectory(bootstrapProperties.getProperty("conf.dir"),rollbackNiFiDirName)) + final File libDir = new File(AdminUtil.getRelativeDirectory(bootstrapProperties.getProperty("lib.dir"),rollbackNiFiDirName)) + + + if(!rollbackNiFiDir.isDirectory()){ + rollbackNiFiDir.mkdirs() + } + + if(!rollbackNiFiLibDir.isDirectory()){ + rollbackNiFiLibDir.mkdirs() + } + + if(!rollbackNiFiConfDir.isDirectory()){ + rollbackNiFiConfDir.mkdirs() + } + + if(!libDir.isDirectory()){ + libDir.mkdirs() + } + + if(!confDir.isDirectory()){ + confDir.mkdirs() + } + + FileUtils.copyFile(Paths.get(backupNiFiDirName,"bootstrap_files","bootstrap.conf").toFile(), new File(bootstrapConfFileName)) + FileUtils.copyDirectoryToDirectory(Paths.get(backupNiFiDirName,"bootstrap_files","bootstrap").toFile(),Paths.get(rollbackNiFiDirName,"lib").toFile()) + FileUtils.copyDirectoryToDirectory(Paths.get(backupNiFiDirName,"bin").toFile(),new File(rollbackNiFiDirName)) + FileUtils.copyDirectoryToDirectory(Paths.get(backupNiFiDirName,"docs").toFile(),new File(rollbackNiFiDirName)) + FileUtils.copyDirectory(Paths.get(backupNiFiDirName,"lib").toFile(),libDir) + FileUtils.copyDirectory(Paths.get(backupNiFiDirName,"conf").toFile(),confDir) + FileUtils.copyFileToDirectory(Paths.get(backupNiFiDirName,"LICENSE").toFile(),new File(rollbackNiFiDirName)) + FileUtils.copyFileToDirectory(Paths.get(backupNiFiDirName,"NOTICE").toFile(),new File(rollbackNiFiDirName)) + FileUtils.copyFileToDirectory(Paths.get(backupNiFiDirName,"README").toFile(),new File(rollbackNiFiDirName)) + + final File binDir = Paths.get(rollbackNiFiDirName,"bin").toFile() + binDir.listFiles().each { setPosixPermissions(it,[PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE, + PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_EXECUTE, PosixFilePermission.OTHERS_READ, + PosixFilePermission.OTHERS_EXECUTE]) } + + if(!StringUtils.isEmpty(currentNiFiDirName) && moveRepositories) { + moveRepository(currentNiFiDirName, rollbackNiFiDirName) + } + + if(isVerbose){ + logger.info("Restore Completed.") + } + + } + + String extract(final File installFile, final File installDirName){ + + if(isVerbose){ + logger.info("Beginning extraction using {} into installation directory {}",installFile.absolutePath,installDirName.absolutePath) + } + + final String extension = FilenameUtils.getExtension(installFile.getName()) + final InputStream fis = extension.equals("gz") ? new GzipCompressorInputStream(new FileInputStream(installFile)) : new FileInputStream(installFile) + final ArchiveInputStream inputStream = new ArchiveStreamFactory().createArchiveInputStream(new BufferedInputStream(fis)) + final ZipFile zipFile = extension.equals("zip") ? new ZipFile(installFile) : null + + ArchiveEntry entry = inputStream.nextEntry + + if(entry != null){ + + String archiveRootDir = null + + while(entry != null){ + + if(archiveRootDir == null & entry.name.toLowerCase().startsWith("nifi")){ + + archiveRootDir = entry.name.indexOf(File.separator) > -1 ? entry.name.substring(0, entry.getName().indexOf(File.separator)) : entry.name + + if(isVerbose){ + logger.info("Upgrade root directory: {}", archiveRootDir) + } + + File archiveRootDirFile = Paths.get(installDirName.getAbsolutePath(),archiveRootDir).toFile() + + if(archiveRootDirFile.exists()){ + archiveRootDirFile.deleteDir() + } + archiveRootDirFile.mkdirs() + } + + if(isVerbose){ + logger.info("Extracting file: {} ",entry.name) + } + + if(archiveRootDir != null && entry.name.startsWith(archiveRootDir)) { + + final File outputFile = Paths.get(installDirName.getAbsolutePath(),entry.name).toFile(); + + if (entry.isDirectory()) { + + if (!outputFile.exists()) { + if (!outputFile.mkdirs()) { + throw new IllegalStateException(String.format("Couldn't create directory %s.", outputFile.getAbsolutePath())); + } + } + + } else { + + File parentDirectory = outputFile.getParentFile() + + if(!parentDirectory.exists()){ + parentDirectory.mkdirs() + } + + final OutputStream outputFileStream = new FileOutputStream(outputFile); + IOUtils.copy(inputStream, outputFileStream); + outputFileStream.close(); + } + + if(!SystemUtils.IS_OS_WINDOWS){ + setPosixPermissions(entry,outputFile,zipFile) + } + } + + entry = inputStream.nextEntry + } + + return archiveRootDir + + }else{ + throw new RuntimeException("Attempting to extract installation file however it is empty: "+installFile.getName()) + } + + } + + void install(final String installFileName, final String installDirName, final String currentNiFiDirName, final String bootstrapConfFileName, final Boolean overwriteConfigs){ + + final File installFile = new File(installFileName) + + if(isVerbose){ + logger.info("Beginning installation into directory:" + installDirName) + } + + if(installFile.exists()){ + + final File installDir = new File(installDirName) + + if(!installDir.exists()){ + installDir.mkdirs() + } + + final String installRootDirName = extract(installFile,installDir) + final File installRootDir = Paths.get(installDirName,installRootDirName).toFile() + + if(valid(installRootDir)){ + + if(!StringUtils.isEmpty(currentNiFiDirName) && !StringUtils.isEmpty(bootstrapConfFileName)){ + copyState(currentNiFiDirName,installRootDir.absolutePath) + if(moveRepositories) { + moveRepository(currentNiFiDirName,installRootDir.absolutePath) + } + final ConfigMigrator configMigrator = new ConfigMigrator(isVerbose,overwriteConfigs) + configMigrator.run(currentNiFiDirName,bootstrapConfFileName,installRootDir.canonicalPath) + } + + }else{ + throw new RuntimeException("Extract failed: Invalid NiFi Installation. Check the install path provided and retry.") + } + + }else{ + throw new RuntimeException("Installation file provided does not exist") + } + + if(isVerbose){ + logger.info("Installation Complete") + } + + } + + void parseInstall(final CommandLine commandLine){ + + if(commandLine.hasOption(MOVE_REPOSITORIES)){ + this.moveRepositories = true + } + + if(!commandLine.hasOption(INSTALL_FILE)){ + throw new ParseException("Missing -i option") + } else if(!commandLine.hasOption(NIFI_INSTALL_DIR)){ + throw new ParseException("Missing -d option") + } else if (!commandLine.hasOption(NIFI_CURRENT_DIR) && moveRepositories){ + throw new ParseException("Missing -c option: Moving repositories requires current nifi directory") + } + + final String installFileName = commandLine.getOptionValue(INSTALL_FILE) + final String nifiCurrentDirName = commandLine.getOptionValue(NIFI_CURRENT_DIR) + final String nifiInstallDirName = commandLine.getOptionValue(NIFI_INSTALL_DIR) + final Boolean overwriteConfigs = commandLine.hasOption(OVERWRITE_CONFIGS) + final String bootstrapConfFileName = commandLine.hasOption(BOOTSTRAP_CONF) ? commandLine.getOptionValue(BOOTSTRAP_CONF) : !StringUtils.isEmpty(nifiCurrentDirName) ? + Paths.get(nifiCurrentDirName,"conf","bootstrap.conf").toString() : null + + if (Files.notExists(Paths.get(installFileName))) { + throw new ParseException("Missing installation file: " + installFileName) + } + + if (!StringUtils.isEmpty(nifiCurrentDirName) && Files.notExists(Paths.get(nifiCurrentDirName))) { + throw new ParseException("Current NiFi installation path does not exist: " + nifiCurrentDirName) + } + + if(!StringUtils.isEmpty(nifiCurrentDirName) && !StringUtils.isEmpty(bootstrapConfFileName) && !supportedNiFiMinimumVersion(nifiCurrentDirName, bootstrapConfFileName, SUPPORTED_MINIMUM_VERSION)) { + throw new UnsupportedOperationException("File Manager Tool only supports NiFi versions 1.0.0 or higher.") + } + + install(installFileName, nifiInstallDirName, StringUtils.isEmpty(nifiCurrentDirName)? null : Paths.get(nifiCurrentDirName).toFile().getCanonicalPath(), bootstrapConfFileName, overwriteConfigs) + + } + + void parseBackup(final CommandLine commandLine){ + + if(!commandLine.hasOption(BACKUP_DIR)){ + throw new ParseException("Missing -b option") + } else if(!commandLine.hasOption(NIFI_CURRENT_DIR)){ + throw new ParseException("Missing -c option") + } + + final String backupDirName = commandLine.getOptionValue(BACKUP_DIR) + final String nifiCurrentDirName = commandLine.getOptionValue(NIFI_CURRENT_DIR) + + if (Files.notExists(Paths.get(nifiCurrentDirName))) { + throw new ParseException("Current NiFi installation link does not exist: " + nifiCurrentDirName) + } + + final String bootstrapConfFileName = commandLine.hasOption(BOOTSTRAP_CONF) ? commandLine.getOptionValue(BOOTSTRAP_CONF) : Paths.get(nifiCurrentDirName,"conf","bootstrap.conf").toString() + + if(supportedNiFiMinimumVersion(nifiCurrentDirName, bootstrapConfFileName, SUPPORTED_MINIMUM_VERSION)) { + backup(backupDirName, Paths.get(nifiCurrentDirName).toFile().getCanonicalPath(),bootstrapConfFileName) + }else{ + throw new UnsupportedOperationException("File Manager Tool only supports NiFi versions 1.0.0 or higher.") + } + + } + + void parseRestore(final CommandLine commandLine){ + + if(commandLine.hasOption(MOVE_REPOSITORIES)){ + this.moveRepositories = true + } + + if(!commandLine.hasOption(BACKUP_DIR)) { + throw new ParseException("Missing -b option") + }else if(!commandLine.hasOption(NIFI_ROLLBACK_DIR)){ + throw new ParseException("Missing -r option") + }else if (!commandLine.hasOption(NIFI_CURRENT_DIR) && moveRepositories){ + throw new ParseException("Missing -c option: Moving repositories requires current nifi directory") + } + + final String backupDirName = commandLine.getOptionValue(BACKUP_DIR) + final String nifiRollbackDirName = commandLine.getOptionValue(NIFI_ROLLBACK_DIR) + final String nifiCurrentDirName = commandLine.getOptionValue(NIFI_CURRENT_DIR) + + if (Files.notExists(Paths.get(backupDirName)) || !Files.isDirectory(Paths.get(backupDirName))) { + throw new ParseException("Missing or invalid backup directory: " + backupDirName) + } + + if (!StringUtils.isEmpty(nifiCurrentDirName) && Files.notExists(Paths.get(nifiCurrentDirName))) { + throw new ParseException("Current NiFi installation path does not exist: " + nifiCurrentDirName) + } + + if(!supportedNiFiMinimumVersion(backupDirName, Paths.get(backupDirName,"bootstrap_files","bootstrap.conf").toString(), SUPPORTED_MINIMUM_VERSION)) { + throw new UnsupportedOperationException("File Manager Tool only supports NiFi versions 1.0.0 or higher.") + } + + final String bootstrapConfFileName = commandLine.hasOption(BOOTSTRAP_CONF) ? commandLine.getOptionValue(BOOTSTRAP_CONF) : Paths.get(nifiRollbackDirName,"conf","bootstrap.conf").toString() + restore(backupDirName, nifiRollbackDirName, StringUtils.isEmpty(nifiCurrentDirName)? null : Paths.get(nifiCurrentDirName).toFile().getCanonicalPath(), bootstrapConfFileName) + + } + + void parse(final String[] args) throws ParseException, IllegalArgumentException { + + CommandLine commandLine = new DefaultParser().parse(options, args) + + if (commandLine.hasOption(HELP_ARG)) { + printUsage(null) + } else if (commandLine.hasOption(OPERATION)) { + + if(commandLine.hasOption(VERBOSE_ARG)){ + this.isVerbose = true + } + + String operation = commandLine.getOptionValue(OPERATION).toLowerCase() + + if(operation.equals("install")){ + parseInstall(commandLine) + }else if(operation.equals("backup")){ + parseBackup(commandLine) + }else if(operation.equals("restore")){ + parseRestore(commandLine) + }else{ + throw new ParseException("Invalid operation value:" + operation) + } + + }else{ + throw new ParseException("Missing -o option") + } + + } + + public static void main(String[] args) { + FileManagerTool tool = new FileManagerTool() + + try { + tool.parse(args) + } catch (ParseException | IllegalArgumentException e) { + tool.printUsage(e.getLocalizedMessage()); + System.exit(1) + } + + System.exit(0) + } + + +} diff --git a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/nodemanager/NodeManagerTool.groovy b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/nodemanager/NodeManagerTool.groovy index e78adec4b3be..10e8975ed9e0 100644 --- a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/nodemanager/NodeManagerTool.groovy +++ b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/nodemanager/NodeManagerTool.groovy @@ -30,6 +30,7 @@ import org.apache.commons.cli.ParseException import org.apache.nifi.properties.NiFiPropertiesLoader import org.apache.nifi.toolkit.admin.client.ClientFactory import org.apache.nifi.toolkit.admin.client.NiFiClientFactory +import org.apache.nifi.toolkit.admin.util.AdminUtil import org.apache.nifi.util.NiFiProperties import org.apache.nifi.util.StringUtils import org.apache.nifi.web.api.dto.NodeDTO @@ -168,31 +169,40 @@ public class NodeManagerTool extends AbstractAdminTool { void disconnectNode(final Client client, NiFiProperties niFiProperties, List activeUrls, final String proxyDN){ final ClusterEntity clusterEntity = NiFiClientUtil.getCluster(client, niFiProperties, activeUrls,proxyDN) NodeDTO currentNode = getCurrentNode(clusterEntity,niFiProperties) - for(String activeUrl: activeUrls) { - try { - final String url = activeUrl + NODE_ENDPOINT + File.separator + currentNode.nodeId - updateNode(url, client, currentNode, STATUS.DISCONNECTING,proxyDN) - return - } catch (Exception ex){ - logger.warn("Could not connect to node on "+activeUrl+". Exception: "+ex.toString()) + if(currentNode != null){ + for(String activeUrl: activeUrls) { + try { + final String url = activeUrl + NODE_ENDPOINT + File.separator + currentNode.nodeId + updateNode(url, client, currentNode, STATUS.DISCONNECTING,proxyDN) + return + } catch (Exception ex){ + logger.warn("Could not connect to node on "+activeUrl+". Exception: "+ex.toString()) + } } + throw new RuntimeException("Could not successfully complete request") + }else{ + throw new RuntimeException("Current node could not be found in the cluster") } - throw new RuntimeException("Could not successfully complete request") } void connectNode(final Client client, NiFiProperties niFiProperties,List activeUrls, final String proxyDN){ final ClusterEntity clusterEntity = NiFiClientUtil.getCluster(client, niFiProperties, activeUrls,proxyDN) NodeDTO currentNode = getCurrentNode(clusterEntity,niFiProperties) - for(String activeUrl: activeUrls) { - try { - final String url = activeUrl + NODE_ENDPOINT + File.separator + currentNode.nodeId - updateNode(url, client, currentNode, STATUS.CONNECTING,proxyDN) - return - } catch (Exception ex){ - logger.warn("Could not connect to node on "+activeUrl+". Exception: "+ex.toString()) + + if(currentNode != null) { + for(String activeUrl: activeUrls) { + try { + final String url = activeUrl + NODE_ENDPOINT + File.separator + currentNode.nodeId + updateNode(url, client, currentNode, STATUS.CONNECTING,proxyDN) + return + } catch (Exception ex){ + logger.warn("Could not connect to node on "+activeUrl+". Exception: "+ex.toString()) + } } + throw new RuntimeException("Could not successfully complete request") + }else{ + throw new RuntimeException("Current node could not be found in the cluster") } - throw new RuntimeException("Could not successfully complete request") } void removeNode(final Client client, NiFiProperties niFiProperties, List activeUrls, final String proxyDN){ @@ -256,9 +266,9 @@ public class NodeManagerTool extends AbstractAdminTool { final String bootstrapConfFileName = commandLine.getOptionValue(BOOTSTRAP_CONF) final String proxyDN = commandLine.getOptionValue(PROXY_DN) final File bootstrapConf = new File(bootstrapConfFileName) - Properties bootstrapProperties = getBootstrapConf(Paths.get(bootstrapConfFileName)) - String nifiConfDir = getRelativeDirectory(bootstrapProperties.getProperty("conf.dir"), bootstrapConf.getCanonicalFile().getParentFile().getParentFile().getCanonicalPath()) - String nifiLibDir = getRelativeDirectory(bootstrapProperties.getProperty("lib.dir"), bootstrapConf.getCanonicalFile().getParentFile().getParentFile().getCanonicalPath()) + Properties bootstrapProperties = AdminUtil.getBootstrapConf(Paths.get(bootstrapConfFileName)) + String nifiConfDir = AdminUtil.getRelativeDirectory(bootstrapProperties.getProperty("conf.dir"), bootstrapConf.getCanonicalFile().getParentFile().getParentFile().getCanonicalPath()) + String nifiLibDir = AdminUtil.getRelativeDirectory(bootstrapProperties.getProperty("lib.dir"), bootstrapConf.getCanonicalFile().getParentFile().getParentFile().getCanonicalPath()) String nifiPropertiesFileName = nifiConfDir + File.separator +"nifi.properties" final String key = NiFiPropertiesLoader.extractKeyFromBootstrapFile(bootstrapConfFileName) final NiFiProperties niFiProperties = NiFiPropertiesLoader.withKey(key).load(nifiPropertiesFileName) @@ -270,7 +280,7 @@ public class NodeManagerTool extends AbstractAdminTool { final String nifiInstallDir = commandLine.getOptionValue(NIFI_INSTALL_DIR) - if(supportedNiFiMinimumVersion(nifiConfDir,nifiLibDir,SUPPORTED_MINIMUM_VERSION)){ + if(AdminUtil.supportedNiFiMinimumVersion(nifiConfDir,nifiLibDir,SUPPORTED_MINIMUM_VERSION)){ final Client client = clientFactory.getClient(niFiProperties,nifiInstallDir) diff --git a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/notify/NotificationTool.groovy b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/notify/NotificationTool.groovy index 215aee839e80..a1f753110c4f 100644 --- a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/notify/NotificationTool.groovy +++ b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/notify/NotificationTool.groovy @@ -31,6 +31,7 @@ import org.apache.nifi.properties.NiFiPropertiesLoader import org.apache.nifi.toolkit.admin.AbstractAdminTool import org.apache.nifi.toolkit.admin.client.ClientFactory import org.apache.nifi.toolkit.admin.client.NiFiClientFactory +import org.apache.nifi.toolkit.admin.util.AdminUtil import org.apache.nifi.util.NiFiProperties import org.apache.nifi.web.api.dto.BulletinDTO import org.apache.nifi.web.api.entity.BulletinEntity @@ -144,17 +145,17 @@ public class NotificationTool extends AbstractAdminTool { final String bootstrapConfFileName = commandLine.getOptionValue(BOOTSTRAP_CONF) final File bootstrapConf = new File(bootstrapConfFileName) - final Properties bootstrapProperties = getBootstrapConf(Paths.get(bootstrapConfFileName)) + final Properties bootstrapProperties = AdminUtil.getBootstrapConf(Paths.get(bootstrapConfFileName)) final String proxyDN = commandLine.getOptionValue(PROXY_DN) final String parentPathName = bootstrapConf.getCanonicalFile().getParentFile().getParentFile().getCanonicalPath() - final String nifiConfDir = getRelativeDirectory(bootstrapProperties.getProperty("conf.dir"),parentPathName) - final String nifiLibDir = getRelativeDirectory(bootstrapProperties.getProperty("lib.dir"),parentPathName) + final String nifiConfDir = AdminUtil.getRelativeDirectory(bootstrapProperties.getProperty("conf.dir"),parentPathName) + final String nifiLibDir = AdminUtil.getRelativeDirectory(bootstrapProperties.getProperty("lib.dir"),parentPathName) final String nifiPropertiesFileName = nifiConfDir + File.separator +"nifi.properties" final String notificationMessage = commandLine.getOptionValue(NOTIFICATION_MESSAGE) final String notificationLevel = commandLine.getOptionValue(NOTIFICATION_LEVEL) final String nifiInstallDir = commandLine.getOptionValue(NIFI_INSTALL_DIR) - if(supportedNiFiMinimumVersion(nifiConfDir, nifiLibDir, SUPPORTED_MINIMUM_VERSION)){ + if(AdminUtil.supportedNiFiMinimumVersion(nifiConfDir, nifiLibDir, SUPPORTED_MINIMUM_VERSION)){ if(isVerbose){ logger.info("Attempting to connect with nifi using properties:", nifiPropertiesFileName) } diff --git a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/util/AdminUtil.groovy b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/util/AdminUtil.groovy index 9dc0090d13f6..bcebea1ff010 100644 --- a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/util/AdminUtil.groovy +++ b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/util/AdminUtil.groovy @@ -19,6 +19,9 @@ package org.apache.nifi.toolkit.admin.util import org.apache.commons.compress.archivers.zip.ZipArchiveEntry import org.apache.commons.compress.archivers.zip.ZipFile import org.apache.commons.lang3.StringUtils +import org.apache.commons.lang3.SystemUtils + +import java.nio.file.Path class AdminUtil { @@ -66,4 +69,42 @@ class AdminUtil { } + public static Properties getBootstrapConf(Path bootstrapConfFileName) { + Properties bootstrapProperties = new Properties() + File bootstrapConf = bootstrapConfFileName.toFile() + bootstrapProperties.load(new FileInputStream(bootstrapConf)) + return bootstrapProperties + } + + public static String getRelativeDirectory(String directory, String rootDirectory) { + if (directory.startsWith("./")) { + final String directoryUpdated = SystemUtils.IS_OS_WINDOWS ? File.separator + directory.substring(2,directory.length()) : directory.substring(1,directory.length()) + rootDirectory + directoryUpdated + } else { + directory + } + } + + public static Boolean supportedNiFiMinimumVersion(final String nifiConfDirName, final String nifiLibDirName, final String supportedMinimumVersion){ + final File nifiConfDir = new File(nifiConfDirName) + final File nifiLibDir = new File (nifiLibDirName) + final String versionStr = getNiFiVersion(nifiConfDir,nifiLibDir) + + if(!org.apache.nifi.util.StringUtils.isEmpty(versionStr)){ + Version version = new Version(versionStr.replace("-","."),".") + Version minVersion = new Version(supportedMinimumVersion,".") + Version.VERSION_COMPARATOR.compare(version,minVersion) >= 0 + }else{ + return false + } + } + + public static Boolean supportedVersion(String minimumVersion, String maximumVersion, String incomingVersion) { + Version version = new Version(incomingVersion,incomingVersion[1]) + Version supportedMinimum = new Version(minimumVersion,minimumVersion[1]) + Version supportedMaximum = new Version(maximumVersion,maximumVersion[1]) + return Version.VERSION_COMPARATOR.compare(version,supportedMinimum) >= 0 && Version.VERSION_COMPARATOR.compare(version,supportedMaximum) <= 0 + } + + } diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/groovy/org/apache/nifi/toolkit/admin/configmigrator/ConfigMigratorSpec.groovy b/nifi-toolkit/nifi-toolkit-admin/src/test/groovy/org/apache/nifi/toolkit/admin/configmigrator/ConfigMigratorSpec.groovy new file mode 100644 index 000000000000..fb82a22bfaa4 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/groovy/org/apache/nifi/toolkit/admin/configmigrator/ConfigMigratorSpec.groovy @@ -0,0 +1,224 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.nifi.toolkit.admin.configmigrator + +import groovy.xml.XmlUtil +import org.apache.commons.io.FileUtils +import org.apache.commons.lang3.SystemUtils +import org.junit.Rule +import org.junit.contrib.java.lang.system.SystemOutRule +import spock.lang.Specification +import org.junit.contrib.java.lang.system.ExpectedSystemExit + +import java.nio.file.Files +import java.nio.file.attribute.PosixFilePermission + +class ConfigMigratorSpec extends Specification{ + + @Rule + public final ExpectedSystemExit exit = ExpectedSystemExit.none() + + @Rule + public final SystemOutRule systemOutRule = new SystemOutRule().enableLog() + + + def "get rules directory name"(){ + + setup: + + def config = new ConfigMigrator(false,false) + def nifiVersion = "1.1.0" + def nifiUpgradeVersion = "1.3.0" + + when: + + def rulesDirs = config.getRulesDirectoryName(nifiVersion,nifiUpgradeVersion) + + then: + rulesDirs.size() == 2 + rulesDirs[0].endsWith("rules/v1_2_0") + rulesDirs[1].endsWith("rules/v1_3_0") + + } + + def "get script rule name"(){ + + setup: + def config = new ConfigMigrator(false,false) + def fileName = "flow.xml.gz" + + when: + + def script = config.getScriptRuleName(fileName) + + then: + + script == "flow-xml-gz.groovy" + + } + + def "parse argument and migrate property config successfully"(){ + + setup: + + def File tmpDir = setupTmpDir() + def config = new ConfigMigrator(true,false) + def bootstrapFile = new File("src/test/resources/conf/bootstrap.conf") + def upgradeConfDir = new File("src/test/resources/upgrade") + def File workingFile = new File("target/tmp/upgrade") + + if(workingFile.exists()) { + workingFile.delete() + } + + FileUtils.copyDirectory(upgradeConfDir,workingFile) + def Properties updatedProperties = new Properties() + def Properties bootstrapProperties = new Properties() + + when: + + config.run("src/test/resources/",bootstrapFile.path,workingFile.path) + updatedProperties.load(new FileInputStream(workingFile.path + "/conf/nifi.properties")) + bootstrapProperties.load(new FileInputStream(workingFile.path + "/conf/bootstrap.conf")) + + then: + updatedProperties.getProperty("nifi.cluster.node.protocol.port") == "8300" + bootstrapProperties.getProperty("java.arg.2") == "-Xms512m" + bootstrapProperties.getProperty("lib.dir") == "./lib" + + cleanup: + + tmpDir.deleteOnExit() + + } + + def "parse argument and move over configs due to no rules successfully"(){ + + setup: + + def File tmpDir = setupTmpDir() + def config = new ConfigMigrator(true,false) + def bootstrapFile = new File("src/test/resources/conf/bootstrap.conf") + def upgradeConfDir = new File("src/test/resources/no_rules") + def File workingFile = new File("target/tmp/no_rules") + + if(workingFile.exists()) { + workingFile.delete() + } + + FileUtils.copyDirectory(upgradeConfDir,workingFile) + def Properties updatedProperties = new Properties() + def Properties bootstrapProperties = new Properties() + + when: + + config.run("src/test/resources/",bootstrapFile.path,workingFile.path) + updatedProperties.load(new FileInputStream(workingFile.path + "/conf/nifi.properties")) + bootstrapProperties.load(new FileInputStream(workingFile.path + "/conf/bootstrap.conf")) + + then: + updatedProperties.getProperty("nifi.cluster.node.protocol.port") == "8300" + updatedProperties.getProperty("nifi.cluster.is.node") == "true" + bootstrapProperties.getProperty("java.arg.1") + + cleanup: + + tmpDir.deleteOnExit() + + } + + def "parse arguments and migrate property config successfully with override"(){ + + setup: + + def File tmpDir = setupTmpDir() + def config = new ConfigMigrator(true,true) + def nifiConfDir = new File("src/test/resources/conf") + def nifiLibDir = new File("src/test/resources/lib") + def externalConfDir = new File("src/test/resources/external/conf") + def upgradeConfDir = new File("src/test/resources/upgrade") + + def File workingFile = new File("target/tmp/conf") + def File workingLibFile = new File("target/tmp/lib") + def File externalWorkingFile = new File("target/tmp/external/conf") + def File upgradeWorkingFile = new File("target/tmp/upgrade") + + + if(workingFile.exists()) { + workingFile.delete() + } + + if(externalWorkingFile.exists()){ + externalWorkingFile.delete() + } + + if(upgradeWorkingFile.exists()){ + upgradeWorkingFile.delete() + } + + FileUtils.copyDirectory(nifiConfDir,workingFile) + FileUtils.copyDirectory(nifiLibDir,workingLibFile) + FileUtils.copyDirectory(externalConfDir,externalWorkingFile) + FileUtils.copyDirectory(upgradeConfDir,upgradeWorkingFile) + + def bootstrapFile = new File("target/tmp/external/conf/bootstrap.conf") + def Properties updatedNiFiProperties = new Properties() + def Properties updatedBootstrapProperties = new Properties() + def File updatedLoginProvidersFile + def xml + + when: + config.run("target/tmp/external",bootstrapFile.path,upgradeWorkingFile.path) + updatedNiFiProperties.load(new FileInputStream(workingFile.path + "/nifi.properties")) + updatedBootstrapProperties.load(new FileInputStream(upgradeWorkingFile.path + "/conf/bootstrap.conf")) + updatedLoginProvidersFile = new File(workingFile.path + "/login-identity-providers.xml") + xml = new XmlSlurper().parse(updatedLoginProvidersFile) + + + then: + updatedNiFiProperties.getProperty("nifi.cluster.node.protocol.port") == "8300" + updatedBootstrapProperties.getProperty("java.arg.2") == "-Xms512m" + updatedBootstrapProperties.getProperty("lib.dir") == "./lib" + xml.depthFirst().findAll { it.name() == "fake"}.size() == 1 + + cleanup: + + tmpDir.deleteOnExit() + + } + + def setFilePermissions(File file, List permissions = []) { + if (SystemUtils.IS_OS_WINDOWS) { + file?.setReadable(permissions.contains(PosixFilePermission.OWNER_READ)) + file?.setWritable(permissions.contains(PosixFilePermission.OWNER_WRITE)) + file?.setExecutable(permissions.contains(PosixFilePermission.OWNER_EXECUTE)) + } else { + Files.setPosixFilePermissions(file?.toPath(), permissions as Set) + } + } + + def setupTmpDir(String tmpDirPath = "target/tmp/") { + File tmpDir = new File(tmpDirPath) + tmpDir.mkdirs() + setFilePermissions(tmpDir, [PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE, + PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE, PosixFilePermission.GROUP_EXECUTE, + PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_WRITE, PosixFilePermission.OTHERS_EXECUTE]) + tmpDir + } + +} diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/groovy/org/apache/nifi/toolkit/admin/filemanager/FileManagerToolSpec.groovy b/nifi-toolkit/nifi-toolkit-admin/src/test/groovy/org/apache/nifi/toolkit/admin/filemanager/FileManagerToolSpec.groovy new file mode 100644 index 000000000000..1e19badda270 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/groovy/org/apache/nifi/toolkit/admin/filemanager/FileManagerToolSpec.groovy @@ -0,0 +1,511 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.toolkit.admin.filemanager + +import org.apache.commons.cli.ParseException +import org.apache.commons.io.FileUtils +import org.apache.commons.lang3.SystemUtils +import org.junit.Rule +import org.junit.contrib.java.lang.system.ExpectedSystemExit +import org.junit.contrib.java.lang.system.SystemOutRule +import spock.lang.Specification + +import java.nio.file.Files +import java.nio.file.attribute.PosixFilePermission + +class FileManagerToolSpec extends Specification{ + @Rule + public final ExpectedSystemExit exit = ExpectedSystemExit.none() + + @Rule + public final SystemOutRule systemOutRule = new SystemOutRule().enableLog() + + + def "print help and usage info"() { + + given: + def manager = new FileManagerTool() + + when: + manager.parse(["-h"] as String[]) + + then: + systemOutRule.getLog().contains("usage: org.apache.nifi.toolkit.admin.filemanager.FileManagerTool") + } + + def "throws exception missing operation flag"() { + + given: + def manager = new FileManagerTool() + + when: + manager.parse(["-d", "/missing/upgrade/dir"] as String[]) + + then: + def e = thrown(ParseException) + e.message == "Missing -o option" + } + + def "throws exception if missing upgrade file for install"() { + + given: + def manager = new FileManagerTool() + + when: + manager.parse(["-o", "install","-d","/missing/upgrade/dir"] as String[]) + + then: + def e = thrown(ParseException) + e.message == "Missing -i option" + } + + def "throws exception if missing install directory for install"() { + + given: + def manager = new FileManagerTool() + + when: + manager.parse(["-o", "install","-i","/missing/upgrade/dir"] as String[]) + + then: + def e = thrown(ParseException) + e.message == "Missing -d option" + } + + def "throws exception if missing current directory when moving repositories for install"() { + + given: + def manager = new FileManagerTool() + + when: + manager.parse(["-o", "install","-i","/missing/current/dir","-d","/missing/current/dir","-m"] as String[]) + + then: + def e = thrown(ParseException) + e.message == "Missing -c option: Moving repositories requires current nifi directory" + } + + + def "throws exception if missing backup directory for backup"() { + + given: + def manager = new FileManagerTool() + + when: + manager.parse(["-o", "backup","-c","/missing/backup/dir"] as String[]) + + then: + def e = thrown(ParseException) + e.message == "Missing -b option" + } + + def "throws exception if missing current dir for backup"() { + + given: + def manager = new FileManagerTool() + + when: + manager.parse(["-o", "backup","-b","/missing/current/dir"] as String[]) + + then: + def e = thrown(ParseException) + e.message == "Missing -c option" + } + + def "throws exception if missing rollback directory for restore"() { + + given: + def manager = new FileManagerTool() + + when: + manager.parse(["-o", "restore","-b","/missing/rollback/dir"] as String[]) + + then: + def e = thrown(ParseException) + e.message == "Missing -r option" + } + + def "throws exception if missing backup directory for restore"() { + + given: + def manager = new FileManagerTool() + + when: + manager.parse(["-o", "restore","-r","/missing/backup/dir","-c","/missing/rollback/dir"] as String[]) + + then: + def e = thrown(ParseException) + e.message == "Missing -b option" + } + + def "throws exception if missing current directory when wanting to move repositories during install"() { + + given: + def manager = new FileManagerTool() + + when: + manager.parse(["-o", "restore","-r","/missing/current/dir","-b","/missing/current/dir","-m"] as String[]) + + then: + def e = thrown(ParseException) + e.message == "Missing -c option: Moving repositories requires current nifi directory" + } + + + def "move directory from src to target"(){ + + setup: + def File tmpDir = setupTmpDir() + def File testDir = new File("target/tmp/conf/test") + def File oldConfDir= new File("target/tmp/conf") + def File newConfDir= new File("target/tmp/new_conf") + oldConfDir.mkdirs() + newConfDir.mkdirs() + testDir.mkdirs() + def manager = new FileManagerTool() + def File newTestDir = new File("target/tmp/new_conf/test") + + when: + manager.move("./test","target/tmp/conf","target/tmp/new_conf") + + then: + newTestDir.exists() + + cleanup: + tmpDir.deleteDir() + + } + + def "move zookeeper from src to target"(){ + + setup: + def File tmpDir = setupTmpDir() + def manager = new FileManagerTool() + def File oldNiFiDir = new File("target/tmp/nifi") + def File zookeeperDir = new File("target/tmp/nifi/state/zookeeper") + def File upgradeNiFiDir= new File("target/tmp/nifi_upgrade") + def myid = new File("src/test/resources/filemanager/myid") + oldNiFiDir.mkdirs() + zookeeperDir.mkdirs() + upgradeNiFiDir.mkdirs() + + FileUtils.copyFileToDirectory(myid,zookeeperDir) + + when: + manager.copyState("target/tmp/nifi","target/tmp/nifi_upgrade") + def File upgradeMyid = new File("target/tmp/nifi_upgrade/state/zookeeper/myid") + + then: + upgradeMyid.exists() + + cleanup: + tmpDir.deleteDir() + + } + + def "move repositories from src to target"(){ + + setup: + def File tmpDir = setupTmpDir() + def manager = new FileManagerTool() + + def File oldNiFiDir = new File("target/tmp/nifi") + def File oldNiFiConfDir = new File("target/tmp/nifi/conf") + def File upgradeNiFiDir= new File("target/tmp/nifi_upgrade") + + oldNiFiDir.mkdirs() + oldNiFiConfDir.mkdirs() + upgradeNiFiDir.mkdirs() + + def bootstrapFile = new File("src/test/resources/filemanager/bootstrap.conf") + def nifiProperties = new File("src/test/resources/filemanager/nifi.properties") + def File flowfileRepositoryDir = new File("target/tmp/nifi/flowfile_repository") + def File contentRepositoryDir = new File("target/tmp/nifi/content_repository") + def File databaseRepositoryDir = new File("target/tmp/nifi/database_repository") + def File provenanceRepositoryDir = new File("target/tmp/nifi/provenance_repository") + + FileUtils.copyFileToDirectory(bootstrapFile,oldNiFiConfDir) + FileUtils.copyFileToDirectory(nifiProperties,oldNiFiConfDir) + flowfileRepositoryDir.mkdirs() + contentRepositoryDir.mkdirs() + databaseRepositoryDir.mkdirs() + provenanceRepositoryDir.mkdirs() + + when: + manager.moveRepository("target/tmp/nifi","target/tmp/nifi_upgrade") + def files = upgradeNiFiDir.listFiles() + def count = files.findAll { it.name in ["flowfile_repository","content_repository","database_repository","provenance_repository"]}.size() + + then: + count == 4 + + cleanup: + tmpDir.deleteDir() + + } + + def "backup nifi installation successfully"(){ + + setup: + def File tmpDir = setupTmpDir() + def manager = new FileManagerTool() + def File niFiDir = new File("target/tmp/nifi") + def File niFiConfDir = new File("target/tmp/nifi/conf") + def File backupNiFiDir= new File("target/tmp/nifi_bak") + def File binDir = new File("target/tmp/nifi/bin") + def File nifiShell = new File("target/tmp/nifi/bin/nifi.sh") + def File libDir = new File("target/tmp/nifi/lib") + def File bootstrapLibDir = new File("target/tmp/nifi/lib/bootstrap") + def File docDir = new File("target/tmp/nifi/docs") + def bootstrapFile = new File("src/test/resources/filemanager/bootstrap.conf") + def nifiProperties = new File("src/test/resources/filemanager/nifi.properties") + def license = new File("target/tmp/nifi/LICENSE") + def notice = new File("target/tmp/nifi/NOTICE") + def readme = new File("target/tmp/nifi/README") + + binDir.mkdirs() + libDir.mkdirs() + bootstrapLibDir.mkdirs() + docDir.mkdirs() + niFiDir.mkdirs() + niFiConfDir.mkdirs() + nifiShell.write("shell") + license.write("license") + readme.write("readme") + notice.write("notice") + FileUtils.copyFileToDirectory(bootstrapFile,niFiConfDir) + FileUtils.copyFileToDirectory(nifiProperties,niFiConfDir) + + when: + manager.backup("target/tmp/nifi_bak","target/tmp/nifi","target/tmp/nifi/conf/bootstrap.conf") + + then: + backupNiFiDir.exists() + def files = backupNiFiDir.listFiles() + def expectedFiles = ["bin","lib","docs","README","LICENSE","NOTICE","conf","bootstrap_files"] + def count = files.findAll {it.name in expectedFiles}.size() + count == expectedFiles.size() + + cleanup: + tmpDir.deleteDir() + + } + + def "restore nifi installation successfully"(){ + + setup: + def File tmpDir = setupTmpDir() + def manager = new FileManagerTool() + def File rollbackNiFiDir= new File("target/tmp/nifi_1") + def File rollbackNiFiLibDir = new File("target/tmp/nifi_1/lib") + def File currentNiFiConfDir= new File("target/tmp/nifi/conf") + def File backupNiFiDir = new File("target/tmp/nifi_bak") + def File backupNiFiConfDir = new File("target/tmp/nifi_bak/conf") + def File backupBinDir = new File("target/tmp/nifi_bak/bin") + def File backupNiFiShell = new File("target/tmp/nifi_bak/bin/nifi.sh") + def File backupLibDir = new File("target/tmp/nifi_bak/lib") + def File bootstrapDir = new File("target/tmp/nifi_bak/bootstrap_files") + def File bootstrapLibDir = new File("target/tmp/nifi_bak/bootstrap_files/bootstrap") + def File backupDocDir = new File("target/tmp/nifi_bak/docs") + + def bootstrapFile = new File("src/test/resources/filemanager/bootstrap.conf") + def nifiProperties = new File("src/test/resources/filemanager/nifi.properties") + def license = new File("target/tmp/nifi_bak/LICENSE") + def notice = new File("target/tmp/nifi_bak/NOTICE") + def readme = new File("target/tmp/nifi_bak/README") + def libjar = new File("target/tmp/nifi_bak/lib/lib.jar") + def File flowfileRepositoryDir = new File("target/tmp/nifi/flowfile_repository") + def File contentRepositoryDir = new File("target/tmp/nifi/content_repository") + def File databaseRepositoryDir = new File("target/tmp/nifi/database_repository") + def File provenanceRepositoryDir = new File("target/tmp/nifi/provenance_repository") + + currentNiFiConfDir.mkdirs() + backupNiFiDir.mkdirs() + backupNiFiConfDir.mkdirs() + bootstrapDir.mkdirs() + bootstrapLibDir.mkdirs() + backupDocDir.mkdirs() + backupNiFiDir.mkdirs() + backupNiFiConfDir.mkdirs() + backupBinDir.mkdirs() + backupLibDir.mkdirs() + backupNiFiShell.write("shell") + license.write("license") + readme.write("readme") + notice.write("notice") + libjar.write("fakejar") + flowfileRepositoryDir.mkdirs() + contentRepositoryDir.mkdirs() + databaseRepositoryDir.mkdirs() + provenanceRepositoryDir.mkdirs() + + FileUtils.copyFileToDirectory(bootstrapFile,bootstrapDir) + FileUtils.copyFileToDirectory(bootstrapFile,currentNiFiConfDir) + FileUtils.copyFileToDirectory(nifiProperties,backupNiFiConfDir) + FileUtils.copyFileToDirectory(nifiProperties,currentNiFiConfDir) + + when: + manager.restore("target/tmp/nifi_bak","target/tmp/nifi_1","target/tmp/nifi","target/tmp/nifi_1/conf/boostrap.conf") + + then: + rollbackNiFiDir.exists() + rollbackNiFiLibDir.exists() + def files = rollbackNiFiDir.listFiles() + def expectedFiles = ["bin","lib","docs","README","LICENSE","NOTICE","conf"] + def count = files.findAll {it.name in expectedFiles}.size() + count == expectedFiles.size() + def libFiles = rollbackNiFiLibDir.listFiles() + libFiles.findAll{it.name == "lib.jar"}.size() == 1 + + cleanup: + tmpDir.deleteDir() + + } + + def "extract compressed tar file successfully"(){ + + setup: + def File tmpDir = setupTmpDir() + def manager = new FileManagerTool() + def File nifiArchive = new File("src/test/resources/filemanager/nifi-test-archive.tar.gz") + def File nifiInstallDir = new File("target/tmp/nifi_tar") + def File nifiInstallBinDir = new File("target/tmp/nifi_tar/nifi-test-archive/bin") + + nifiInstallDir.mkdirs() + + when: + manager.extract(nifiArchive,nifiInstallDir) + + then: + nifiInstallBinDir.exists() + + cleanup: + tmpDir.deleteDir() + + } + + def "extract zip file successfully"(){ + + setup: + def File tmpDir = setupTmpDir() + def manager = new FileManagerTool() + def File nifiArchive = new File("src/test/resources/filemanager/nifi-test-archive.zip") + def File nifiInstallDir = new File("target/tmp/nifi_zip") + def File nifiInstallBinDir = new File("target/tmp/nifi_zip/nifi-test-archive/bin") + + nifiInstallDir.mkdirs() + + when: + def upgradeRoot = manager.extract(nifiArchive,nifiInstallDir) + + then: + upgradeRoot == "nifi-test-archive" + nifiInstallBinDir.exists() + + cleanup: + tmpDir.deleteDir() + + } + + def "install nifi with existing installation successfully"(){ + setup: + def File tmpDir = setupTmpDir() + def manager = new FileManagerTool() + def File nifiArchive = new File("src/test/resources/filemanager/nifi-test-archive.tar.gz") + def bootstrapFile = new File("src/test/resources/filemanager/bootstrap.conf") + def nifiProperties = new File("src/test/resources/filemanager/nifi.properties") + def File nifiCurrentDir = new File("target/tmp/nifi_old") + def File nifiCurrentConfDir = new File("target/tmp/nifi_old/conf") + def File nifiInstallDir = new File("target/tmp/nifi") + def File nifiInstallBinDir = new File("target/tmp/nifi/nifi-test-archive/bin") + def File flowfileRepositoryDir = new File("target/tmp/nifi_old/flowfile_repository") + def File contentRepositoryDir = new File("target/tmp/nifi_old/content_repository") + def File databaseRepositoryDir = new File("target/tmp/nifi_old/database_repository") + def File provenanceRepositoryDir = new File("target/tmp/nifi_old/provenance_repository") + + nifiInstallDir.mkdirs() + nifiCurrentDir.mkdirs() + flowfileRepositoryDir.mkdirs() + contentRepositoryDir.mkdirs() + databaseRepositoryDir.mkdirs() + provenanceRepositoryDir.mkdirs() + FileUtils.copyFileToDirectory(bootstrapFile,nifiCurrentConfDir) + FileUtils.copyFileToDirectory(nifiProperties,nifiCurrentConfDir) + + when: + manager.install(nifiArchive.getAbsolutePath(),nifiInstallDir.getAbsolutePath(),nifiCurrentDir.getAbsolutePath(),"target/tmp/nifi_old/conf/bootstrap.conf",false) + + then: + nifiInstallBinDir.exists() + + cleanup: + tmpDir.deleteDir() + + } + + def "install nifi without existing installation successfully"(){ + setup: + def File tmpDir = setupTmpDir() + def manager = new FileManagerTool() + def File nifiArchive = new File("src/test/resources/filemanager/nifi-test-archive.tar.gz") + def bootstrapFile = new File("src/test/resources/filemanager/bootstrap.conf") + def nifiProperties = new File("src/test/resources/filemanager/nifi.properties") + def File nifiInstallDir = new File("target/tmp/nifi") + def File nifiInstallBinDir = new File("target/tmp/nifi/nifi-test-archive/bin") + def File flowfileRepositoryDir = new File("target/tmp/nifi_old/flowfile_repository") + def File contentRepositoryDir = new File("target/tmp/nifi_old/content_repository") + def File databaseRepositoryDir = new File("target/tmp/nifi_old/database_repository") + def File provenanceRepositoryDir = new File("target/tmp/nifi_old/provenance_repository") + + nifiInstallDir.mkdirs() + + flowfileRepositoryDir.mkdirs() + contentRepositoryDir.mkdirs() + databaseRepositoryDir.mkdirs() + provenanceRepositoryDir.mkdirs() + + when: + manager.install(nifiArchive.getAbsolutePath(),nifiInstallDir.getAbsolutePath(),null,null,false) + + then: + nifiInstallBinDir.exists() + + cleanup: + tmpDir.deleteDir() + + } + + def setFilePermissions(File file, List permissions = []) { + if (SystemUtils.IS_OS_WINDOWS) { + file?.setReadable(permissions.contains(PosixFilePermission.OWNER_READ)) + file?.setWritable(permissions.contains(PosixFilePermission.OWNER_WRITE)) + file?.setExecutable(permissions.contains(PosixFilePermission.OWNER_EXECUTE)) + } else { + Files.setPosixFilePermissions(file?.toPath(), permissions as Set) + } + } + + def setupTmpDir(String tmpDirPath = "target/tmp/") { + File tmpDir = new File(tmpDirPath) + tmpDir.mkdirs() + setFilePermissions(tmpDir, [PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE, + PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE, PosixFilePermission.GROUP_EXECUTE, + PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_WRITE, PosixFilePermission.OTHERS_EXECUTE]) + tmpDir + } + +} diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/groovy/org/apache/nifi/toolkit/admin/util/AdminUtilSpec.groovy b/nifi-toolkit/nifi-toolkit-admin/src/test/groovy/org/apache/nifi/toolkit/admin/util/AdminUtilSpec.groovy index 854eefb83d85..e6e5d6431e7e 100644 --- a/nifi-toolkit/nifi-toolkit-admin/src/test/groovy/org/apache/nifi/toolkit/admin/util/AdminUtilSpec.groovy +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/groovy/org/apache/nifi/toolkit/admin/util/AdminUtilSpec.groovy @@ -51,4 +51,35 @@ class AdminUtilSpec extends Specification{ } + def "get bootstrap properties"(){ + + given: + + def bootstrapConf = new File("src/test/resources/conf/bootstrap.conf") + + when: + + def properties = AdminUtil.getBootstrapConf(bootstrapConf.toPath()) + + then: + properties.get("conf.dir") == "./conf" + + } + + def "supported version should be true"(){ + + expect: + AdminUtil.supportedVersion("1.0.0","1.2.0","1.1.0") + + } + + def "supported version should be false"(){ + + expect: + !AdminUtil.supportedVersion("1.0.0","1.2.0","1.3.0") + + } + + + } diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/resources/conf/bootstrap-notification-services.xml b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/conf/bootstrap-notification-services.xml new file mode 100644 index 000000000000..dcf6a2505c2d --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/conf/bootstrap-notification-services.xml @@ -0,0 +1,46 @@ + + + + + + + + \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/resources/conf/logback.xml b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/conf/logback.xml new file mode 100644 index 000000000000..a6148b820643 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/conf/logback.xml @@ -0,0 +1,169 @@ + + + + + + true + + + + ${org.apache.nifi.bootstrap.config.log.dir}/nifi-app.log + + + ${org.apache.nifi.bootstrap.config.log.dir}/nifi-app_%d{yyyy-MM-dd_HH}.%i.log + + 100MB + + + 30 + + + %date %level [%thread] %logger{40} %msg%n + true + + + + + ${org.apache.nifi.bootstrap.config.log.dir}/nifi-user.log + + + ${org.apache.nifi.bootstrap.config.log.dir}/nifi-user_%d.log + + 30 + + + %date %level [%thread] %logger{40} %msg%n + + + + + ${org.apache.nifi.bootstrap.config.log.dir}/nifi-bootstrap.log + + + ${org.apache.nifi.bootstrap.config.log.dir}/nifi-bootstrap_%d.log + + 5 + + + %date %level [%thread] %logger{40} %msg%n + + + + + + %date %level [%thread] %logger{40} %msg%n + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/resources/filemanager/nifi-test-archive.tar.gz b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/filemanager/nifi-test-archive.tar.gz index c7bcdf413477ef5cbbc6ec34d1c02f3143f2d089..e7751175a982c9df5a49239a8116ff569c72fc9b 100644 GIT binary patch literal 29306 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 literal 27167 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 0 && Version.VERSION_COMPARATOR.compare(version,supportedV120) <= 0 + } + + @Override + byte[] migrate(byte[] oldContent, byte[]newContent) { + + String xmlString = new String(oldContent) + xmlString.replace("-->\n","-->\n\n" + + "").bytes + } + +} \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/resources/rules/v1_2_0/logback-xml.groovy b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/rules/v1_2_0/logback-xml.groovy new file mode 100644 index 000000000000..8ea4726c4019 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/rules/v1_2_0/logback-xml.groovy @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rules.v1_2_0 + +import org.apache.nifi.toolkit.admin.configmigrator.rules.XmlMigrationRule +import org.apache.nifi.toolkit.admin.util.AdminUtil + +class LogbackRule extends XmlMigrationRule{ + + @Override + Boolean supportedVersion(String versionStr) { + return AdminUtil.supportedVersion("1.0.0","1.2.0",versionStr) + } + + @Override + byte[] migrateXml(Node oldXmlContent, Node newXmlContent) { + def configuration = oldXmlContent + configuration.appender.findAll { appender -> + appender.@name == "APP_FILE" + }.each { Node appender -> + def rollingPolicy = appender.rollingPolicy[0] + if(rollingPolicy.@class != "ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy") { + rollingPolicy.@class = "ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy" + def maxFileSize = rollingPolicy.timeBasedFileNamingAndTriggeringPolicy.maxFileSize.text() + rollingPolicy.remove(rollingPolicy.timeBasedFileNamingAndTriggeringPolicy) + def maxFileSizeNode = new Node(rollingPolicy, "maxFileSize", [:]) + maxFileSizeNode.value = maxFileSize + def encoder = appender.encoder[0] + def immediateFlush = encoder.immediateFlush.text() + encoder.remove(encoder.immediateFlush) + def immediateFlushNode = new Node(appender, "immediateFlush", [:]) + immediateFlushNode.value = immediateFlush + } + } + + def hasLogMessage = configuration.findAll { element -> + element.@name == "org.apache.nifi.processors.standard.LogMessage" + }.size() + + def hasCalciteException = configuration.logger.findAll { element -> + element.@name == "org.apache.nifi.processors.standard.LogMessage" + }.size() + + if(hasLogMessage == 0) { + new Node(configuration, 'logger', ["name": "org.apache.nifi.processors.standard.LogMessage", "level": "INFO"]) + } + + if(hasCalciteException == 0) { + new Node(configuration, 'logger', ["name": "org.apache.calcite.runtime.CalciteException", "level": "OFF"]) + } + + return convertToByteArray(configuration) + } + + +} \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/resources/rules/v1_2_0/login-identity-providers-xml.groovy b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/rules/v1_2_0/login-identity-providers-xml.groovy new file mode 100644 index 000000000000..64abc2cc0e3d --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/rules/v1_2_0/login-identity-providers-xml.groovy @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rules.v1_2_0 + +import org.apache.nifi.toolkit.admin.configmigrator.rules.XmlMigrationRule +import org.apache.nifi.toolkit.admin.util.AdminUtil +import org.apache.nifi.toolkit.admin.util.Version + +class LoginIdentityProvidersRule extends XmlMigrationRule{ + + @Override + Boolean supportedVersion(String versionStr) { + return AdminUtil.supportedVersion("1.0.0","1.2.0",versionStr) + } + + @Override + byte[] migrateXml(Node oldXmlContent, Node newXmlContent) { + new Node(oldXmlContent, 'fake', [:]) + return convertToByteArray(oldXmlContent) + } + +} \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/resources/rules/v1_2_0/nifi-properties.groovy b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/rules/v1_2_0/nifi-properties.groovy new file mode 100644 index 000000000000..aa24fd5fe6dc --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/rules/v1_2_0/nifi-properties.groovy @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rules.v1_2_0 + +import org.apache.nifi.toolkit.admin.configmigrator.rules.PropertyMigrationRule +import org.apache.nifi.toolkit.admin.util.AdminUtil +import org.apache.nifi.toolkit.admin.util.Version + +class NiFiPropertiesRule extends PropertyMigrationRule{ + @Override + Boolean supportedVersion(String versionStr) { + return AdminUtil.supportedVersion("1.0.0","1.2.0",versionStr) + } + + @Override + boolean keyAllowed(Object key) { + return !((String)key).equals("nifi.version") + } +} diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/resources/rules/v1_2_0/state-management-xml.groovy b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/rules/v1_2_0/state-management-xml.groovy new file mode 100644 index 000000000000..11153621ba80 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/rules/v1_2_0/state-management-xml.groovy @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rules.v1_2_0 + +import org.apache.nifi.toolkit.admin.configmigrator.rules.GenericMigrationRule +import org.apache.nifi.toolkit.admin.util.AdminUtil +import org.apache.nifi.toolkit.admin.util.Version + +class StateManagementRule extends GenericMigrationRule{ + + @Override + Boolean supportedVersion(String versionStr) { + return AdminUtil.supportedVersion("1.0.0","1.2.0",versionStr) + } + + @Override + byte[] migrate(byte[] oldContent, byte[]newContent) { + return oldContent + } + +} + + diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/resources/rules/v1_2_0/users-xml.groovy b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/rules/v1_2_0/users-xml.groovy new file mode 100644 index 000000000000..4ee299cd8ea1 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/rules/v1_2_0/users-xml.groovy @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rules.v1_2_0 + +import org.apache.nifi.toolkit.admin.configmigrator.rules.GenericMigrationRule +import org.apache.nifi.toolkit.admin.util.AdminUtil +import org.apache.nifi.toolkit.admin.util.Version + +class UsersRule extends GenericMigrationRule{ + + @Override + Boolean supportedVersion(String versionStr) { + return AdminUtil.supportedVersion("1.0.0","1.2.0",versionStr) + } + + @Override + byte[] migrate(byte[] oldContent, byte[]newContent) { + return oldContent + } + +} diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/resources/rules/v1_2_0/zookeeper-properties.groovy b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/rules/v1_2_0/zookeeper-properties.groovy new file mode 100644 index 000000000000..4beaa5d771b5 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/rules/v1_2_0/zookeeper-properties.groovy @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package rules.v1_2_0 + +import org.apache.nifi.toolkit.admin.configmigrator.rules.PropertyMigrationRule +import org.apache.nifi.toolkit.admin.util.AdminUtil +import org.apache.nifi.toolkit.admin.util.Version + +class ZookeeperPropertiesRule extends PropertyMigrationRule{ + + @Override + Boolean supportedVersion(String versionStr) { + return AdminUtil.supportedVersion("1.0.0","1.2.0",versionStr) + } + + @Override + boolean keyAllowed(Object key) { + return true + } +} \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/resources/rules/v1_3_0/authorizations-xml.groovy b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/rules/v1_3_0/authorizations-xml.groovy new file mode 100644 index 000000000000..3937ff93b4b8 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/rules/v1_3_0/authorizations-xml.groovy @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rules.v1_3_0 + +import org.apache.nifi.toolkit.admin.configmigrator.rules.GenericMigrationRule +import org.apache.nifi.toolkit.admin.util.AdminUtil +import org.apache.nifi.toolkit.admin.util.Version + +class AuthorizationsRule extends GenericMigrationRule{ + + @Override + Boolean supportedVersion(String versionStr) { + return AdminUtil.supportedVersion("1.2.0","1.3.0",versionStr) + } + + @Override + byte[] migrate(byte[] oldContent, byte[]newContent) { + return oldContent + } + +} diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/resources/upgrade/conf/bootstrap-notification-services.xml b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/upgrade/conf/bootstrap-notification-services.xml new file mode 100644 index 000000000000..7c4d50d36d70 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/upgrade/conf/bootstrap-notification-services.xml @@ -0,0 +1,53 @@ + + + + + + + + + \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/resources/upgrade/conf/logback.xml b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/upgrade/conf/logback.xml new file mode 100644 index 000000000000..8d1db0a5c822 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/upgrade/conf/logback.xml @@ -0,0 +1,170 @@ + + + + + + true + + + + ${org.apache.nifi.bootstrap.config.log.dir}/nifi-app.log + + + ${org.apache.nifi.bootstrap.config.log.dir}/nifi-app_%d{yyyy-MM-dd_HH}.%i.log + 100MB + + 30 + + true + + %date %level [%thread] %logger{40} %msg%n + + + + + ${org.apache.nifi.bootstrap.config.log.dir}/nifi-user.log + + + ${org.apache.nifi.bootstrap.config.log.dir}/nifi-user_%d.log + + 30 + + + %date %level [%thread] %logger{40} %msg%n + + + + + ${org.apache.nifi.bootstrap.config.log.dir}/nifi-bootstrap.log + + + ${org.apache.nifi.bootstrap.config.log.dir}/nifi-bootstrap_%d.log + + 5 + + + %date %level [%thread] %logger{40} %msg%n + + + + + + %date %level [%thread] %logger{40} %msg%n + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nifi-toolkit/nifi-toolkit-assembly/src/main/assembly/dependencies.xml b/nifi-toolkit/nifi-toolkit-assembly/src/main/assembly/dependencies.xml index 1ade1820df2c..a96953bfc426 100644 --- a/nifi-toolkit/nifi-toolkit-assembly/src/main/assembly/dependencies.xml +++ b/nifi-toolkit/nifi-toolkit-assembly/src/main/assembly/dependencies.xml @@ -59,6 +59,11 @@ classpath/ 0600 + + ${project.basedir}/src/main/resources/rules + classpath/rules/ + 0600 + ${project.build.directory}/nifi-resources/conf lib/ diff --git a/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/file-manager.bat b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/file-manager.bat new file mode 100644 index 000000000000..001d0cd45418 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/file-manager.bat @@ -0,0 +1,39 @@ +@echo off +rem +rem Licensed to the Apache Software Foundation (ASF) under one or more +rem contributor license agreements. See the NOTICE file distributed with +rem this work for additional information regarding copyright ownership. +rem The ASF licenses this file to You under the Apache License, Version 2.0 +rem (the "License"); you may not use this file except in compliance with +rem the License. You may obtain a copy of the License at +rem +rem http://www.apache.org/licenses/LICENSE-2.0 +rem +rem Unless required by applicable law or agreed to in writing, software +rem distributed under the License is distributed on an "AS IS" BASIS, +rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +rem See the License for the specific language governing permissions and +rem limitations under the License. +rem + +rem Use JAVA_HOME if it's set; otherwise, just use java + +if "%JAVA_HOME%" == "" goto noJavaHome +if not exist "%JAVA_HOME%\bin\java.exe" goto noJavaHome +set JAVA_EXE=%JAVA_HOME%\bin\java.exe +goto startConfig + +:noJavaHome +echo The JAVA_HOME environment variable is not defined correctly. +echo Instead the PATH will be used to find the java executable. +echo. +set JAVA_EXE=java +goto startConfig + +:startConfig +set LIB_DIR=%~dp0..\classpath;%~dp0..\lib + +SET JAVA_PARAMS=-cp %LIB_DIR%\* -Xms12m -Xmx24m %JAVA_ARGS% org.apache.nifi.toolkit.admin.filemanager.FileManagerTool + +cmd.exe /C ""%JAVA_EXE%" %JAVA_PARAMS% %* "" + diff --git a/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/file-manager.sh b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/file-manager.sh new file mode 100644 index 000000000000..91f16647354b --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/file-manager.sh @@ -0,0 +1,119 @@ +#!/bin/sh +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# +# + +# Script structure inspired from Apache Karaf and other Apache projects with similar startup approaches + +SCRIPT_DIR=$(dirname "$0") +SCRIPT_NAME=$(basename "$0") +NIFI_TOOLKIT_HOME=$(cd "${SCRIPT_DIR}" && cd .. && pwd) +PROGNAME=$(basename "$0") + +warn() { + (>&2 echo "${PROGNAME}: $*") +} + +die() { + warn "$*" + exit 1 +} + +detectOS() { + # OS specific support (must be 'true' or 'false'). + cygwin=false; + aix=false; + os400=false; + darwin=false; + case "$(uname)" in + CYGWIN*) + cygwin=true + ;; + AIX*) + aix=true + ;; + OS400*) + os400=true + ;; + Darwin) + darwin=true + ;; + esac + # For AIX, set an environment variable + if ${aix}; then + export LDR_CNTRL=MAXDATA=0xB0000000@DSA + echo ${LDR_CNTRL} + fi +} + +locateJava() { + # Setup the Java Virtual Machine + if $cygwin ; then + [ -n "${JAVA}" ] && JAVA=$(cygpath --unix "${JAVA}") + [ -n "${JAVA_HOME}" ] && JAVA_HOME=$(cygpath --unix "${JAVA_HOME}") + fi + + if [ "x${JAVA}" = "x" ] && [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi + if [ "x${JAVA}" = "x" ]; then + if [ "x${JAVA_HOME}" != "x" ]; then + if [ ! -d "${JAVA_HOME}" ]; then + die "JAVA_HOME is not valid: ${JAVA_HOME}" + fi + JAVA="${JAVA_HOME}/bin/java" + else + warn "JAVA_HOME not set; results may vary" + JAVA=$(type java) + JAVA=$(expr "${JAVA}" : '.* \(/.*\)$') + if [ "x${JAVA}" = "x" ]; then + die "java command not found" + fi + fi + fi +} + +init() { + # Determine if there is special OS handling we must perform + detectOS + + # Locate the Java VM to execute + locateJava "$1" +} + +run() { + LIBS="${NIFI_TOOLKIT_HOME}/lib/*" + + sudo_cmd_prefix="" + if $cygwin; then + NIFI_TOOLKIT_HOME=$(cygpath --path --windows "${NIFI_TOOLKIT_HOME}") + CLASSPATH="$NIFI_TOOLKIT_HOME/classpath;$(cygpath --path --windows "${LIBS}")" + else + CLASSPATH="$NIFI_TOOLKIT_HOME/classpath:${LIBS}" + fi + + export JAVA_HOME="$JAVA_HOME" + export NIFI_TOOLKIT_HOME="$NIFI_TOOLKIT_HOME" + + umask 0077 + "${JAVA}" -cp "${CLASSPATH}" -Xms12m -Xmx24m org.apache.nifi.toolkit.admin.filemanager.FileManagerTool "$@" + return $? +} + + +init "$1" +run "$@" \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/rules/v1_2_0/logback-xml.groovy b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/rules/v1_2_0/logback-xml.groovy new file mode 100644 index 000000000000..8ea4726c4019 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/rules/v1_2_0/logback-xml.groovy @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rules.v1_2_0 + +import org.apache.nifi.toolkit.admin.configmigrator.rules.XmlMigrationRule +import org.apache.nifi.toolkit.admin.util.AdminUtil + +class LogbackRule extends XmlMigrationRule{ + + @Override + Boolean supportedVersion(String versionStr) { + return AdminUtil.supportedVersion("1.0.0","1.2.0",versionStr) + } + + @Override + byte[] migrateXml(Node oldXmlContent, Node newXmlContent) { + def configuration = oldXmlContent + configuration.appender.findAll { appender -> + appender.@name == "APP_FILE" + }.each { Node appender -> + def rollingPolicy = appender.rollingPolicy[0] + if(rollingPolicy.@class != "ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy") { + rollingPolicy.@class = "ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy" + def maxFileSize = rollingPolicy.timeBasedFileNamingAndTriggeringPolicy.maxFileSize.text() + rollingPolicy.remove(rollingPolicy.timeBasedFileNamingAndTriggeringPolicy) + def maxFileSizeNode = new Node(rollingPolicy, "maxFileSize", [:]) + maxFileSizeNode.value = maxFileSize + def encoder = appender.encoder[0] + def immediateFlush = encoder.immediateFlush.text() + encoder.remove(encoder.immediateFlush) + def immediateFlushNode = new Node(appender, "immediateFlush", [:]) + immediateFlushNode.value = immediateFlush + } + } + + def hasLogMessage = configuration.findAll { element -> + element.@name == "org.apache.nifi.processors.standard.LogMessage" + }.size() + + def hasCalciteException = configuration.logger.findAll { element -> + element.@name == "org.apache.nifi.processors.standard.LogMessage" + }.size() + + if(hasLogMessage == 0) { + new Node(configuration, 'logger', ["name": "org.apache.nifi.processors.standard.LogMessage", "level": "INFO"]) + } + + if(hasCalciteException == 0) { + new Node(configuration, 'logger', ["name": "org.apache.calcite.runtime.CalciteException", "level": "OFF"]) + } + + return convertToByteArray(configuration) + } + + +} \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/rules/v1_2_0/nifi-properties.groovy b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/rules/v1_2_0/nifi-properties.groovy new file mode 100644 index 000000000000..386936ae307c --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/rules/v1_2_0/nifi-properties.groovy @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rules.v1_2_0 + +import org.apache.nifi.toolkit.admin.configmigrator.rules.PropertyMigrationRule +import org.apache.nifi.toolkit.admin.util.AdminUtil + +class NiFiPropertiesRule extends PropertyMigrationRule{ + @Override + Boolean supportedVersion(String versionStr) { + return AdminUtil.supportedVersion("1.0.0","1.2.0",versionStr) + } + + @Override + boolean keyAllowed(Object key) { + def notAllowed = ["nifi.version","nifi.build.tag","nifi.build.branch","nifi.build.revision","nifi.build.timestamp"] + def keyInList = ((String)key) in notAllowed + return !(keyInList) + } + +} From 4c9ab2e3b2add0b9b37c769e9dbaabbc3d1258c1 Mon Sep 17 00:00:00 2001 From: "Yolanda M. Davis" Date: Mon, 5 Jun 2017 22:44:43 -0400 Subject: [PATCH 2/4] NIFI-3696 - adding in some groovy specific syntatic sugar, minor corrections to exception handling --- .../configmigrator/ConfigMigrator.groovy | 102 ++++--- .../admin/filemanager/FileManagerTool.groovy | 276 +++++++++--------- .../admin/nodemanager/NodeManagerTool.groovy | 8 +- .../admin/notify/NotificationTool.groovy | 8 +- .../nifi/toolkit/admin/util/AdminUtil.groovy | 10 +- 5 files changed, 200 insertions(+), 204 deletions(-) diff --git a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/configmigrator/ConfigMigrator.groovy b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/configmigrator/ConfigMigrator.groovy index bafc87e499b6..5a1deeed8ff5 100644 --- a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/configmigrator/ConfigMigrator.groovy +++ b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/configmigrator/ConfigMigrator.groovy @@ -31,10 +31,10 @@ import java.nio.file.Paths public class ConfigMigrator { - private final static String SUPPORTED_MINIMUM_VERSION = "1.0.0" + private final static String SUPPORTED_MINIMUM_VERSION = '1.0.0' private final String RULES_DIR = getRulesDirectory() private final Boolean overwrite - protected Logger logger = LoggerFactory.getLogger(ConfigMigrator.class) + protected Logger logger = LoggerFactory.getLogger(ConfigMigrator) protected final Boolean isVerbose public ConfigMigrator(Boolean verbose, Boolean overwrite) { @@ -44,15 +44,15 @@ public class ConfigMigrator { String getRulesDirectory() { final ClassLoader cl = this.getClass().getClassLoader() - cl.getResource("rules").path.replaceAll("%20"," "); + cl.getResource('rules').path.replaceAll('%20',' ') } List getRulesDirectoryName(final String currentVersion, final String upgradeVersion) { - Version current = new Version(currentVersion.take(5).toString(),".") - Version upgrade = new Version(upgradeVersion.take(5).toString(),".") + Version current = new Version(currentVersion.take(5).toString(),'.') + Version upgrade = new Version(upgradeVersion.take(5).toString(),'.') File rulesDir = new File(rulesDirectory) List rules = Lists.newArrayList(rulesDir.listFiles()) - List versions = rules.collect { new Version(it.name.substring(1,it.name.length()),"_")} + List versions = rules.collect { new Version(it.name[1..-1],'_')} versions.sort(Version.VERSION_COMPARATOR) List matches = versions.findAll { Version.VERSION_COMPARATOR.compare(it,upgrade) <= 0 && Version.VERSION_COMPARATOR.compare(it,current) == 1} @@ -61,36 +61,30 @@ public class ConfigMigrator { }else{ matches.sort(Version.VERSION_COMPARATOR) List directoryNames = [] - matches.each { directoryNames.add(RULES_DIR + File.separator + "v" + it.toString()) } - return directoryNames + matches.each { directoryNames.add(RULES_DIR + File.separator + 'v' + it.toString()) } + directoryNames } } Boolean supportedVersion(final File script, final String currentVersion) { - final Class ruleClass = new GroovyClassLoader(getClass().getClassLoader()).parseClass(script); - final GroovyObject ruleObject = (GroovyObject) ruleClass.newInstance(); - ruleObject.invokeMethod("supportedVersion", [currentVersion]) + final Class ruleClass = new GroovyClassLoader(getClass().getClassLoader()).parseClass(script) + final GroovyObject ruleObject = (GroovyObject) ruleClass.newInstance() + ruleObject.invokeMethod('supportedVersion', [currentVersion]) } byte[] migrateContent(final File script, final byte[] content, final byte[] upgradeContent) { - final Class ruleClass = new GroovyClassLoader(getClass().getClassLoader()).parseClass(script); - final GroovyObject ruleObject = (GroovyObject) ruleClass.newInstance(); - ruleObject.invokeMethod("migrate", [content, upgradeContent]) + final Class ruleClass = new GroovyClassLoader(getClass().getClassLoader()).parseClass(script) + final GroovyObject ruleObject = (GroovyObject) ruleClass.newInstance() + ruleObject.invokeMethod('migrate', [content, upgradeContent]) } String getScriptRuleName(final String fileName) { - fileName.replace(".", "-") + ".groovy" + fileName.replace('.', '-') + '.groovy' } - File getUpgradeFile(File upgradeDir, String fileName){ - - final File[] upgradeFiles = upgradeDir.listFiles(new FilenameFilter() { - @Override - boolean accept(File dir, String name) { - name.equals(fileName) - } - }) + File getUpgradeFile(final File upgradeDir, final String fileName){ + final File[] upgradeFiles = upgradeDir.listFiles({dir, name -> name == fileName }as FilenameFilter) upgradeFiles.size() == 1 ? upgradeFiles[0] : new File(upgradeDir.path + File.separator + fileName) } @@ -100,11 +94,11 @@ public class ConfigMigrator { final String nifiUpgradeVersion = AdminUtil.getNiFiVersion(nifiUpgradeConfigDir,nifiUpgradeLibDir) if (nifiCurrentVersion == null) { - throw new IllegalArgumentException("Could not determine current nifi version") + throw new IllegalArgumentException('Could not determine current nifi version') } if (nifiUpgradeVersion == null) { - throw new IllegalArgumentException("Could not determine upgrade nifi version") + throw new IllegalArgumentException('Could not determine upgrade nifi version') } final List nifiConfigFiles = Lists.newArrayList(nifiConfDir.listFiles()) @@ -131,14 +125,14 @@ public class ConfigMigrator { if (script.exists() && supportedVersion(script, nifiCurrentVersion)) { if (isVerbose) { - logger.info("Applying rules to {} from directory {} ", file.name, ruleDir) + logger.info('Applying rules to {} from directory {} ', file.name, ruleDir) } content = migrateContent(script, content, upgradeFile.exists() ? upgradeFile.bytes : new byte[0]) } else { if (isVerbose) { - logger.info("No migration rule exists in {} for file {}. ",ruleDir,file.getName()) + logger.info('No migration rule exists in {} for file {}. ',ruleDir,file.getName()) } } @@ -161,12 +155,12 @@ public class ConfigMigrator { }else{ if(isVerbose) { - logger.info("No upgrade rules are required for these configurations.") + logger.info('No upgrade rules are required for these configurations.') } if(!this.overwrite){ if(isVerbose) { - logger.info("Copying configurations over to upgrade directory") + logger.info('Copying configurations over to upgrade directory') } nifiConfigFiles.each { file -> @@ -187,40 +181,42 @@ public class ConfigMigrator { File bootstrapConf = Paths.get(bootstrapConfFile).toFile() if (!bootstrapConf.exists()) { - throw new IllegalArgumentException("NiFi Bootstrap File provided does not exist: " + bootstrapConfFile) + throw new IllegalArgumentException('NiFi Bootstrap File provided does not exist: ' + bootstrapConfFile) } - Properties bootstrapProperties = AdminUtil.getBootstrapConf(bootstrapConfPath) - File nifiConfDir = new File(AdminUtil.getRelativeDirectory(bootstrapProperties.getProperty("conf.dir"), nifiCurrentDir)) - File nifiLibDir = new File(AdminUtil.getRelativeDirectory(bootstrapProperties.getProperty("lib.dir"), nifiCurrentDir)) - final File nifiUpgradeConfDir = Paths.get(nifiUpgDirString,"conf").toFile() - final File nifiUpgradeLibDir = Paths.get(nifiUpgDirString,"lib").toFile() + AdminUtil.with{ - if(AdminUtil.supportedNiFiMinimumVersion(nifiConfDir.canonicalPath, nifiLibDir.canonicalPath, SUPPORTED_MINIMUM_VERSION) && - AdminUtil.supportedNiFiMinimumVersion(nifiUpgradeConfDir.canonicalPath, nifiUpgradeLibDir.canonicalPath, SUPPORTED_MINIMUM_VERSION)) { + Properties bootstrapProperties = getBootstrapConf(bootstrapConfPath) + File nifiConfDir = new File(getRelativeDirectory(bootstrapProperties.getProperty('conf.dir'), nifiCurrentDir)) + File nifiLibDir = new File(getRelativeDirectory(bootstrapProperties.getProperty('lib.dir'), nifiCurrentDir)) + final File nifiUpgradeConfDir = Paths.get(nifiUpgDirString,'conf').toFile() + final File nifiUpgradeLibDir = Paths.get(nifiUpgDirString,'lib').toFile() - if (!nifiConfDir.exists() || !nifiConfDir.isDirectory()) { - throw new IllegalArgumentException("NiFi Configuration Directory provided is not valid: " + nifiConfDir.absolutePath) - } + if(supportedNiFiMinimumVersion(nifiConfDir.canonicalPath, nifiLibDir.canonicalPath, SUPPORTED_MINIMUM_VERSION) && + supportedNiFiMinimumVersion(nifiUpgradeConfDir.canonicalPath, nifiUpgradeLibDir.canonicalPath, SUPPORTED_MINIMUM_VERSION)) { - if (!nifiUpgradeConfDir.exists() || !nifiUpgradeConfDir.isDirectory()) { - throw new IllegalArgumentException("Upgrade Configuration Directory provided is not valid: " + nifiUpgradeConfDir) - } + if (!nifiConfDir.exists() || !nifiConfDir.isDirectory()) { + throw new IllegalArgumentException('NiFi Configuration Directory provided is not valid: ' + nifiConfDir.absolutePath) + } - if (isVerbose) { - logger.info("Migrating configurations from {} to {}", nifiConfDir.absolutePath, nifiUpgradeConfDir.absolutePath) - } + if (!nifiUpgradeConfDir.exists() || !nifiUpgradeConfDir.isDirectory()) { + throw new IllegalArgumentException('Upgrade Configuration Directory provided is not valid: ' + nifiUpgradeConfDir) + } - migrate(nifiConfDir,nifiLibDir,nifiUpgradeConfDir,nifiUpgradeLibDir,bootstrapConf,nifiCurrentDir) + if (isVerbose) { + logger.info('Migrating configurations from {} to {}', nifiConfDir.absolutePath, nifiUpgradeConfDir.absolutePath) + } - if (isVerbose) { - logger.info("Migration completed.") - } + migrate(nifiConfDir,nifiLibDir,nifiUpgradeConfDir,nifiUpgradeLibDir,bootstrapConf,nifiCurrentDir) - }else{ - throw new UnsupportedOperationException("Config Migration Tool only supports NiFi version 1.0.0 and above") - } + if (isVerbose) { + logger.info('Migration completed.') + } + }else{ + throw new UnsupportedOperationException('Config Migration Tool only supports NiFi version 1.0.0 and above') + } + } } diff --git a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/filemanager/FileManagerTool.groovy b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/filemanager/FileManagerTool.groovy index 0bf5f07f3630..f71182f0f431 100644 --- a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/filemanager/FileManagerTool.groovy +++ b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/filemanager/FileManagerTool.groovy @@ -48,20 +48,20 @@ import java.nio.file.attribute.PosixFilePermission public class FileManagerTool extends AbstractAdminTool{ - private static final String DEFAULT_DESCRIPTION = "This tool is used to perform backup, install and restore activities for a NiFi node. " - private static final String HELP_ARG = "help" - private static final String VERBOSE_ARG = "verbose" - private static final String OPERATION = "operation" - private static final String NIFI_CURRENT_DIR = "nifiCurrentDir" - private static final String NIFI_INSTALL_DIR = "nifiInstallDir" - private static final String NIFI_ROLLBACK_DIR = "nifiRollbackDir" - private static final String BACKUP_DIR = "backupDir" - private static final String INSTALL_FILE = "installFile" - private static final String MOVE_REPOSITORIES = "moveRepositories" - private static final String OVERWRITE_CONFIGS = "overwriteConfigs" - private static final String BOOTSTRAP_CONF = "bootstrapConf" + private static final String DEFAULT_DESCRIPTION = 'This tool is used to perform backup, install and restore activities for a NiFi node. ' + private static final String HELP_ARG = 'help' + private static final String VERBOSE_ARG = 'verbose' + private static final String OPERATION = 'operation' + private static final String NIFI_CURRENT_DIR = 'nifiCurrentDir' + private static final String NIFI_INSTALL_DIR = 'nifiInstallDir' + private static final String NIFI_ROLLBACK_DIR = 'nifiRollbackDir' + private static final String BACKUP_DIR = 'backupDir' + private static final String INSTALL_FILE = 'installFile' + private static final String MOVE_REPOSITORIES = 'moveRepositories' + private static final String OVERWRITE_CONFIGS = 'overwriteConfigs' + private static final String BOOTSTRAP_CONF = 'bootstrapConf' private boolean moveRepositories = false - private final static String SUPPORTED_MINIMUM_VERSION = "1.0.0" + private final static String SUPPORTED_MINIMUM_VERSION = '1.0.0' private static final List POSIX_PERMISSIONS = [PosixFilePermission.OTHERS_EXECUTE, PosixFilePermission.OTHERS_WRITE, @@ -71,7 +71,7 @@ public class FileManagerTool extends AbstractAdminTool{ PosixFilePermission.GROUP_READ, PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.OWNER_WRITE, - PosixFilePermission.OWNER_READ]; + PosixFilePermission.OWNER_READ] FileManagerTool() { @@ -81,49 +81,49 @@ public class FileManagerTool extends AbstractAdminTool{ @Override protected Logger getLogger() { - LoggerFactory.getLogger(FileManagerTool.class) + LoggerFactory.getLogger(FileManagerTool) } protected Options getOptions(){ final Options options = new Options() - options.addOption(Option.builder("o").longOpt(OPERATION).hasArg().desc("File operation (install | backup | restore)").build()) - options.addOption(Option.builder("b").longOpt(BACKUP_DIR).hasArg().desc("Backup NiFi Directory (used with backup or restore operation)").build()) - options.addOption(Option.builder("c").longOpt(NIFI_CURRENT_DIR).hasArg().desc("Current NiFi Installation Directory (used optionally with install or restore operation)").build()) - options.addOption(Option.builder("d").longOpt(NIFI_INSTALL_DIR).hasArg().desc("NiFi Installation Directory (used with install or restore operation)").build()) - options.addOption(Option.builder("i").longOpt(INSTALL_FILE).hasArg().desc("NiFi Install File").build()) - options.addOption(Option.builder("r").longOpt(NIFI_ROLLBACK_DIR).hasArg().desc("NiFi Installation Directory (used with install or restore operation)").build()) - options.addOption(Option.builder("t").longOpt(BOOTSTRAP_CONF).hasArg().desc("Current NiFi Bootstrap Configuration File (optional)").build()) - options.addOption(Option.builder("m").longOpt(MOVE_REPOSITORIES).desc("Allow repositories to be moved to new/restored nifi directory from existing installation, if available (used optionally with install or restore operation)").build()) - options.addOption(Option.builder("x").longOpt(OVERWRITE_CONFIGS).desc("Overwrite existing configuration directory with upgrade changes (used optionally with install or restore operation)").build()) - options.addOption(Option.builder("h").longOpt(HELP_ARG).desc("Print help info (optional)").build()) - options.addOption(Option.builder("v").longOpt(VERBOSE_ARG).desc("Set mode to verbose (optional, default is false)").build()) + options.addOption(Option.builder('o').longOpt(OPERATION).hasArg().desc('File operation (install | backup | restore)').build()) + options.addOption(Option.builder('b').longOpt(BACKUP_DIR).hasArg().desc('Backup NiFi Directory (used with backup or restore operation)').build()) + options.addOption(Option.builder('c').longOpt(NIFI_CURRENT_DIR).hasArg().desc('Current NiFi Installation Directory (used optionally with install or restore operation)').build()) + options.addOption(Option.builder('d').longOpt(NIFI_INSTALL_DIR).hasArg().desc('NiFi Installation Directory (used with install or restore operation)').build()) + options.addOption(Option.builder('i').longOpt(INSTALL_FILE).hasArg().desc('NiFi Install File').build()) + options.addOption(Option.builder('r').longOpt(NIFI_ROLLBACK_DIR).hasArg().desc('NiFi Installation Directory (used with install or restore operation)').build()) + options.addOption(Option.builder('t').longOpt(BOOTSTRAP_CONF).hasArg().desc('Current NiFi Bootstrap Configuration File (optional)').build()) + options.addOption(Option.builder('m').longOpt(MOVE_REPOSITORIES).desc('Allow repositories to be moved to new/restored nifi directory from existing installation, if available (used optionally with install or restore operation)').build()) + options.addOption(Option.builder('x').longOpt(OVERWRITE_CONFIGS).desc('Overwrite existing configuration directory with upgrade changes (used optionally with install or restore operation)').build()) + options.addOption(Option.builder('h').longOpt(HELP_ARG).desc('Print help info (optional)').build()) + options.addOption(Option.builder('v').longOpt(VERBOSE_ARG).desc('Set mode to verbose (optional, default is false)').build()) options } Set fromMode(final long mode) { - Set permissions = Sets.newHashSet(); + Set permissions = Sets.newHashSet() POSIX_PERMISSIONS.eachWithIndex{ perm,index -> if ((mode & (1 << index)) != 0) { - permissions.add(perm); + permissions.add(perm) } } - return permissions; + permissions } Properties getProperties(Path confFileName){ final Properties properties = new Properties() final File confFile = confFileName.toFile() properties.load(new FileInputStream(confFile)) - return properties + properties } boolean valid(File nifiDir){ - if(nifiDir.isDirectory() && Files.exists(Paths.get(nifiDir.absolutePath,"bin","nifi.sh"))){ + if(nifiDir.isDirectory() && Files.exists(Paths.get(nifiDir.absolutePath,'bin','nifi.sh'))){ true }else { false @@ -132,8 +132,8 @@ public class FileManagerTool extends AbstractAdminTool{ void move(final String srcDir, final String oldDir, final String newDir){ - final String oldPathName = srcDir.startsWith("./") ? oldDir + File.separator + srcDir.substring(2,srcDir.length()) : oldDir + File.separator + srcDir - final String newPathName = srcDir.startsWith("./") ? newDir + File.separator + srcDir.substring(2,srcDir.length()) : newDir + File.separator + srcDir + final String oldPathName = srcDir.startsWith('./') ? oldDir + File.separator + srcDir[2..-1] : oldDir + File.separator + srcDir + final String newPathName = srcDir.startsWith('./') ? newDir + File.separator + srcDir[2..-1] : newDir + File.separator + srcDir final Path oldPath = Paths.get(oldPathName) final Path newPath = Paths.get(newPathName) @@ -147,42 +147,42 @@ public class FileManagerTool extends AbstractAdminTool{ void moveRepository(final String dirName, final String installDirName){ if(isVerbose){ - logger.info("Moving repositories from {} to {}:",dirName,installDirName) + logger.info('Moving repositories from {} to {}:',dirName,installDirName) } - final String bootstrapConfFileName = dirName + File.separator + "conf" + File.separator + "bootstrap.conf" + final String bootstrapConfFileName = dirName + File.separator + 'conf' + File.separator + 'bootstrap.conf' final Properties bootstrapProperties = getProperties(Paths.get(bootstrapConfFileName)) - final String nifiPropertiesFile = AdminUtil.getRelativeDirectory(bootstrapProperties.getProperty("conf.dir"),dirName) + File.separator +"nifi.properties" + final String nifiPropertiesFile = AdminUtil.getRelativeDirectory(bootstrapProperties.getProperty('conf.dir'),dirName) + File.separator +'nifi.properties' final Properties nifiProperties = getProperties(Paths.get(nifiPropertiesFile)) - final String flowFileDirectory = nifiProperties.getProperty("nifi.flowfile.repository.directory") - final String contentRepositoryDir = nifiProperties.getProperty("nifi.content.repository.directory.default") - final String provenanceRepositoryDir = nifiProperties.getProperty("nifi.provenance.repository.directory.default") - final String databaseDirectory = nifiProperties.getProperty("nifi.database.directory") + final String flowFileDirectory = nifiProperties.getProperty('nifi.flowfile.repository.directory') + final String contentRepositoryDir = nifiProperties.getProperty('nifi.content.repository.directory.default') + final String provenanceRepositoryDir = nifiProperties.getProperty('nifi.provenance.repository.directory.default') + final String databaseDirectory = nifiProperties.getProperty('nifi.database.directory') - if(flowFileDirectory.startsWith("./")){ + if(flowFileDirectory.startsWith('./')){ if(isVerbose){ - logger.info("Moving flowfile repo") + logger.info('Moving flowfile repo') } move(flowFileDirectory,dirName,installDirName) } - if(contentRepositoryDir.startsWith("./")){ + if(contentRepositoryDir.startsWith('./')){ if(isVerbose){ - logger.info("Moving content repo") + logger.info('Moving content repo') } move(contentRepositoryDir,dirName,installDirName) } - if(provenanceRepositoryDir.startsWith("./")){ + if(provenanceRepositoryDir.startsWith('./')){ if(isVerbose){ - logger.info("Moving provenance repo") + logger.info('Moving provenance repo') } move(provenanceRepositoryDir,dirName,installDirName) } - if(databaseDirectory.startsWith("./")){ + if(databaseDirectory.startsWith('./')){ if(isVerbose){ - logger.info("Moving database repo") + logger.info('Moving database repo') } move(databaseDirectory,dirName,installDirName) } @@ -190,12 +190,12 @@ public class FileManagerTool extends AbstractAdminTool{ void copyState(final String currentNiFiDirName, final String installDirName){ - File stateDir = Paths.get(currentNiFiDirName,"state").toFile() + File stateDir = Paths.get(currentNiFiDirName,'state').toFile() if(stateDir.exists()){ - if(Files.exists(Paths.get(installDirName,"state"))){ - Files.delete(Paths.get(installDirName,"state")) + if(Files.exists(Paths.get(installDirName,'state'))){ + Files.delete(Paths.get(installDirName,'state')) } FileUtils.copyDirectoryToDirectory(stateDir, Paths.get(installDirName).toFile()) @@ -207,10 +207,10 @@ public class FileManagerTool extends AbstractAdminTool{ int mode = 0 if (entry instanceof TarArchiveEntry) { - mode = ((TarArchiveEntry) entry).getMode(); + mode = ((TarArchiveEntry) entry).getMode() }else if(entry instanceof ZipArchiveEntry && zipFile != null){ - mode = zipFile.getEntry(entry.name).getUnixMode(); + mode = zipFile.getEntry(entry.name).getUnixMode() } if(mode == 0){ @@ -219,7 +219,7 @@ public class FileManagerTool extends AbstractAdminTool{ Set permissions = fromMode(mode) if(permissions.size() > 0) { - Files.setPosixFilePermissions(outputFile.toPath(), fromMode(mode)); + Files.setPosixFilePermissions(outputFile.toPath(), fromMode(mode)) } } @@ -239,13 +239,13 @@ public class FileManagerTool extends AbstractAdminTool{ void backup(String backupNiFiDirName, String currentNiFiDirName, String bootstrapConfFileName){ if(isVerbose){ - logger.info("Creating backup in directory:" + backupNiFiDirName) + logger.info('Creating backup in directory:' + backupNiFiDirName) } final File backupNiFiDir = new File(backupNiFiDirName) final Properties bootstrapProperties = getProperties(Paths.get(bootstrapConfFileName)) - final File confDir = new File(AdminUtil.getRelativeDirectory(bootstrapProperties.getProperty("conf.dir"),currentNiFiDirName)) - final File libDir = new File(AdminUtil.getRelativeDirectory(bootstrapProperties.getProperty("lib.dir"),currentNiFiDirName)) + final File confDir = new File(AdminUtil.getRelativeDirectory(bootstrapProperties.getProperty('conf.dir'),currentNiFiDirName)) + final File libDir = new File(AdminUtil.getRelativeDirectory(bootstrapProperties.getProperty('lib.dir'),currentNiFiDirName)) if( backupNiFiDir.exists() && backupNiFiDir.isDirectory()){ backupNiFiDir.deleteDir() @@ -253,21 +253,21 @@ public class FileManagerTool extends AbstractAdminTool{ backupNiFiDir.mkdirs() - Files.createDirectory(Paths.get(backupNiFiDirName,"bootstrap_files")) - FileUtils.copyFileToDirectory(Paths.get(bootstrapConfFileName).toFile(),Paths.get(backupNiFiDirName,"bootstrap_files").toFile()) - FileUtils.copyDirectoryToDirectory(Paths.get(currentNiFiDirName,"lib","bootstrap").toFile(),Paths.get(backupNiFiDirName,"bootstrap_files").toFile()) - Files.createDirectories(Paths.get(backupNiFiDirName,"conf")) - Files.createDirectories(Paths.get(backupNiFiDirName,"lib")) + Files.createDirectory(Paths.get(backupNiFiDirName,'bootstrap_files')) + FileUtils.copyFileToDirectory(Paths.get(bootstrapConfFileName).toFile(),Paths.get(backupNiFiDirName,'bootstrap_files').toFile()) + FileUtils.copyDirectoryToDirectory(Paths.get(currentNiFiDirName,'lib','bootstrap').toFile(),Paths.get(backupNiFiDirName,'bootstrap_files').toFile()) + Files.createDirectories(Paths.get(backupNiFiDirName,'conf')) + Files.createDirectories(Paths.get(backupNiFiDirName,'lib')) FileUtils.copyDirectoryToDirectory(confDir,Paths.get(backupNiFiDirName).toFile()) FileUtils.copyDirectoryToDirectory(libDir,Paths.get(backupNiFiDirName).toFile()) - FileUtils.copyDirectoryToDirectory(Paths.get(currentNiFiDirName,"bin").toFile(),new File(backupNiFiDirName)) - FileUtils.copyDirectoryToDirectory(Paths.get(currentNiFiDirName,"docs").toFile(),new File(backupNiFiDirName)) - FileUtils.copyFileToDirectory(Paths.get(currentNiFiDirName,"LICENSE").toFile(),new File(backupNiFiDirName)) - FileUtils.copyFileToDirectory(Paths.get(currentNiFiDirName,"NOTICE").toFile(),new File(backupNiFiDirName)) - FileUtils.copyFileToDirectory(Paths.get(currentNiFiDirName,"README").toFile(),new File(backupNiFiDirName)) + FileUtils.copyDirectoryToDirectory(Paths.get(currentNiFiDirName,'bin').toFile(),new File(backupNiFiDirName)) + FileUtils.copyDirectoryToDirectory(Paths.get(currentNiFiDirName,'docs').toFile(),new File(backupNiFiDirName)) + FileUtils.copyFileToDirectory(Paths.get(currentNiFiDirName,'LICENSE').toFile(),new File(backupNiFiDirName)) + FileUtils.copyFileToDirectory(Paths.get(currentNiFiDirName,'NOTICE').toFile(),new File(backupNiFiDirName)) + FileUtils.copyFileToDirectory(Paths.get(currentNiFiDirName,'README').toFile(),new File(backupNiFiDirName)) if(isVerbose){ - logger.info("Backup Complete") + logger.info('Backup Complete') } } @@ -275,15 +275,15 @@ public class FileManagerTool extends AbstractAdminTool{ void restore(String backupNiFiDirName, String rollbackNiFiDirName, String currentNiFiDirName, String bootstrapConfFileName){ if(isVerbose){ - logger.info("Restoring to directory:" + rollbackNiFiDirName) + logger.info('Restoring to directory:' + rollbackNiFiDirName) } final File rollbackNiFiDir = new File(rollbackNiFiDirName) - final File rollbackNiFiLibDir = Paths.get(rollbackNiFiDirName,"lib").toFile() - final File rollbackNiFiConfDir = Paths.get(rollbackNiFiDirName,"conf").toFile() - final Properties bootstrapProperties = getProperties(Paths.get(backupNiFiDirName,"bootstrap_files","bootstrap.conf")) - final File confDir = new File(AdminUtil.getRelativeDirectory(bootstrapProperties.getProperty("conf.dir"),rollbackNiFiDirName)) - final File libDir = new File(AdminUtil.getRelativeDirectory(bootstrapProperties.getProperty("lib.dir"),rollbackNiFiDirName)) + final File rollbackNiFiLibDir = Paths.get(rollbackNiFiDirName,'lib').toFile() + final File rollbackNiFiConfDir = Paths.get(rollbackNiFiDirName,'conf').toFile() + final Properties bootstrapProperties = getProperties(Paths.get(backupNiFiDirName,'bootstrap_files','bootstrap.conf')) + final File confDir = new File(AdminUtil.getRelativeDirectory(bootstrapProperties.getProperty('conf.dir'),rollbackNiFiDirName)) + final File libDir = new File(AdminUtil.getRelativeDirectory(bootstrapProperties.getProperty('lib.dir'),rollbackNiFiDirName)) if(!rollbackNiFiDir.isDirectory()){ @@ -306,27 +306,27 @@ public class FileManagerTool extends AbstractAdminTool{ confDir.mkdirs() } - FileUtils.copyFile(Paths.get(backupNiFiDirName,"bootstrap_files","bootstrap.conf").toFile(), new File(bootstrapConfFileName)) - FileUtils.copyDirectoryToDirectory(Paths.get(backupNiFiDirName,"bootstrap_files","bootstrap").toFile(),Paths.get(rollbackNiFiDirName,"lib").toFile()) - FileUtils.copyDirectoryToDirectory(Paths.get(backupNiFiDirName,"bin").toFile(),new File(rollbackNiFiDirName)) - FileUtils.copyDirectoryToDirectory(Paths.get(backupNiFiDirName,"docs").toFile(),new File(rollbackNiFiDirName)) - FileUtils.copyDirectory(Paths.get(backupNiFiDirName,"lib").toFile(),libDir) - FileUtils.copyDirectory(Paths.get(backupNiFiDirName,"conf").toFile(),confDir) - FileUtils.copyFileToDirectory(Paths.get(backupNiFiDirName,"LICENSE").toFile(),new File(rollbackNiFiDirName)) - FileUtils.copyFileToDirectory(Paths.get(backupNiFiDirName,"NOTICE").toFile(),new File(rollbackNiFiDirName)) - FileUtils.copyFileToDirectory(Paths.get(backupNiFiDirName,"README").toFile(),new File(rollbackNiFiDirName)) + FileUtils.copyFile(Paths.get(backupNiFiDirName,'bootstrap_files','bootstrap.conf').toFile(), new File(bootstrapConfFileName)) + FileUtils.copyDirectoryToDirectory(Paths.get(backupNiFiDirName,'bootstrap_files','bootstrap').toFile(),Paths.get(rollbackNiFiDirName,'lib').toFile()) + FileUtils.copyDirectoryToDirectory(Paths.get(backupNiFiDirName,'bin').toFile(),new File(rollbackNiFiDirName)) + FileUtils.copyDirectoryToDirectory(Paths.get(backupNiFiDirName,'docs').toFile(),new File(rollbackNiFiDirName)) + FileUtils.copyDirectory(Paths.get(backupNiFiDirName,'lib').toFile(),libDir) + FileUtils.copyDirectory(Paths.get(backupNiFiDirName,'conf').toFile(),confDir) + FileUtils.copyFileToDirectory(Paths.get(backupNiFiDirName,'LICENSE').toFile(),new File(rollbackNiFiDirName)) + FileUtils.copyFileToDirectory(Paths.get(backupNiFiDirName,'NOTICE').toFile(),new File(rollbackNiFiDirName)) + FileUtils.copyFileToDirectory(Paths.get(backupNiFiDirName,'README').toFile(),new File(rollbackNiFiDirName)) - final File binDir = Paths.get(rollbackNiFiDirName,"bin").toFile() + final File binDir = Paths.get(rollbackNiFiDirName,'bin').toFile() binDir.listFiles().each { setPosixPermissions(it,[PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_EXECUTE, PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_EXECUTE]) } - if(!StringUtils.isEmpty(currentNiFiDirName) && moveRepositories) { + if(currentNiFiDirName && moveRepositories) { moveRepository(currentNiFiDirName, rollbackNiFiDirName) } if(isVerbose){ - logger.info("Restore Completed.") + logger.info('Restore Completed.') } } @@ -334,13 +334,13 @@ public class FileManagerTool extends AbstractAdminTool{ String extract(final File installFile, final File installDirName){ if(isVerbose){ - logger.info("Beginning extraction using {} into installation directory {}",installFile.absolutePath,installDirName.absolutePath) + logger.info('Beginning extraction using {} into installation directory {}',installFile.absolutePath,installDirName.absolutePath) } final String extension = FilenameUtils.getExtension(installFile.getName()) - final InputStream fis = extension.equals("gz") ? new GzipCompressorInputStream(new FileInputStream(installFile)) : new FileInputStream(installFile) + final InputStream fis = extension.equals('gz') ? new GzipCompressorInputStream(new FileInputStream(installFile)) : new FileInputStream(installFile) final ArchiveInputStream inputStream = new ArchiveStreamFactory().createArchiveInputStream(new BufferedInputStream(fis)) - final ZipFile zipFile = extension.equals("zip") ? new ZipFile(installFile) : null + final ZipFile zipFile = extension.equals('zip') ? new ZipFile(installFile) : null ArchiveEntry entry = inputStream.nextEntry @@ -350,12 +350,12 @@ public class FileManagerTool extends AbstractAdminTool{ while(entry != null){ - if(archiveRootDir == null & entry.name.toLowerCase().startsWith("nifi")){ + if(archiveRootDir == null & entry.name.toLowerCase().startsWith('nifi')){ - archiveRootDir = entry.name.indexOf(File.separator) > -1 ? entry.name.substring(0, entry.getName().indexOf(File.separator)) : entry.name + archiveRootDir = entry.name.indexOf(File.separator) > -1 ? entry.name[0..entry.name.indexOf(File.separator)-1] : entry.name if(isVerbose){ - logger.info("Upgrade root directory: {}", archiveRootDir) + logger.info('Upgrade root directory: {}', archiveRootDir) } File archiveRootDirFile = Paths.get(installDirName.getAbsolutePath(),archiveRootDir).toFile() @@ -367,18 +367,18 @@ public class FileManagerTool extends AbstractAdminTool{ } if(isVerbose){ - logger.info("Extracting file: {} ",entry.name) + logger.info('Extracting file: {} ',entry.name) } - if(archiveRootDir != null && entry.name.startsWith(archiveRootDir)) { + if(archiveRootDir && entry.name.startsWith(archiveRootDir)) { - final File outputFile = Paths.get(installDirName.getAbsolutePath(),entry.name).toFile(); + final File outputFile = Paths.get(installDirName.getAbsolutePath(),entry.name).toFile() if (entry.isDirectory()) { if (!outputFile.exists()) { if (!outputFile.mkdirs()) { - throw new IllegalStateException(String.format("Couldn't create directory %s.", outputFile.getAbsolutePath())); + throw new IllegalStateException('Could not create directory :' + outputFile.getAbsolutePath()) } } @@ -390,9 +390,9 @@ public class FileManagerTool extends AbstractAdminTool{ parentDirectory.mkdirs() } - final OutputStream outputFileStream = new FileOutputStream(outputFile); - IOUtils.copy(inputStream, outputFileStream); - outputFileStream.close(); + final OutputStream outputFileStream = new FileOutputStream(outputFile) + IOUtils.copy(inputStream, outputFileStream) + outputFileStream.close() } if(!SystemUtils.IS_OS_WINDOWS){ @@ -406,7 +406,7 @@ public class FileManagerTool extends AbstractAdminTool{ return archiveRootDir }else{ - throw new RuntimeException("Attempting to extract installation file however it is empty: "+installFile.getName()) + throw new RuntimeException('Attempting to extract installation file however it is empty: '+installFile.getName()) } } @@ -416,7 +416,7 @@ public class FileManagerTool extends AbstractAdminTool{ final File installFile = new File(installFileName) if(isVerbose){ - logger.info("Beginning installation into directory:" + installDirName) + logger.info('Beginning installation into directory:' + installDirName) } if(installFile.exists()){ @@ -432,7 +432,7 @@ public class FileManagerTool extends AbstractAdminTool{ if(valid(installRootDir)){ - if(!StringUtils.isEmpty(currentNiFiDirName) && !StringUtils.isEmpty(bootstrapConfFileName)){ + if(currentNiFiDirName && bootstrapConfFileName){ copyState(currentNiFiDirName,installRootDir.absolutePath) if(moveRepositories) { moveRepository(currentNiFiDirName,installRootDir.absolutePath) @@ -442,15 +442,15 @@ public class FileManagerTool extends AbstractAdminTool{ } }else{ - throw new RuntimeException("Extract failed: Invalid NiFi Installation. Check the install path provided and retry.") + throw new RuntimeException('Extract failed: Invalid NiFi Installation. Check the install path provided and retry.') } }else{ - throw new RuntimeException("Installation file provided does not exist") + throw new RuntimeException('Installation file provided does not exist') } if(isVerbose){ - logger.info("Installation Complete") + logger.info('Installation Complete') } } @@ -462,57 +462,57 @@ public class FileManagerTool extends AbstractAdminTool{ } if(!commandLine.hasOption(INSTALL_FILE)){ - throw new ParseException("Missing -i option") + throw new ParseException('Missing -i option') } else if(!commandLine.hasOption(NIFI_INSTALL_DIR)){ - throw new ParseException("Missing -d option") + throw new ParseException('Missing -d option') } else if (!commandLine.hasOption(NIFI_CURRENT_DIR) && moveRepositories){ - throw new ParseException("Missing -c option: Moving repositories requires current nifi directory") + throw new ParseException('Missing -c option: Moving repositories requires current nifi directory') } final String installFileName = commandLine.getOptionValue(INSTALL_FILE) final String nifiCurrentDirName = commandLine.getOptionValue(NIFI_CURRENT_DIR) final String nifiInstallDirName = commandLine.getOptionValue(NIFI_INSTALL_DIR) final Boolean overwriteConfigs = commandLine.hasOption(OVERWRITE_CONFIGS) - final String bootstrapConfFileName = commandLine.hasOption(BOOTSTRAP_CONF) ? commandLine.getOptionValue(BOOTSTRAP_CONF) : !StringUtils.isEmpty(nifiCurrentDirName) ? - Paths.get(nifiCurrentDirName,"conf","bootstrap.conf").toString() : null + final String bootstrapConfFileName = commandLine.hasOption(BOOTSTRAP_CONF) ? commandLine.getOptionValue(BOOTSTRAP_CONF) : nifiCurrentDirName ? + Paths.get(nifiCurrentDirName,'conf','bootstrap.conf').toString() : null if (Files.notExists(Paths.get(installFileName))) { - throw new ParseException("Missing installation file: " + installFileName) + throw new ParseException('Missing installation file: ' + installFileName) } - if (!StringUtils.isEmpty(nifiCurrentDirName) && Files.notExists(Paths.get(nifiCurrentDirName))) { - throw new ParseException("Current NiFi installation path does not exist: " + nifiCurrentDirName) + if (nifiCurrentDirName && Files.notExists(Paths.get(nifiCurrentDirName))) { + throw new ParseException('Current NiFi installation path does not exist: ' + nifiCurrentDirName) } - if(!StringUtils.isEmpty(nifiCurrentDirName) && !StringUtils.isEmpty(bootstrapConfFileName) && !supportedNiFiMinimumVersion(nifiCurrentDirName, bootstrapConfFileName, SUPPORTED_MINIMUM_VERSION)) { - throw new UnsupportedOperationException("File Manager Tool only supports NiFi versions 1.0.0 or higher.") + if(nifiCurrentDirName && bootstrapConfFileName && !supportedNiFiMinimumVersion(nifiCurrentDirName, bootstrapConfFileName, SUPPORTED_MINIMUM_VERSION)) { + throw new UnsupportedOperationException('File Manager Tool only supports NiFi versions 1.0.0 or higher.') } - install(installFileName, nifiInstallDirName, StringUtils.isEmpty(nifiCurrentDirName)? null : Paths.get(nifiCurrentDirName).toFile().getCanonicalPath(), bootstrapConfFileName, overwriteConfigs) + install(installFileName, nifiInstallDirName, !nifiCurrentDirName ? null : Paths.get(nifiCurrentDirName).toFile().getCanonicalPath(), bootstrapConfFileName, overwriteConfigs) } void parseBackup(final CommandLine commandLine){ if(!commandLine.hasOption(BACKUP_DIR)){ - throw new ParseException("Missing -b option") + throw new ParseException('Missing -b option') } else if(!commandLine.hasOption(NIFI_CURRENT_DIR)){ - throw new ParseException("Missing -c option") + throw new ParseException('Missing -c option') } final String backupDirName = commandLine.getOptionValue(BACKUP_DIR) final String nifiCurrentDirName = commandLine.getOptionValue(NIFI_CURRENT_DIR) if (Files.notExists(Paths.get(nifiCurrentDirName))) { - throw new ParseException("Current NiFi installation link does not exist: " + nifiCurrentDirName) + throw new ParseException('Current NiFi installation link does not exist: ' + nifiCurrentDirName) } - final String bootstrapConfFileName = commandLine.hasOption(BOOTSTRAP_CONF) ? commandLine.getOptionValue(BOOTSTRAP_CONF) : Paths.get(nifiCurrentDirName,"conf","bootstrap.conf").toString() + final String bootstrapConfFileName = commandLine.hasOption(BOOTSTRAP_CONF) ? commandLine.getOptionValue(BOOTSTRAP_CONF) : Paths.get(nifiCurrentDirName,'conf','bootstrap.conf').toString() if(supportedNiFiMinimumVersion(nifiCurrentDirName, bootstrapConfFileName, SUPPORTED_MINIMUM_VERSION)) { backup(backupDirName, Paths.get(nifiCurrentDirName).toFile().getCanonicalPath(),bootstrapConfFileName) }else{ - throw new UnsupportedOperationException("File Manager Tool only supports NiFi versions 1.0.0 or higher.") + throw new UnsupportedOperationException('File Manager Tool only supports NiFi versions 1.0.0 or higher.') } } @@ -524,11 +524,11 @@ public class FileManagerTool extends AbstractAdminTool{ } if(!commandLine.hasOption(BACKUP_DIR)) { - throw new ParseException("Missing -b option") + throw new ParseException('Missing -b option') }else if(!commandLine.hasOption(NIFI_ROLLBACK_DIR)){ - throw new ParseException("Missing -r option") + throw new ParseException('Missing -r option') }else if (!commandLine.hasOption(NIFI_CURRENT_DIR) && moveRepositories){ - throw new ParseException("Missing -c option: Moving repositories requires current nifi directory") + throw new ParseException('Missing -c option: Moving repositories requires current nifi directory') } final String backupDirName = commandLine.getOptionValue(BACKUP_DIR) @@ -536,19 +536,19 @@ public class FileManagerTool extends AbstractAdminTool{ final String nifiCurrentDirName = commandLine.getOptionValue(NIFI_CURRENT_DIR) if (Files.notExists(Paths.get(backupDirName)) || !Files.isDirectory(Paths.get(backupDirName))) { - throw new ParseException("Missing or invalid backup directory: " + backupDirName) + throw new ParseException('Missing or invalid backup directory: ' + backupDirName) } - if (!StringUtils.isEmpty(nifiCurrentDirName) && Files.notExists(Paths.get(nifiCurrentDirName))) { - throw new ParseException("Current NiFi installation path does not exist: " + nifiCurrentDirName) + if (nifiCurrentDirName && Files.notExists(Paths.get(nifiCurrentDirName))) { + throw new ParseException('Current NiFi installation path does not exist: ' + nifiCurrentDirName) } - if(!supportedNiFiMinimumVersion(backupDirName, Paths.get(backupDirName,"bootstrap_files","bootstrap.conf").toString(), SUPPORTED_MINIMUM_VERSION)) { - throw new UnsupportedOperationException("File Manager Tool only supports NiFi versions 1.0.0 or higher.") + if(!supportedNiFiMinimumVersion(backupDirName, Paths.get(backupDirName,'bootstrap_files','bootstrap.conf').toString(), SUPPORTED_MINIMUM_VERSION)) { + throw new UnsupportedOperationException('File Manager Tool only supports NiFi versions 1.0.0 or higher.') } - final String bootstrapConfFileName = commandLine.hasOption(BOOTSTRAP_CONF) ? commandLine.getOptionValue(BOOTSTRAP_CONF) : Paths.get(nifiRollbackDirName,"conf","bootstrap.conf").toString() - restore(backupDirName, nifiRollbackDirName, StringUtils.isEmpty(nifiCurrentDirName)? null : Paths.get(nifiCurrentDirName).toFile().getCanonicalPath(), bootstrapConfFileName) + final String bootstrapConfFileName = commandLine.hasOption(BOOTSTRAP_CONF) ? commandLine.getOptionValue(BOOTSTRAP_CONF) : Paths.get(nifiRollbackDirName,'conf','bootstrap.conf').toString() + restore(backupDirName, nifiRollbackDirName, !nifiCurrentDirName ? null : Paths.get(nifiCurrentDirName).toFile().getCanonicalPath(), bootstrapConfFileName) } @@ -566,18 +566,18 @@ public class FileManagerTool extends AbstractAdminTool{ String operation = commandLine.getOptionValue(OPERATION).toLowerCase() - if(operation.equals("install")){ + if(operation.equals('install')){ parseInstall(commandLine) - }else if(operation.equals("backup")){ + }else if(operation.equals('backup')){ parseBackup(commandLine) - }else if(operation.equals("restore")){ + }else if(operation.equals('restore')){ parseRestore(commandLine) }else{ - throw new ParseException("Invalid operation value:" + operation) + throw new ParseException('Invalid operation value:' + operation) } }else{ - throw new ParseException("Missing -o option") + throw new ParseException('Missing -o option') } } @@ -587,8 +587,8 @@ public class FileManagerTool extends AbstractAdminTool{ try { tool.parse(args) - } catch (ParseException | IllegalArgumentException e) { - tool.printUsage(e.getLocalizedMessage()); + } catch (Exception e) { + tool.printUsage(e.getLocalizedMessage()) System.exit(1) } diff --git a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/nodemanager/NodeManagerTool.groovy b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/nodemanager/NodeManagerTool.groovy index 10e8975ed9e0..3290fc345c75 100644 --- a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/nodemanager/NodeManagerTool.groovy +++ b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/nodemanager/NodeManagerTool.groovy @@ -73,7 +73,7 @@ public class NodeManagerTool extends AbstractAdminTool { @Override protected Logger getLogger() { - LoggerFactory.getLogger(NodeManagerTool.class) + LoggerFactory.getLogger(NodeManagerTool) } protected Options getOptions(){ @@ -260,7 +260,7 @@ public class NodeManagerTool extends AbstractAdminTool { if(commandLine.hasOption(BOOTSTRAP_CONF) && commandLine.hasOption(NIFI_INSTALL_DIR) && commandLine.hasOption(OPERATION)) { if(commandLine.hasOption(VERBOSE_ARG)){ - this.isVerbose = true; + this.isVerbose = true } final String bootstrapConfFileName = commandLine.getOptionValue(BOOTSTRAP_CONF) @@ -345,8 +345,8 @@ public class NodeManagerTool extends AbstractAdminTool { try{ tool.parse(clientFactory,args) - } catch (ParseException | RuntimeException e ) { - tool.printUsage(e.getLocalizedMessage()); + } catch (Exception e ) { + tool.printUsage(e.getLocalizedMessage()) System.exit(1) } diff --git a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/notify/NotificationTool.groovy b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/notify/NotificationTool.groovy index a1f753110c4f..0405dbf920be 100644 --- a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/notify/NotificationTool.groovy +++ b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/notify/NotificationTool.groovy @@ -66,7 +66,7 @@ public class NotificationTool extends AbstractAdminTool { @Override protected Logger getLogger() { - LoggerFactory.getLogger(NotificationTool.class) + LoggerFactory.getLogger(NotificationTool) } protected Options getOptions(){ @@ -140,7 +140,7 @@ public class NotificationTool extends AbstractAdminTool { if(commandLine.hasOption(BOOTSTRAP_CONF) && commandLine.hasOption(NOTIFICATION_MESSAGE) && commandLine.hasOption(NIFI_INSTALL_DIR)) { if(commandLine.hasOption(VERBOSE_ARG)){ - this.isVerbose = true; + this.isVerbose = true } final String bootstrapConfFileName = commandLine.getOptionValue(BOOTSTRAP_CONF) @@ -186,8 +186,8 @@ public class NotificationTool extends AbstractAdminTool { try{ tool.parse(clientFactory,args) - } catch (ParseException | UnsupportedOperationException | RuntimeException e) { - tool.printUsage(e.message); + } catch (Exception e) { + tool.printUsage(e.message) System.exit(1) } diff --git a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/util/AdminUtil.groovy b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/util/AdminUtil.groovy index bcebea1ff010..f69f2188d02a 100644 --- a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/util/AdminUtil.groovy +++ b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/util/AdminUtil.groovy @@ -65,7 +65,7 @@ class AdminUtil { if(StringUtils.isEmpty(nifiVersion)){ nifiVersion = getNiFiVersionFromNar(nifiLibDir) } - return nifiVersion.replace("-SNAPSHOT","") + nifiVersion.replace("-SNAPSHOT","") } @@ -73,12 +73,12 @@ class AdminUtil { Properties bootstrapProperties = new Properties() File bootstrapConf = bootstrapConfFileName.toFile() bootstrapProperties.load(new FileInputStream(bootstrapConf)) - return bootstrapProperties + bootstrapProperties } public static String getRelativeDirectory(String directory, String rootDirectory) { if (directory.startsWith("./")) { - final String directoryUpdated = SystemUtils.IS_OS_WINDOWS ? File.separator + directory.substring(2,directory.length()) : directory.substring(1,directory.length()) + final String directoryUpdated = SystemUtils.IS_OS_WINDOWS ? File.separator + directory[2..-1] : directory[1..-1] rootDirectory + directoryUpdated } else { directory @@ -95,7 +95,7 @@ class AdminUtil { Version minVersion = new Version(supportedMinimumVersion,".") Version.VERSION_COMPARATOR.compare(version,minVersion) >= 0 }else{ - return false + false } } @@ -103,7 +103,7 @@ class AdminUtil { Version version = new Version(incomingVersion,incomingVersion[1]) Version supportedMinimum = new Version(minimumVersion,minimumVersion[1]) Version supportedMaximum = new Version(maximumVersion,maximumVersion[1]) - return Version.VERSION_COMPARATOR.compare(version,supportedMinimum) >= 0 && Version.VERSION_COMPARATOR.compare(version,supportedMaximum) <= 0 + Version.VERSION_COMPARATOR.compare(version,supportedMinimum) >= 0 && Version.VERSION_COMPARATOR.compare(version,supportedMaximum) <= 0 } From ad2c8cb570590bbb2a08371a77c7f5c2915f16a7 Mon Sep 17 00:00:00 2001 From: "Yolanda M. Davis" Date: Wed, 7 Jun 2017 16:06:29 -0400 Subject: [PATCH 3/4] NIFI-3696 - added some info around repo upgrades and potential impact --- nifi-docs/src/main/asciidoc/administration-guide.adoc | 4 +++- .../nifi/toolkit/admin/filemanager/FileManagerTool.groovy | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/nifi-docs/src/main/asciidoc/administration-guide.adoc b/nifi-docs/src/main/asciidoc/administration-guide.adoc index 84b1df712d5e..ed7dc82e729f 100644 --- a/nifi-docs/src/main/asciidoc/administration-guide.adoc +++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc @@ -1392,7 +1392,9 @@ folder of the current installation) to the new installation as well as migrate c Restore: The restore operation allows an existing installation to revert back to a previous installation. Using an existing backup directory (created from the backup operation) -the FileManager utility will restore libraries, scripts and documents as well as revert to previous configurations. +the FileManager utility will restore libraries, scripts and documents as well as revert to previous configurations. NOTE: If repositories were changed due to the installation +of a newer version of NiFi these may no longer be compatible during restore. In that scenario exclude the -m option to ensure new repositories will be created or, if repositories +live outside of the NiFi directory, remove them so they can be recreated on startup after restore. [[clustering]] diff --git a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/filemanager/FileManagerTool.groovy b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/filemanager/FileManagerTool.groovy index f71182f0f431..ecead1ad9eaa 100644 --- a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/filemanager/FileManagerTool.groovy +++ b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/filemanager/FileManagerTool.groovy @@ -147,7 +147,7 @@ public class FileManagerTool extends AbstractAdminTool{ void moveRepository(final String dirName, final String installDirName){ if(isVerbose){ - logger.info('Moving repositories from {} to {}:',dirName,installDirName) + logger.info('Moving repositories from {} to {}. Please note that repositories may be upgraded during install and become incompatible with a previous version. ',dirName,installDirName) } final String bootstrapConfFileName = dirName + File.separator + 'conf' + File.separator + 'bootstrap.conf' @@ -239,7 +239,7 @@ public class FileManagerTool extends AbstractAdminTool{ void backup(String backupNiFiDirName, String currentNiFiDirName, String bootstrapConfFileName){ if(isVerbose){ - logger.info('Creating backup in directory:' + backupNiFiDirName) + logger.info('Creating backup in directory {}. Please note that repositories are not included in backup operation.',backupNiFiDirName) } final File backupNiFiDir = new File(backupNiFiDirName) From 728a0d3465827f21d3cbe7c51f47140e81f54884 Mon Sep 17 00:00:00 2001 From: "Yolanda M. Davis" Date: Wed, 7 Jun 2017 16:08:58 -0400 Subject: [PATCH 4/4] NIFI-3696 - added some info around repo upgrades and potential impact --- nifi-docs/src/main/asciidoc/administration-guide.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nifi-docs/src/main/asciidoc/administration-guide.adoc b/nifi-docs/src/main/asciidoc/administration-guide.adoc index ed7dc82e729f..7062689dbf26 100644 --- a/nifi-docs/src/main/asciidoc/administration-guide.adoc +++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc @@ -1392,8 +1392,8 @@ folder of the current installation) to the new installation as well as migrate c Restore: The restore operation allows an existing installation to revert back to a previous installation. Using an existing backup directory (created from the backup operation) -the FileManager utility will restore libraries, scripts and documents as well as revert to previous configurations. NOTE: If repositories were changed due to the installation -of a newer version of NiFi these may no longer be compatible during restore. In that scenario exclude the -m option to ensure new repositories will be created or, if repositories +the FileManager utility will restore libraries, scripts and documents as well as revert to previous configurations. *If repositories were changed due to the installation +of a newer version of NiFi these may no longer be compatible during restore.* In that scenario exclude the -m option to ensure new repositories will be created or, if repositories live outside of the NiFi directory, remove them so they can be recreated on startup after restore.