From c0f0462e8bc829d5e4ac2415a9fa77d3f5d6bdc3 Mon Sep 17 00:00:00 2001 From: "Yolanda M. Davis" Date: Tue, 7 Feb 2017 10:28:15 -0500 Subject: [PATCH] NIFI-3695 - created the nifi admin toolkit which includes shell scripts and classes to support notification and basic node management in standalone and clustered nifi. This closes #1669. Signed-off-by: Andy LoPresto --- .../nifi/web/api/entity/BulletinEntity.java | 2 + .../nifi/authorization/FileAuthorizer.java | 4 + .../nifi/cluster/manager/BulletinMerger.java | 12 +- .../apache/nifi/web/NiFiServiceFacade.java | 11 + .../nifi/web/StandardNiFiServiceFacade.java | 7 + .../nifi/web/api/ControllerResource.java | 68 +++ .../web/StandardNiFiServiceFacadeSpec.groovy | 56 ++- nifi-toolkit/nifi-toolkit-admin/pom.xml | 186 ++++++++ .../toolkit/admin/AbstractAdminTool.groovy | 110 +++++ .../toolkit/admin/client/ClientFactory.groovy | 27 ++ .../admin/client/NiFiClientFactory.groovy | 172 ++++++++ .../admin/client/NiFiClientUtil.groovy | 144 ++++++ .../admin/nodemanager/NodeManagerTool.groovy | 289 ++++++++++++ .../admin/notify/NotificationTool.groovy | 181 ++++++++ .../nifi/toolkit/admin/util/AdminUtil.groovy | 69 +++ .../nifi/toolkit/admin/util/Version.groovy | 82 ++++ .../admin/client/NiFiClientFactorySpec.groovy | 247 +++++++++++ .../admin/client/NiFiClientUtilSpec.groovy | 109 +++++ .../nodemanager/NodeManagerToolSpec.groovy | 414 ++++++++++++++++++ .../admin/notify/NotificationToolSpec.groovy | 171 ++++++++ .../toolkit/admin/util/AdminUtilSpec.groovy | 54 +++ .../src/test/resources/conf/bootstrap.conf | 32 ++ .../conf/login-identity-providers.xml | 112 +++++ .../src/test/resources/conf/nifi.properties | 28 ++ .../resources/external/conf/bootstrap.conf | 32 ++ .../conf/login-identity-providers.xml | 112 +++++ .../resources/external/conf/nifi.properties | 28 ++ .../test/resources/filemanager/bootstrap.conf | 32 ++ .../src/test/resources/filemanager/myid | 1 + .../filemanager/nifi-test-archive.tar.gz | Bin 0 -> 27167 bytes .../filemanager/nifi-test-archive.zip | Bin 0 -> 32415 bytes .../resources/filemanager/nifi.properties | 32 ++ .../lib/nifi-framework-nar-1.2.0.nar | Bin 0 -> 1385 bytes .../resources/no_rules/conf/bootstrap.conf | 21 + .../conf/login-identity-providers.xml | 112 +++++ .../resources/no_rules/conf/nifi.properties | 29 ++ .../test/resources/notify/conf/bootstrap.conf | 74 ++++ .../notify/conf/nifi-secured.properties | 107 +++++ .../resources/notify/conf/nifi.properties | 204 +++++++++ .../src/test/resources/overlay.properties | 41 ++ .../resources/upgrade/conf/bootstrap.conf | 21 + .../upgrade/conf/login-identity-providers.xml | 112 +++++ .../resources/upgrade/conf/nifi.properties | 28 ++ .../upgrade/lib/nifi-framework-nar-1.2.0.nar | Bin 0 -> 1385 bytes nifi-toolkit/nifi-toolkit-assembly/pom.xml | 4 + .../src/main/resources/bin/node-manager.bat | 39 ++ .../src/main/resources/bin/node-manager.sh | 119 +++++ .../src/main/resources/bin/notify.bat | 39 ++ .../src/main/resources/bin/notify.sh | 120 +++++ nifi-toolkit/pom.xml | 4 + pom.xml | 5 + 51 files changed, 3900 insertions(+), 3 deletions(-) create mode 100644 nifi-toolkit/nifi-toolkit-admin/pom.xml create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/AbstractAdminTool.groovy create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/client/ClientFactory.groovy create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/client/NiFiClientFactory.groovy create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/client/NiFiClientUtil.groovy create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/nodemanager/NodeManagerTool.groovy create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/notify/NotificationTool.groovy create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/util/AdminUtil.groovy create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/util/Version.groovy create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/groovy/org/apache/nifi/toolkit/admin/client/NiFiClientFactorySpec.groovy create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/groovy/org/apache/nifi/toolkit/admin/client/NiFiClientUtilSpec.groovy create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/groovy/org/apache/nifi/toolkit/admin/nodemanager/NodeManagerToolSpec.groovy create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/groovy/org/apache/nifi/toolkit/admin/notify/NotificationToolSpec.groovy create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/groovy/org/apache/nifi/toolkit/admin/util/AdminUtilSpec.groovy create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/conf/bootstrap.conf create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/conf/login-identity-providers.xml create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/conf/nifi.properties create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/external/conf/bootstrap.conf create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/external/conf/login-identity-providers.xml create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/external/conf/nifi.properties create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/filemanager/bootstrap.conf create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/filemanager/myid create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/filemanager/nifi-test-archive.tar.gz create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/filemanager/nifi-test-archive.zip create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/filemanager/nifi.properties create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/lib/nifi-framework-nar-1.2.0.nar create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/no_rules/conf/bootstrap.conf create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/no_rules/conf/login-identity-providers.xml create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/no_rules/conf/nifi.properties create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/notify/conf/bootstrap.conf create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/notify/conf/nifi-secured.properties create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/notify/conf/nifi.properties create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/overlay.properties create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/upgrade/conf/bootstrap.conf create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/upgrade/conf/login-identity-providers.xml create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/upgrade/conf/nifi.properties create mode 100644 nifi-toolkit/nifi-toolkit-admin/src/test/resources/upgrade/lib/nifi-framework-nar-1.2.0.nar create mode 100644 nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/node-manager.bat create mode 100644 nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/node-manager.sh create mode 100644 nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/notify.bat create mode 100644 nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/notify.sh diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/BulletinEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/BulletinEntity.java index 9e93a240c8..83f45d1210 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/BulletinEntity.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/BulletinEntity.java @@ -21,12 +21,14 @@ import org.apache.nifi.web.api.dto.BulletinDTO; import org.apache.nifi.web.api.dto.ReadablePermission; import org.apache.nifi.web.api.dto.util.TimeAdapter; +import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; import java.util.Date; /** * A serialized representation of this class can be placed in the entity body of a request or response to or from the API. This particular entity holds a reference to a BulletinDTO. */ +@XmlRootElement(name = "bulletinEntity") public class BulletinEntity extends Entity implements ReadablePermission { private Long id; diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-file-authorizer/src/main/java/org/apache/nifi/authorization/FileAuthorizer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-file-authorizer/src/main/java/org/apache/nifi/authorization/FileAuthorizer.java index 9a310a2fcc..c7440e2696 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-file-authorizer/src/main/java/org/apache/nifi/authorization/FileAuthorizer.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-file-authorizer/src/main/java/org/apache/nifi/authorization/FileAuthorizer.java @@ -361,6 +361,10 @@ public class FileAuthorizer extends AbstractPolicyBasedAuthorizer { // grant access to the proxy resource addAccessPolicy(authorizations, ResourceType.Proxy.getValue(), jaxbNodeUser.getIdentifier(), WRITE_CODE); + //grant access to controller resource + addAccessPolicy(authorizations, ResourceType.Controller.getValue(), jaxbNodeUser.getIdentifier(), READ_CODE); + addAccessPolicy(authorizations, ResourceType.Controller.getValue(), jaxbNodeUser.getIdentifier(), WRITE_CODE); + // grant the user read/write access data of the root group if (rootGroupId != null) { addAccessPolicy(authorizations, ResourceType.Data.getValue() + ResourceType.ProcessGroup.getValue() + "/" + rootGroupId, jaxbNodeUser.getIdentifier(), READ_CODE); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/BulletinMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/BulletinMerger.java index 79b1447b8b..952edabbc7 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/BulletinMerger.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/BulletinMerger.java @@ -24,6 +24,9 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; + +import com.google.common.collect.Lists; public final class BulletinMerger { @@ -71,7 +74,12 @@ public final class BulletinMerger { } } - Collections.sort(bulletinEntities, (BulletinEntity o1, BulletinEntity o2) -> { + final List entities = Lists.newArrayList(); + + final Map> groupingEntities = bulletinEntities.stream().collect(Collectors.groupingBy(b -> b.getBulletin().getMessage())); + groupingEntities.values().stream().map(e -> e.get(0)).forEach(entities::add); + + Collections.sort(entities, (BulletinEntity o1, BulletinEntity o2) -> { final int timeComparison = o1.getTimestamp().compareTo(o2.getTimestamp()); if (timeComparison != 0) { return timeComparison; @@ -80,6 +88,6 @@ public final class BulletinMerger { return o1.getNodeAddress().compareTo(o2.getNodeAddress()); }); - return bulletinEntities; + return entities; } } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java index 039cbf8c8f..6f9ea9877a 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java @@ -25,6 +25,7 @@ import org.apache.nifi.controller.service.ControllerServiceState; import org.apache.nifi.groups.ProcessGroup; import org.apache.nifi.web.api.dto.AccessPolicyDTO; import org.apache.nifi.web.api.dto.BulletinBoardDTO; +import org.apache.nifi.web.api.dto.BulletinDTO; import org.apache.nifi.web.api.dto.BulletinQueryDTO; import org.apache.nifi.web.api.dto.ClusterDTO; import org.apache.nifi.web.api.dto.ComponentHistoryDTO; @@ -66,6 +67,7 @@ import org.apache.nifi.web.api.dto.search.SearchResultsDTO; import org.apache.nifi.web.api.dto.status.ControllerStatusDTO; import org.apache.nifi.web.api.entity.AccessPolicyEntity; import org.apache.nifi.web.api.entity.ActionEntity; +import org.apache.nifi.web.api.entity.BulletinEntity; import org.apache.nifi.web.api.entity.ConnectionEntity; import org.apache.nifi.web.api.entity.ConnectionStatusEntity; import org.apache.nifi.web.api.entity.ControllerBulletinsEntity; @@ -1049,6 +1051,15 @@ public interface NiFiServiceFacade { */ RemoteProcessGroupEntity deleteRemoteProcessGroup(Revision revision, String remoteProcessGroupId); + + /** + * Create a system bulletin + * + * @param bulletinDTO bulletin to send to users + * @param canRead allow users to read bulletin + */ + BulletinEntity createBulletin(final BulletinDTO bulletinDTO, final Boolean canRead); + // ---------------------------------------- // Funnel methods // ---------------------------------------- diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java index 4179745f44..b9b208ef53 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java @@ -78,6 +78,7 @@ import org.apache.nifi.controller.service.ControllerServiceReference; import org.apache.nifi.controller.service.ControllerServiceState; import org.apache.nifi.controller.status.ProcessGroupStatus; import org.apache.nifi.diagnostics.SystemDiagnostics; +import org.apache.nifi.events.BulletinFactory; import org.apache.nifi.groups.ProcessGroup; import org.apache.nifi.groups.ProcessGroupCounts; import org.apache.nifi.groups.RemoteProcessGroup; @@ -1380,6 +1381,12 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade { }); } + @Override + public BulletinEntity createBulletin(final BulletinDTO bulletinDTO, final Boolean canRead){ + final Bulletin bulletin = BulletinFactory.createBulletin(bulletinDTO.getCategory(),bulletinDTO.getLevel(),bulletinDTO.getMessage()); + bulletinRepository.addBulletin(bulletin); + return entityFactory.createBulletinEntity(dtoFactory.createBulletinDto(bulletin),canRead); + } @Override public FunnelEntity createFunnel(final Revision revision, final String groupId, final FunnelDTO funnelDTO) { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.java index 98400f2661..cb87ca2101 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.java @@ -40,10 +40,12 @@ import org.apache.nifi.controller.FlowController; import org.apache.nifi.web.IllegalClusterResourceRequestException; import org.apache.nifi.web.NiFiServiceFacade; import org.apache.nifi.web.Revision; +import org.apache.nifi.web.api.dto.BulletinDTO; import org.apache.nifi.web.api.dto.ClusterDTO; import org.apache.nifi.web.api.dto.ControllerServiceDTO; import org.apache.nifi.web.api.dto.NodeDTO; import org.apache.nifi.web.api.dto.ReportingTaskDTO; +import org.apache.nifi.web.api.entity.BulletinEntity; import org.apache.nifi.web.api.entity.ClusterEntity; import org.apache.nifi.web.api.entity.ControllerConfigurationEntity; import org.apache.nifi.web.api.entity.ControllerServiceEntity; @@ -261,6 +263,7 @@ public class ControllerResource extends ApplicationResource { @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.") } ) + public Response createReportingTask( @Context final HttpServletRequest httpServletRequest, @ApiParam( @@ -330,6 +333,71 @@ public class ControllerResource extends ApplicationResource { ); } + /** + * Creates a Bulletin. + * + * @param httpServletRequest request + * @param requestBulletinEntity A bulletinEntity. + * @return A bulletinEntity. + */ + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Path("bulletin") + @ApiOperation( + value = "Creates a new bulletin", + response = BulletinEntity.class, + authorizations = { + @Authorization(value = "Write - /controller", type = "") + } + ) + @ApiResponses( + value = { + @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."), + @ApiResponse(code = 401, message = "Client could not be authenticated."), + @ApiResponse(code = 403, message = "Client is not authorized to make this request."), + @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.") + } + ) + public Response createBulletin( + @Context final HttpServletRequest httpServletRequest, + @ApiParam( + value = "The reporting task configuration details.", + required = true + ) final BulletinEntity requestBulletinEntity) { + + if (requestBulletinEntity == null || requestBulletinEntity.getBulletin() == null) { + throw new IllegalArgumentException("Bulletin details must be specified."); + } + + final BulletinDTO requestBulletin = requestBulletinEntity.getBulletin(); + if (requestBulletin.getId() != null) { + throw new IllegalArgumentException("A bulletin ID cannot be specified."); + } + + if (StringUtils.isBlank(requestBulletin.getMessage())) { + throw new IllegalArgumentException("The bulletin message must be specified."); + } + + if (isReplicateRequest()) { + return replicate(HttpMethod.POST, requestBulletinEntity); + } + + return withWriteLock( + serviceFacade, + requestBulletinEntity, + lookup -> { + authorizeController(RequestAction.WRITE); + }, + null, + (bulletinEntity) -> { + final BulletinDTO bulletin = bulletinEntity.getBulletin(); + final BulletinEntity entity = serviceFacade.createBulletin(bulletin,true); + return generateOkResponse(entity).build(); + } + ); + } + // ------------------- // controller services // ------------------- diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/groovy/org/apache/nifi/web/StandardNiFiServiceFacadeSpec.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/groovy/org/apache/nifi/web/StandardNiFiServiceFacadeSpec.groovy index 677b25d57a..29ab83a0ca 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/groovy/org/apache/nifi/web/StandardNiFiServiceFacadeSpec.groovy +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/groovy/org/apache/nifi/web/StandardNiFiServiceFacadeSpec.groovy @@ -23,7 +23,11 @@ import org.apache.nifi.authorization.user.NiFiUser import org.apache.nifi.authorization.user.StandardNiFiUser import org.apache.nifi.authorization.user.NiFiUserDetails import org.apache.nifi.controller.service.ControllerServiceProvider +import org.apache.nifi.reporting.Bulletin +import org.apache.nifi.reporting.BulletinRepository +import org.apache.nifi.reporting.ComponentType import org.apache.nifi.web.api.dto.* +import org.apache.nifi.web.api.entity.BulletinEntity import org.apache.nifi.web.api.entity.UserEntity import org.apache.nifi.web.controller.ControllerFacade import org.apache.nifi.web.dao.AccessPolicyDAO @@ -36,7 +40,7 @@ import spock.lang.Ignore import spock.lang.Specification import spock.lang.Unroll -@Ignore + class StandardNiFiServiceFacadeSpec extends Specification { def setup() { @@ -49,6 +53,7 @@ class StandardNiFiServiceFacadeSpec extends Specification { SecurityContextHolder.getContext().setAuthentication(null); } + @Ignore @Unroll def "CreateUser: isAuthorized: #isAuthorized"() { given: @@ -87,6 +92,7 @@ class StandardNiFiServiceFacadeSpec extends Specification { createUserDTO() | null | ResourceFactory.usersResource | false | AuthorizationResult.denied() } + @Ignore @Unroll def "GetUser: isAuthorized: #isAuthorized"() { given: @@ -134,6 +140,7 @@ class StandardNiFiServiceFacadeSpec extends Specification { createUserDTO() | false | AuthorizationResult.denied() } + @Ignore @Unroll def "UpdateUser: isAuthorized: #isAuthorized, policy exists: #userExists"() { given: @@ -188,6 +195,7 @@ class StandardNiFiServiceFacadeSpec extends Specification { true | new Revision(1L, 'client1', 'root') | createUserDTO() | false | AuthorizationResult.denied() } + @Ignore @Unroll def "DeleteUser: isAuthorized: #isAuthorized, user exists: #userExists"() { given: @@ -239,6 +247,7 @@ class StandardNiFiServiceFacadeSpec extends Specification { false | null | createUserDTO() | false | AuthorizationResult.denied() } + @Ignore @Unroll def "CreateUserGroup: isAuthorized: #isAuthorized"() { given: @@ -307,6 +316,7 @@ class StandardNiFiServiceFacadeSpec extends Specification { createUserGroupDTO() | false | [(ResourceFactory.userGroupsResource): AuthorizationResult.denied(), (ResourceFactory.usersResource): AuthorizationResult.denied()] } + @Ignore @Unroll def "GetUserGroup: isAuthorized: #isAuthorized"() { given: @@ -363,6 +373,7 @@ class StandardNiFiServiceFacadeSpec extends Specification { new UserGroupDTO(id: '1', name: 'test group', users: [createUserEntity()]) | false | AuthorizationResult.denied() } + @Ignore @Unroll def "UpdateUserGroup: isAuthorized: #isAuthorized, userGroupExists exists: #userGroupExists"() { given: @@ -444,6 +455,7 @@ class StandardNiFiServiceFacadeSpec extends Specification { [(ResourceFactory.userGroupsResource): AuthorizationResult.denied(), (ResourceFactory.usersResource): AuthorizationResult.denied()] } + @Ignore @Unroll def "DeleteUserGroup: isAuthorized: #isAuthorized, userGroup exists: #userGroupExists"() { given: @@ -521,6 +533,7 @@ class StandardNiFiServiceFacadeSpec extends Specification { [(ResourceFactory.userGroupsResource): AuthorizationResult.denied(), (ResourceFactory.usersResource): AuthorizationResult.denied()] } + @Ignore @Unroll def "CreateAccessPolicy: #isAuthorized"() { given: @@ -589,6 +602,7 @@ class StandardNiFiServiceFacadeSpec extends Specification { new AccessPolicyDTO(id: '1', resource: ResourceFactory.flowResource.identifier, users: [createUserEntity()], canRead: true) | false | AuthorizationResult.denied() } + @Ignore @Unroll def "GetAccessPolicy: isAuthorized: #isAuthorized"() { given: @@ -654,6 +668,7 @@ class StandardNiFiServiceFacadeSpec extends Specification { new AccessPolicyDTO(id: '1', resource: ResourceFactory.flowResource.identifier, users: [createUserEntity()], canRead: true) | false | AuthorizationResult.denied() } + @Ignore @Unroll def "UpdateAccessPolicy: isAuthorized: #isAuthorized, policy exists: #hasPolicy"() { given: @@ -741,6 +756,7 @@ class StandardNiFiServiceFacadeSpec extends Specification { AuthorizationResult.denied() } + @Ignore @Unroll def "DeleteAccessPolicy: isAuthorized: #isAuthorized, hasPolicy: #hasPolicy"() { given: @@ -828,6 +844,44 @@ class StandardNiFiServiceFacadeSpec extends Specification { AuthorizationResult.denied() } + + def "CreateBulletin Successfully"() { + given: + + def entityFactory = new EntityFactory() + def dtoFactory = new DtoFactory() + dtoFactory.setEntityFactory entityFactory + def authorizableLookup = Mock AuthorizableLookup + def controllerFacade = Mock ControllerFacade + def niFiServiceFacade = new StandardNiFiServiceFacade() + def bulletinRepository = Mock BulletinRepository + niFiServiceFacade.setAuthorizableLookup authorizableLookup + niFiServiceFacade.setDtoFactory dtoFactory + niFiServiceFacade.setEntityFactory entityFactory + niFiServiceFacade.setControllerFacade controllerFacade + niFiServiceFacade.setBulletinRepository bulletinRepository + + def bulletinDto = new BulletinDTO() + bulletinDto.category = "SYSTEM" + bulletinDto.message = "test system message" + bulletinDto.level = "WARN" + def bulletinEntity + def retBulletinEntity = new BulletinEntity() + retBulletinEntity.bulletin = bulletinDto + + when: + + bulletinEntity = niFiServiceFacade.createBulletin(bulletinDto,true) + + + then: + 1 * bulletinRepository.addBulletin(_ as Bulletin) + bulletinEntity + bulletinEntity.bulletin.message == bulletinDto.message + + + } + private UserGroupDTO createUserGroupDTO() { new UserGroupDTO(id: 'group-1', name: 'test group', users: [createUserEntity()] as Set) } diff --git a/nifi-toolkit/nifi-toolkit-admin/pom.xml b/nifi-toolkit/nifi-toolkit-admin/pom.xml new file mode 100644 index 0000000000..37500c653d --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/pom.xml @@ -0,0 +1,186 @@ + + + + + org.apache.nifi + nifi-toolkit + 1.2.0-SNAPSHOT + + + 4.0.0 + + nifi-toolkit-admin + + + + commons-cli + commons-cli + + + com.google.guava + guava + + + org.apache.nifi + nifi-toolkit-tls + + + com.sun.jersey + jersey-client + + + com.fasterxml.jackson.core + jackson-databind + + + org.apache.nifi + nifi-client-dto + ${client.version} + + + org.apache.nifi + nifi-properties + + + org.apache.nifi + nifi-properties-loader + + + ch.qos.logback + logback-classic + + + + + org.apache.nifi + nifi-security-utils + + + org.codehaus.jackson + jackson-mapper-asl + + + com.fasterxml.jackson.jaxrs + jackson-jaxrs-json-provider + RELEASE + + + com.sun.jersey + jersey-bundle + RELEASE + + + com.sun.jersey + jersey-json + RELEASE + + + org.apache.commons + commons-compress + + + + com.github.stefanbirkner + system-rules + 1.16.0 + test + + + org.spockframework + spock-core + test + + + cglib + cglib-nodep + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + compile + testCompile + + + groovy-eclipse-compiler + + + + + 1.8 + 1.8 + + + + org.codehaus.groovy + groovy-eclipse-compiler + 2.9.2-01 + + + org.codehaus.groovy + groovy-eclipse-batch + 2.4.3-01 + + + + + org.codehaus.mojo + build-helper-maven-plugin + 1.5 + + + add-source + generate-sources + + add-source + + + + src/main/groovy + + + + + add-test-source + generate-test-sources + + add-test-source + + + + src/test/groovy + + + + + + + org.apache.rat + apache-rat-plugin + + + src/test/resources/filemanager/myid + + + + + + \ No newline at end of file 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 new file mode 100644 index 0000000000..aed30274ca --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/AbstractAdminTool.groovy @@ -0,0 +1,110 @@ +/* + * 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 + +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 { + + protected static final String JAVA_HOME = "JAVA_HOME" + protected static final String NIFI_TOOLKIT_HOME = "NIFI_TOOLKIT_HOME" + protected static final String SEP = System.lineSeparator() + protected Options options + protected String header + protected String footer + protected Boolean isVerbose + protected Logger logger + + protected void setup(){ + options = getOptions() + footer = buildFooter() + logger = getLogger() + } + + protected String buildHeader(final String description ) { + "${SEP}${description}${SEP * 2}" + } + + protected String buildFooter() { + "${SEP}Java home: ${System.getenv(JAVA_HOME)}${SEP}NiFi Toolkit home: ${System.getenv(NIFI_TOOLKIT_HOME)}" + } + + public void printUsage(final String errorMessage) { + if (errorMessage) { + System.out.println(errorMessage) + System.out.println() + } + final HelpFormatter helpFormatter = new HelpFormatter() + helpFormatter.setWidth(160) + helpFormatter.printHelp(this.class.getCanonicalName(), this.header, options, footer, true) + } + + protected abstract Options getOptions() + + 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,".") + 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) + } + + +} diff --git a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/client/ClientFactory.groovy b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/client/ClientFactory.groovy new file mode 100644 index 0000000000..960ac6a40e --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/client/ClientFactory.groovy @@ -0,0 +1,27 @@ +/* + * 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.client + +import com.sun.jersey.api.client.Client +import org.apache.nifi.util.NiFiProperties + +interface ClientFactory { + + Client getClient(NiFiProperties niFiProperties, String nifiInstallDir) throws Exception + +} \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/client/NiFiClientFactory.groovy b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/client/NiFiClientFactory.groovy new file mode 100644 index 0000000000..5c0333af85 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/client/NiFiClientFactory.groovy @@ -0,0 +1,172 @@ +/* + * 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.client + +import com.sun.jersey.api.client.Client +import com.sun.jersey.api.client.config.ClientConfig +import com.sun.jersey.api.client.config.DefaultClientConfig +import com.sun.jersey.client.urlconnection.HTTPSProperties +import org.apache.commons.lang3.StringUtils +import org.apache.nifi.security.util.CertificateUtils +import org.apache.nifi.util.NiFiProperties +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import javax.naming.ldap.LdapName +import javax.naming.ldap.Rdn +import javax.net.ssl.HostnameVerifier +import javax.net.ssl.KeyManagerFactory +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLPeerUnverifiedException +import javax.net.ssl.SSLSession +import javax.net.ssl.TrustManagerFactory +import java.security.KeyManagementException +import java.security.KeyStore +import java.security.KeyStoreException +import java.security.NoSuchAlgorithmException +import java.security.SecureRandom +import java.security.UnrecoverableKeyException +import java.security.cert.Certificate +import java.security.cert.CertificateException +import java.security.cert.CertificateParsingException +import java.security.cert.X509Certificate + +class NiFiClientFactory implements ClientFactory{ + + private static final Logger logger = LoggerFactory.getLogger(NiFiClientFactory.class) + static enum NiFiAuthType{ NONE, SSL } + + public Client getClient(NiFiProperties niFiProperties, String nifiInstallDir) throws Exception { + + final String authTypeStr = StringUtils.isEmpty(niFiProperties.getProperty(NiFiProperties.WEB_HTTPS_HOST)) && StringUtils.isEmpty(niFiProperties.getProperty(NiFiProperties.WEB_HTTPS_PORT)) ? NiFiAuthType.NONE : NiFiAuthType.SSL; + final NiFiAuthType authType = NiFiAuthType.valueOf(authTypeStr); + + SSLContext sslContext = null; + + if (NiFiAuthType.SSL.equals(authType)) { + String keystore = niFiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE); + final String keystoreType = niFiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE_TYPE); + final String keystorePassword = niFiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE_PASSWD); + String truststore = niFiProperties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE); + final String truststoreType = niFiProperties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_TYPE); + final String truststorePassword = niFiProperties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD); + + if(keystore.startsWith("./")){ + keystore = keystore.replace("./",nifiInstallDir+"/") + } + if(truststore.startsWith("./")){ + truststore = truststore.replace("./",nifiInstallDir+"/") + } + + sslContext = createSslContext( + keystore.trim(), + keystorePassword.trim().toCharArray(), + keystoreType.trim(), + truststore.trim(), + truststorePassword.trim().toCharArray(), + truststoreType.trim(), + "TLS"); + } + + final ClientConfig config = new DefaultClientConfig(); + + if (sslContext != null) { + config.getProperties().put(HTTPSProperties.PROPERTY_HTTPS_PROPERTIES,new HTTPSProperties(new NiFiHostnameVerifier(), sslContext)) + } + + return Client.create(config) + + } + + + static SSLContext createSslContext( + final String keystore, final char[] keystorePasswd, final String keystoreType, + final String truststore, final char[] truststorePasswd, final String truststoreType, + final String protocol) + throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, + UnrecoverableKeyException, KeyManagementException { + + // prepare the keystore + final KeyStore keyStore = KeyStore.getInstance(keystoreType); + final InputStream keyStoreStream = new FileInputStream(keystore) + keyStore.load(keyStoreStream, keystorePasswd); + + + final KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keyStore, keystorePasswd); + + // prepare the truststore + final KeyStore trustStore = KeyStore.getInstance(truststoreType); + final InputStream trustStoreStream = new FileInputStream(truststore) + trustStore.load(trustStoreStream, truststorePasswd); + + final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(trustStore); + + // initialize the ssl context + final SSLContext sslContext = SSLContext.getInstance(protocol); + sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom()); + return sslContext; + } + + static class NiFiHostnameVerifier implements HostnameVerifier { + + @Override + public boolean verify(final String hostname, final SSLSession ssls) { + + if (ssls.getPeerCertificates() != null && ssls.getPeerCertificates().length > 0) { + + try { + final Certificate peerCertificate = ssls.getPeerCertificates()[0] + final X509Certificate x509Cert = CertificateUtils.convertAbstractX509Certificate(peerCertificate) + final String dn = x509Cert.getSubjectDN().getName().trim() + + final LdapName ln = new LdapName(dn) + final boolean match = ln.getRdns().any { Rdn rdn -> rdn.getType().equalsIgnoreCase("CN") && rdn.getValue().toString().equalsIgnoreCase(hostname)} + return match || getSubjectAlternativeNames(x509Cert).any { String san -> san.equalsIgnoreCase(hostname) } + + } catch (final SSLPeerUnverifiedException | CertificateParsingException ex ) { + logger.warn("Hostname Verification encountered exception verifying hostname due to: " + ex, ex); + } + + }else{ + logger.warn("Peer certificates not found on ssl session "); + } + + return false + } + + private List getSubjectAlternativeNames(final X509Certificate certificate) throws CertificateParsingException { + final Collection> altNames = certificate.getSubjectAlternativeNames() + + if (altNames == null) { + return new ArrayList<>() + } + + final List result = new ArrayList<>() + for (final List generalName : altNames) { + final Object value = generalName.get(1) + if (value instanceof String) { + result.add(((String) value).toLowerCase()) + } + } + + return result + } + } + +} diff --git a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/client/NiFiClientUtil.groovy b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/client/NiFiClientUtil.groovy new file mode 100644 index 0000000000..d4e5ff60e4 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/client/NiFiClientUtil.groovy @@ -0,0 +1,144 @@ +/* + * 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.client + +import com.fasterxml.jackson.databind.ObjectMapper +import com.google.common.collect.Lists +import com.sun.jersey.api.client.Client +import com.sun.jersey.api.client.ClientResponse +import com.sun.jersey.api.client.WebResource +import org.apache.nifi.util.NiFiProperties +import org.apache.nifi.util.StringUtils +import org.apache.nifi.web.api.dto.NodeDTO +import org.apache.nifi.web.api.dto.util.DateTimeAdapter +import org.apache.nifi.web.api.entity.ClusterEntity +import org.apache.nifi.web.api.entity.NodeEntity +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import java.text.SimpleDateFormat + +public class NiFiClientUtil { + + private static final Logger logger = LoggerFactory.getLogger(NiFiClientUtil.class) + private final static String GET_CLUSTER_ENDPOINT ="/nifi-api/controller/cluster" + + public static Boolean isCluster(final NiFiProperties niFiProperties){ + String clusterNode = niFiProperties.getProperty(NiFiProperties.CLUSTER_IS_NODE) + return Boolean.valueOf(clusterNode) + } + + public static String getUrl(NiFiProperties niFiProperties, String endpoint){ + + final StringBuilder urlBuilder = new StringBuilder(); + + if(!StringUtils.isEmpty(niFiProperties.getProperty(NiFiProperties.WEB_HTTPS_PORT))){ + urlBuilder.append("https://") + urlBuilder.append(StringUtils.isEmpty(niFiProperties.getProperty(NiFiProperties.WEB_HTTPS_HOST)) ? "localhost": niFiProperties.getProperty(NiFiProperties.WEB_HTTPS_HOST)) + urlBuilder.append(":") + urlBuilder.append(StringUtils.isEmpty(niFiProperties.getProperty(NiFiProperties.WEB_HTTPS_PORT)) ? "8081" : niFiProperties.getProperty(NiFiProperties.WEB_HTTPS_PORT)) + }else{ + urlBuilder.append("http://") + urlBuilder.append(StringUtils.isEmpty(niFiProperties.getProperty(NiFiProperties.WEB_HTTP_HOST)) ? "localhost": niFiProperties.getProperty(NiFiProperties.WEB_HTTP_HOST)) + urlBuilder.append(":") + urlBuilder.append(StringUtils.isEmpty(niFiProperties.getProperty(NiFiProperties.WEB_HTTPS_PORT)) ? "8080": niFiProperties.getProperty(NiFiProperties.WEB_HTTPS_PORT)) + } + + if(!StringUtils.isEmpty(endpoint)) { + urlBuilder.append(endpoint) + } + + urlBuilder.toString() + } + + public static String getUrl(NiFiProperties niFiProperties, NodeDTO nodeDTO, String endpoint){ + + final StringBuilder urlBuilder = new StringBuilder(); + if(!StringUtils.isEmpty(niFiProperties.getProperty(NiFiProperties.WEB_HTTPS_PORT))){ + urlBuilder.append("https://") + + }else{ + urlBuilder.append("http://") + } + urlBuilder.append(nodeDTO.address) + urlBuilder.append(":") + urlBuilder.append(nodeDTO.apiPort) + + if(!StringUtils.isEmpty(endpoint)) { + urlBuilder.append(endpoint) + } + + urlBuilder.toString() + } + + public static ClusterEntity getCluster(final Client client, NiFiProperties niFiProperties, List activeUrls){ + + if(activeUrls.isEmpty()){ + final String url = getUrl(niFiProperties,null) + activeUrls.add(url) + } + + for(String activeUrl: activeUrls) { + + try { + + String url = activeUrl + GET_CLUSTER_ENDPOINT + final WebResource webResource = client.resource(url) + final ClientResponse response = webResource.type("application/json").get(ClientResponse.class) + + Integer status = response.getStatus() + + if (status != 200) { + if (status == 404) { + logger.warn("This node is not attached to a cluster. Please connect to a node that is attached to the cluster for information") + } else { + logger.warn("Failed with HTTP error code: {}, message: {}", status, response.getStatusInfo().getReasonPhrase()) + } + } else if (status == 200) { + return response.getEntity(ClusterEntity.class) + } + + }catch(Exception ex){ + logger.warn("Exception occurred during connection attempt: {}",ex.localizedMessage) + } + + } + + throw new RuntimeException("Unable to obtain cluster information") + + } + + public static List getActiveClusterUrls(final Client client, NiFiProperties niFiProperties){ + + final ClusterEntity clusterEntity = getCluster(client, niFiProperties, Lists.newArrayList()) + final List activeNodes = clusterEntity.cluster.nodes.findAll{ it.status == "CONNECTED" } + final List activeUrls = Lists.newArrayList() + + activeNodes.each { + activeUrls.add(getUrl(niFiProperties,it, null)) + } + activeUrls + } + + public static String convertToJson(NodeDTO nodeDTO){ + ObjectMapper om = new ObjectMapper() + om.setDateFormat(new SimpleDateFormat(DateTimeAdapter.DEFAULT_DATE_TIME_FORMAT)); + NodeEntity ne = new NodeEntity() + ne.setNode(nodeDTO) + return om.writeValueAsString(ne) + } +} 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 new file mode 100644 index 0000000000..3a1c4dfc03 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/nodemanager/NodeManagerTool.groovy @@ -0,0 +1,289 @@ +/* + * 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.nodemanager + +import com.sun.jersey.api.client.Client +import com.sun.jersey.api.client.ClientResponse +import com.sun.jersey.api.client.WebResource +import org.apache.nifi.toolkit.admin.AbstractAdminTool +import org.apache.nifi.toolkit.admin.client.NiFiClientUtil +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.nifi.properties.NiFiPropertiesLoader +import org.apache.nifi.toolkit.admin.client.ClientFactory +import org.apache.nifi.toolkit.admin.client.NiFiClientFactory +import org.apache.nifi.util.NiFiProperties +import org.apache.nifi.util.StringUtils +import org.apache.nifi.web.api.dto.NodeDTO +import org.apache.nifi.web.api.entity.ClusterEntity +import org.apache.nifi.web.api.entity.NodeEntity +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import java.nio.file.Paths + +public class NodeManagerTool extends AbstractAdminTool { + + private static final String DEFAULT_DESCRIPTION = "This tool is used to manage nodes within a cluster. Supported functionality will remove node from cluster. " + private static final String HELP_ARG = "help" + private static final String VERBOSE_ARG = "verbose" + private static final String BOOTSTRAP_CONF = "bootstrapConf" + private static final String NIFI_INSTALL_DIR = "nifiInstallDir" + private static final String CLUSTER_URLS = "clusterUrls" + private static final String REMOVE = "remove" + private static final String DISCONNECT = "disconnect" + private static final String CONNECT = "connect" + private static final String OPERATION = "operation" + private final static String NODE_ENDPOINT = "/nifi-api/controller/cluster/nodes" + private final static String SUPPORTED_MINIMUM_VERSION = "1.0.0" + static enum STATUS {DISCONNECTING,CONNECTING,CONNECTED} + + NodeManagerTool() { + header = buildHeader(DEFAULT_DESCRIPTION) + setup() + } + + NodeManagerTool(final String description){ + this.header = buildHeader(description) + setup() + } + + @Override + protected Logger getLogger() { + LoggerFactory.getLogger(NodeManagerTool.class) + } + + protected Options getOptions(){ + final Options options = new Options() + options.addOption(Option.builder("h").longOpt(HELP_ARG).desc("Print help info").build()) + options.addOption(Option.builder("v").longOpt(VERBOSE_ARG).desc("Set mode to verbose (default is false)").build()) + options.addOption(Option.builder("b").longOpt(BOOTSTRAP_CONF).hasArg().desc("Existing Bootstrap Configuration file").build()) + options.addOption(Option.builder("d").longOpt(NIFI_INSTALL_DIR).hasArg().desc("NiFi Installation Directory").build()) + options.addOption(Option.builder("o").longOpt(OPERATION).hasArg().desc("Operation to connect, disconnect or remove node from cluster").build()) + options.addOption(Option.builder("u").longOpt(CLUSTER_URLS).hasArg().desc("List of active urls for the cluster").build()) + options + } + + NodeDTO getCurrentNode(ClusterEntity clusterEntity, NiFiProperties niFiProperties){ + final List nodeDTOs = clusterEntity.cluster.nodes + final String nodeHost = StringUtils.isEmpty(niFiProperties.getProperty(NiFiProperties.CLUSTER_NODE_ADDRESS)) ? + "localhost":niFiProperties.getProperty(NiFiProperties.CLUSTER_NODE_ADDRESS) + return nodeDTOs.find{ it.address == nodeHost } + } + + NodeEntity updateNode(final String url, final Client client, final NodeDTO nodeDTO, final STATUS nodeStatus){ + final WebResource webResource = client.resource(url) + nodeDTO.status = nodeStatus + String json = NiFiClientUtil.convertToJson(nodeDTO) + + if(isVerbose){ + logger.info("Sending node info for update: " + json) + } + + final ClientResponse response = webResource.type("application/json").put(ClientResponse.class,json) + + if(response.getStatus() != 200){ + throw new RuntimeException("Failed with HTTP error code: " + response.getStatus()) + }else{ + response.getEntity(NodeEntity.class) + } + } + + void deleteNode(final String url, final Client client){ + final WebResource webResource = client.resource(url) + + if(isVerbose){ + logger.info("Attempting to delete node" ) + } + + final ClientResponse response = webResource.type("application/json").delete(ClientResponse.class) + + if(response.getStatus() != 200){ + throw new RuntimeException("Failed with HTTP error code: " + response.getStatus()) + } + } + + void disconnectNode(final Client client, NiFiProperties niFiProperties, List activeUrls){ + final ClusterEntity clusterEntity = NiFiClientUtil.getCluster(client, niFiProperties, activeUrls) + 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) + return + } catch (Exception ex){ + logger.warn("Could not connect to node on "+activeUrl+". Exception: "+ex.toString()) + } + } + throw new RuntimeException("Could not successfully complete request") + } + + void connectNode(final Client client, NiFiProperties niFiProperties,List activeUrls){ + final ClusterEntity clusterEntity = NiFiClientUtil.getCluster(client, niFiProperties, activeUrls) + 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) + return + } catch (Exception ex){ + logger.warn("Could not connect to node on "+activeUrl+". Exception: "+ex.toString()) + } + } + throw new RuntimeException("Could not successfully complete request") + } + + void removeNode(final Client client, NiFiProperties niFiProperties, List activeUrls){ + + final ClusterEntity clusterEntity = NiFiClientUtil.getCluster(client, niFiProperties, activeUrls) + NodeDTO currentNode = getCurrentNode(clusterEntity,niFiProperties) + + if(currentNode != null) { + + for (String activeUrl : activeUrls) { + + try { + + final String url = activeUrl + NODE_ENDPOINT + File.separator + currentNode.nodeId + + if(isVerbose){ + logger.info("Attempting to connect to cluster with url:" + url) + } + + if(currentNode.status == "CONNECTED") { + currentNode = updateNode(url, client, currentNode, STATUS.DISCONNECTING).node + } + + if(currentNode.status == "DISCONNECTED") { + deleteNode(url, client) + } + + if(isVerbose){ + logger.info("Node removed from cluster successfully.") + } + + 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") + } + + } + + void parse(final ClientFactory clientFactory, final String[] args) throws ParseException, UnsupportedOperationException, IllegalArgumentException { + + final CommandLine commandLine = new DefaultParser().parse(options,args) + + if (commandLine.hasOption(HELP_ARG)){ + printUsage(null) + }else{ + + if(commandLine.hasOption(BOOTSTRAP_CONF) && commandLine.hasOption(NIFI_INSTALL_DIR) && commandLine.hasOption(OPERATION)) { + + if(commandLine.hasOption(VERBOSE_ARG)){ + this.isVerbose = true; + } + + final String bootstrapConfFileName = commandLine.getOptionValue(BOOTSTRAP_CONF) + 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()) + String nifiPropertiesFileName = nifiConfDir + File.separator +"nifi.properties" + final String key = NiFiPropertiesLoader.extractKeyFromBootstrapFile(bootstrapConfFileName) + final NiFiProperties niFiProperties = NiFiPropertiesLoader.withKey(key).load(nifiPropertiesFileName) + + final String nifiInstallDir = commandLine.getOptionValue(NIFI_INSTALL_DIR) + + if(supportedNiFiMinimumVersion(nifiConfDir,nifiLibDir,SUPPORTED_MINIMUM_VERSION) && NiFiClientUtil.isCluster(niFiProperties)){ + + final Client client = clientFactory.getClient(niFiProperties,nifiInstallDir) + final String operation = commandLine.getOptionValue(OPERATION) + + if(isVerbose){ + logger.info("Starting {} request",operation) + } + + List activeUrls + + if(commandLine.hasOption(CLUSTER_URLS)){ + final String urlList = commandLine.getOptionValue(CLUSTER_URLS) + activeUrls = urlList.tokenize(',') + }else{ + activeUrls = NiFiClientUtil.getActiveClusterUrls(client,niFiProperties) + } + + if(isVerbose){ + logger.info("Using active urls {} for communication.",activeUrls) + } + + if(operation.toLowerCase().equals(REMOVE)){ + removeNode(client,niFiProperties,activeUrls) + } + else if(operation.toLowerCase().equals(DISCONNECT)){ + disconnectNode(client,niFiProperties,activeUrls) + } + else if(operation.toLowerCase().equals(CONNECT)){ + connectNode(client,niFiProperties,activeUrls) + } + else{ + throw new ParseException("Invalid operation provided: " + operation) + } + + }else{ + throw new UnsupportedOperationException("Node Manager Tool only supports clustered instance of NiFi running versions 1.0.0 or higher.") + } + + }else if(!commandLine.hasOption(BOOTSTRAP_CONF)){ + throw new ParseException("Missing -b option") + }else if(!commandLine.hasOption(NIFI_INSTALL_DIR)){ + throw new ParseException("Missing -d option") + }else{ + throw new ParseException("Missing -o option") + } + } + + } + + public static void main(String[] args) { + final NodeManagerTool tool = new NodeManagerTool() + final ClientFactory clientFactory = new NiFiClientFactory() + + try{ + tool.parse(clientFactory,args) + } catch (ParseException | RuntimeException 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/notify/NotificationTool.groovy b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/notify/NotificationTool.groovy new file mode 100644 index 0000000000..ce87499c8a --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/notify/NotificationTool.groovy @@ -0,0 +1,181 @@ +/* + * 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.notify + +import com.sun.jersey.api.client.Client +import com.sun.jersey.api.client.ClientResponse +import com.sun.jersey.api.client.WebResource +import org.apache.commons.lang3.StringUtils +import org.apache.nifi.toolkit.admin.client.NiFiClientUtil +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.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.util.NiFiProperties +import org.apache.nifi.web.api.dto.BulletinDTO +import org.apache.nifi.web.api.entity.BulletinEntity +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import java.nio.file.Paths + +public class NotificationTool extends AbstractAdminTool { + + private static final String DEFAULT_DESCRIPTION = "This tool is used to send notifications (bulletins) to a NiFi cluster. " + private static final String HELP_ARG = "help" + private static final String VERBOSE_ARG = "verbose" + private static final String BOOTSTRAP_CONF = "bootstrapConf" + private static final String NIFI_INSTALL_DIR = "nifiInstallDir" + private static final String NOTIFICATION_MESSAGE = "message" + private static final String NOTIFICATION_LEVEL = "level" + private final static String NOTIFICATION_ENDPOINT ="/nifi-api/controller/bulletin" + private final static String SUPPORTED_MINIMUM_VERSION = "1.2.0" + + NotificationTool() { + header = buildHeader(DEFAULT_DESCRIPTION) + setup() + } + + NotificationTool(final String description){ + header = buildHeader(description) + setup() + } + + @Override + protected Logger getLogger() { + LoggerFactory.getLogger(NotificationTool.class) + } + + protected Options getOptions(){ + final Options options = new Options() + options.addOption(Option.builder("h").longOpt(HELP_ARG).desc("Print help info").build()) + options.addOption(Option.builder("v").longOpt(VERBOSE_ARG).desc("Set mode to verbose (default is false)").build()) + options.addOption(Option.builder("b").longOpt(BOOTSTRAP_CONF).hasArg().desc("Existing Bootstrap Configuration file").build()) + options.addOption(Option.builder("d").longOpt(NIFI_INSTALL_DIR).hasArg().desc("NiFi Installation Directory").build()) + options.addOption(Option.builder("m").longOpt(NOTIFICATION_MESSAGE).hasArg().desc("Notification message for nifi instance or cluster").build()) + options.addOption(Option.builder("l").longOpt(NOTIFICATION_LEVEL).required(false).hasArg().desc("Level for notification bulletin INFO,WARN,ERROR").build()) + options + } + + void notifyCluster(final ClientFactory clientFactory, final String nifiPropertiesFile, final String bootstrapConfFile, final String nifiInstallDir, final String message, final String level){ + + if(isVerbose){ + logger.info("Loading nifi properties for host information") + } + + final String key = NiFiPropertiesLoader.extractKeyFromBootstrapFile(bootstrapConfFile) + final NiFiProperties niFiProperties = NiFiPropertiesLoader.withKey(key).load(nifiPropertiesFile) + final Client client = clientFactory.getClient(niFiProperties,nifiInstallDir) + final String url = NiFiClientUtil.getUrl(niFiProperties,NOTIFICATION_ENDPOINT) + final WebResource webResource = client.resource(url) + + if(isVerbose){ + logger.info("Contacting node at url:" + url) + } + + final BulletinEntity bulletinEntity = new BulletinEntity() + final BulletinDTO bulletinDTO = new BulletinDTO() + bulletinDTO.message = message + bulletinDTO.category = "NOTICE" + bulletinDTO.level = StringUtils.isEmpty(level) ? "INFO" : level + bulletinEntity.bulletin = bulletinDTO + final ClientResponse response = webResource.type("application/json").post(ClientResponse.class, bulletinEntity) + + Integer status = response.getStatus() + + if(status != 200){ + if(status == 404){ + throw new RuntimeException("The notification feature is not supported by each node in the cluster") + }else{ + throw new RuntimeException("Failed with HTTP error code: " + status) + } + } + + } + + void parse(final ClientFactory clientFactory, final String[] args) throws ParseException, UnsupportedOperationException { + + final CommandLine commandLine = new DefaultParser().parse(options,args) + + if (commandLine.hasOption(HELP_ARG)){ + printUsage(null) + }else{ + + if(commandLine.hasOption(BOOTSTRAP_CONF) && commandLine.hasOption(NOTIFICATION_MESSAGE) && commandLine.hasOption(NIFI_INSTALL_DIR)) { + + if(commandLine.hasOption(VERBOSE_ARG)){ + this.isVerbose = true; + } + + final String bootstrapConfFileName = commandLine.getOptionValue(BOOTSTRAP_CONF) + 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) + 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(isVerbose){ + logger.info("Attempting to connect with nifi using properties:", nifiPropertiesFileName) + } + + notifyCluster(clientFactory, nifiPropertiesFileName, bootstrapConfFileName,nifiInstallDir,notificationMessage,notificationLevel) + + if(isVerbose) { + logger.info("Message sent successfully to NiFi.") + } + }else{ + throw new UnsupportedOperationException("Notification Tool only supports NiFi versions 1.2.0 and above") + } + + }else if(!commandLine.hasOption(BOOTSTRAP_CONF)){ + throw new ParseException("Missing -b option") + }else if(!commandLine.hasOption(NIFI_INSTALL_DIR)){ + throw new ParseException("Missing -d option") + }else{ + throw new ParseException("Missing -m option") + } + } + + } + + public static void main(String[] args) { + final NotificationTool tool = new NotificationTool() + final ClientFactory clientFactory = new NiFiClientFactory() + + try{ + tool.parse(clientFactory,args) + } catch (ParseException | UnsupportedOperationException e) { + tool.printUsage(e.message); + System.exit(1) + } + + System.exit(0) + } + + +} 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 new file mode 100644 index 0000000000..9dc0090d13 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/util/AdminUtil.groovy @@ -0,0 +1,69 @@ +/* + * 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.util + +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry +import org.apache.commons.compress.archivers.zip.ZipFile +import org.apache.commons.lang3.StringUtils + +class AdminUtil { + + protected static String getNiFiVersionFromNar(final File nifiLibDir){ + + if(nifiLibDir.isDirectory()){ + File[] files = nifiLibDir.listFiles(new FilenameFilter() { + @Override + boolean accept(File dir, String name) { + name.startsWith("nifi-framework-nar") + } + }) + + if(files.length == 1){ + final ZipFile zipFile = new ZipFile(files[0]) + final ZipArchiveEntry archiveEntry = zipFile.getEntry("META-INF/MANIFEST.MF") + final InputStream is = zipFile.getInputStream(archiveEntry) + final Properties manifestProperties = new Properties() + manifestProperties.load(is) + String version = manifestProperties.get("Nar-Version") + zipFile.close() + return StringUtils.isEmpty(version)? null : version + + } + } + + null + } + + protected static String getNiFiVersionFromProperties(final File nifiConfDir) { + final String nifiPropertiesFileName = nifiConfDir.getAbsolutePath() + File.separator +"nifi.properties" + final File nifiPropertiesFile = new File(nifiPropertiesFileName) + final Properties nifiProperties = new Properties() + nifiProperties.load(new FileInputStream(nifiPropertiesFile)) + nifiProperties.getProperty("nifi.version") + } + + public static String getNiFiVersion(final File nifiConfDir, final File nifiLibDir){ + + String nifiVersion = getNiFiVersionFromProperties(nifiConfDir) + if(StringUtils.isEmpty(nifiVersion)){ + nifiVersion = getNiFiVersionFromNar(nifiLibDir) + } + return nifiVersion.replace("-SNAPSHOT","") + + } + +} diff --git a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/util/Version.groovy b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/util/Version.groovy new file mode 100644 index 0000000000..db5dc04b19 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/util/Version.groovy @@ -0,0 +1,82 @@ +/* + * 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.util + +import org.apache.commons.lang3.StringUtils + +class Version { + + private String[] versionNumber + private String delimeter + + Version(String version, String delimeter) { + this.versionNumber = version.tokenize(delimeter) + this.delimeter = delimeter + } + + String[] getVersionNumber() { + return versionNumber + } + + void setVersionNumber(String[] versionNumber) { + this.versionNumber = versionNumber + } + + String getDelimeter() { + return delimeter + } + + void setDelimeter(String delimeter) { + this.delimeter = delimeter + } + + boolean equals(o) { + if (this.is(o)) return true + if (getClass() != o.class) return false + Version version = (Version) o + if (!Arrays.equals(versionNumber, version.versionNumber)) return false + return true + } + + int hashCode() { + return (versionNumber != null ? Arrays.hashCode(versionNumber) : 0) + } + + public final static Comparator VERSION_COMPARATOR = new Comparator() { + @Override + int compare(Version o1, Version o2) { + String[] o1V = o1.versionNumber + String[] o2V = o2.versionNumber + + for(int i = 0; i < o1V.length; i++) { + Integer val1 = Integer.parseInt(o1V[i]) + Integer val2 = Integer.parseInt(o2V[i]) + if (val1.compareTo(val2) != 0) { + return val1.compareTo(val2) + } + } + return 0 + } + } + + + @Override + public String toString() { + StringUtils.join(versionNumber,delimeter) + } +} diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/groovy/org/apache/nifi/toolkit/admin/client/NiFiClientFactorySpec.groovy b/nifi-toolkit/nifi-toolkit-admin/src/test/groovy/org/apache/nifi/toolkit/admin/client/NiFiClientFactorySpec.groovy new file mode 100644 index 0000000000..a37b1c185c --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/groovy/org/apache/nifi/toolkit/admin/client/NiFiClientFactorySpec.groovy @@ -0,0 +1,247 @@ +/* + * 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.client + +import org.apache.commons.lang3.SystemUtils +import org.apache.nifi.properties.NiFiPropertiesLoader +import org.apache.nifi.security.util.CertificateUtils +import org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandalone +import org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandaloneCommandLine +import org.apache.nifi.util.NiFiProperties +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x500.X500NameBuilder +import org.bouncycastle.asn1.x500.style.BCStyle +import org.bouncycastle.asn1.x509.Extension +import org.bouncycastle.asn1.x509.Extensions +import org.bouncycastle.asn1.x509.ExtensionsGenerator +import org.bouncycastle.asn1.x509.GeneralName +import org.bouncycastle.asn1.x509.GeneralNames +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo +import org.bouncycastle.cert.X509CertificateHolder +import org.bouncycastle.cert.X509v3CertificateBuilder +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.operator.ContentSigner +import org.bouncycastle.operator.OperatorCreationException +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder +import spock.lang.Specification + +import javax.net.ssl.SSLSession +import java.nio.file.Files +import java.nio.file.attribute.PosixFilePermission +import java.security.InvalidKeyException +import java.security.KeyPair +import java.security.KeyPairGenerator +import java.security.NoSuchAlgorithmException +import java.security.NoSuchProviderException +import java.security.SignatureException +import java.security.cert.Certificate +import java.security.cert.CertificateException +import java.security.cert.X509Certificate +import java.util.concurrent.TimeUnit + +class NiFiClientFactorySpec extends Specification { + + private static final int KEY_SIZE = 2048 + private static final String SIGNATURE_ALGORITHM = "SHA256withRSA" + private static final int DAYS_IN_YEAR = 365 + private static final String ISSUER_DN = "CN=NiFi Test CA,OU=Security,O=Apache,ST=CA,C=US" + + def "get client for unsecure nifi"(){ + + given: + def NiFiProperties niFiProperties = Mock NiFiProperties + def clientFactory = new NiFiClientFactory() + + when: + def client = clientFactory.getClient(niFiProperties,"src/test/resources/notify") + + then: + client + + } + + def "get client for secured nifi"(){ + + given: + def File tmpDir = setupTmpDir() + def File testDir = new File("target/tmp/keys") + def toolkitCommandLine = ["-O", "-o",testDir.absolutePath,"-n","localhost","-C", "CN=user1","-S", "badKeyPass", "-K", "badKeyPass", "-P", "badTrustPass"] + + TlsToolkitStandaloneCommandLine tlsToolkitStandaloneCommandLine = new TlsToolkitStandaloneCommandLine() + tlsToolkitStandaloneCommandLine.parse(toolkitCommandLine as String[]) + new TlsToolkitStandalone().createNifiKeystoresAndTrustStores(tlsToolkitStandaloneCommandLine.createConfig()) + + def bootstrapConfFile = "src/test/resources/notify/conf/bootstrap.conf" + def nifiPropertiesFile = "src/test/resources/notify/conf/nifi-secured.properties" + def key = NiFiPropertiesLoader.extractKeyFromBootstrapFile(bootstrapConfFile) + def NiFiProperties niFiProperties = NiFiPropertiesLoader.withKey(key).load(nifiPropertiesFile) + def clientFactory = new NiFiClientFactory() + + when: + def client = clientFactory.getClient(niFiProperties,"src/test/resources/notify") + + then: + client + + cleanup: + tmpDir.deleteDir() + + } + + def "should verify CN in certificate based on subjectDN"(){ + + given: + final String EXPECTED_DN = "CN=client.nifi.apache.org,OU=Security,O=Apache,ST=CA,C=US" + Certificate[] certificateChain = generateCertificateChain(EXPECTED_DN,ISSUER_DN) + def mockSession = Mock(SSLSession) + NiFiClientFactory.NiFiHostnameVerifier verifier = new NiFiClientFactory.NiFiHostnameVerifier() + mockSession.getPeerCertificates() >> certificateChain + + when: + def verified = verifier.verify("client.nifi.apache.org",mockSession) + + then: + verified + + } + + def "should not verify based on no certificate chain"(){ + + given: + final String EXPECTED_DN = "CN=client.nifi.apache.org, OU=Security, O=Apache, ST=CA, C=US" + Certificate[] certificateChain = [] as Certificate[] + def mockSession = Mock(SSLSession) + NiFiClientFactory.NiFiHostnameVerifier verifier = new NiFiClientFactory.NiFiHostnameVerifier() + mockSession.getPeerCertificates() >> certificateChain + + when: + def notVerified = !verifier.verify("client.nifi.apache.org",mockSession) + + then: + notVerified + + } + + def "should not verify based on multiple CN values"(){ + + given: + final KeyPair issuerKeyPair = generateKeyPair() + KeyPair keyPair = generateKeyPair() + final X509Certificate issuerCertificate = CertificateUtils.generateSelfSignedX509Certificate(issuerKeyPair,ISSUER_DN, SIGNATURE_ALGORITHM, DAYS_IN_YEAR) + + ContentSigner sigGen = new JcaContentSignerBuilder(SIGNATURE_ALGORITHM).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(issuerKeyPair.getPrivate()); + SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(keyPair.public.getEncoded()); + Date startDate = new Date(); + Date endDate = new Date(startDate.getTime() + TimeUnit.DAYS.toMillis(DAYS_IN_YEAR)); + + def X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE) + nameBuilder.addRDN(BCStyle.CN,"client.nifi.apache.org,nifi.apache.org") + def name = nameBuilder.build() + + X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(new X500Name(issuerCertificate.getSubjectX500Principal().getName()), + BigInteger.valueOf(System.currentTimeMillis()), startDate, endDate, name, + subPubKeyInfo); + + X509CertificateHolder certificateHolder = certBuilder.build(sigGen); + Certificate certificate = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certificateHolder); + + Certificate[] certificateChain = [certificate,issuerCertificate] as Certificate[] + def mockSession = Mock(SSLSession) + NiFiClientFactory.NiFiHostnameVerifier verifier = new NiFiClientFactory.NiFiHostnameVerifier() + mockSession.getPeerCertificates() >> certificateChain + + + when: + def notVerified = !verifier.verify("client.nifi.apache.org",mockSession) + + then: + notVerified + + } + + def "should verify appropriately CN in certificate based on SAN"(){ + + given: + + final List SANS = ["127.0.0.1", "nifi.apache.org"] + def gns = SANS.collect { String san -> + new GeneralName(GeneralName.dNSName, san) + } + def generalNames = new GeneralNames(gns as GeneralName[]) + ExtensionsGenerator extensionsGenerator = new ExtensionsGenerator() + extensionsGenerator.addExtension(Extension.subjectAlternativeName, false, generalNames) + Extensions extensions = extensionsGenerator.generate() + + final String EXPECTED_DN = "CN=client.nifi.apache.org,OU=Security,O=Apache,ST=CA,C=US" + final KeyPair issuerKeyPair = generateKeyPair() + final X509Certificate issuerCertificate = CertificateUtils.generateSelfSignedX509Certificate(issuerKeyPair,ISSUER_DN, SIGNATURE_ALGORITHM, DAYS_IN_YEAR) + final X509Certificate certificate = generateIssuedCertificate(EXPECTED_DN, issuerCertificate,extensions, issuerKeyPair) + Certificate[] certificateChain = [certificate, issuerCertificate] as X509Certificate[] + def mockSession = Mock(SSLSession) + NiFiClientFactory.NiFiHostnameVerifier verifier = new NiFiClientFactory.NiFiHostnameVerifier() + mockSession.getPeerCertificates() >> certificateChain + + when: + def verified = verifier.verify("nifi.apache.org",mockSession) + def notVerified = !verifier.verify("fake.apache.org",mockSession) + + + then: + verified + notVerified + + } + + def KeyPair generateKeyPair() throws NoSuchAlgorithmException { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA") + keyPairGenerator.initialize(KEY_SIZE) + return keyPairGenerator.generateKeyPair() + } + + def X509Certificate generateIssuedCertificate(String dn, X509Certificate issuer,Extensions extensions, KeyPair issuerKey) throws IOException, NoSuchAlgorithmException, CertificateException, NoSuchProviderException, SignatureException, InvalidKeyException, OperatorCreationException { + KeyPair keyPair = generateKeyPair() + return CertificateUtils.generateIssuedCertificate(dn, keyPair.getPublic(),extensions, issuer, issuerKey, SIGNATURE_ALGORITHM, DAYS_IN_YEAR) + } + + def X509Certificate[] generateCertificateChain(String dn,String issuerDn) { + final KeyPair issuerKeyPair = generateKeyPair() + final X509Certificate issuerCertificate = CertificateUtils.generateSelfSignedX509Certificate(issuerKeyPair, issuerDn, SIGNATURE_ALGORITHM, DAYS_IN_YEAR) + final X509Certificate certificate = generateIssuedCertificate(dn, issuerCertificate,null, issuerKeyPair) + [certificate, issuerCertificate] as X509Certificate[] + } + + 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/client/NiFiClientUtilSpec.groovy b/nifi-toolkit/nifi-toolkit-admin/src/test/groovy/org/apache/nifi/toolkit/admin/client/NiFiClientUtilSpec.groovy new file mode 100644 index 0000000000..32a1522739 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/groovy/org/apache/nifi/toolkit/admin/client/NiFiClientUtilSpec.groovy @@ -0,0 +1,109 @@ +/* + * 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.client + +import com.sun.jersey.api.client.Client +import com.sun.jersey.api.client.ClientResponse +import com.sun.jersey.api.client.WebResource +import org.apache.nifi.util.NiFiProperties +import org.apache.nifi.web.api.entity.ClusterEntity +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 javax.ws.rs.core.Response + +class NiFiClientUtilSpec extends Specification{ + + @Rule + public final ExpectedSystemExit exit = ExpectedSystemExit.none() + + @Rule + public final SystemOutRule systemOutRule = new SystemOutRule().enableLog() + + def "build unsecure url successfully"(){ + + given: + def NiFiProperties niFiProperties = Mock NiFiProperties + + + when: + def url = NiFiClientUtil.getUrl(niFiProperties,"/nifi-api/controller/cluster/nodes/1") + + then: + + 3 * niFiProperties.getProperty(_) + url == "http://localhost:8080/nifi-api/controller/cluster/nodes/1" + } + + + def "get cluster info successfully"(){ + + given: + def Client client = Mock Client + def NiFiProperties niFiProperties = Mock NiFiProperties + def WebResource resource = Mock WebResource + def WebResource.Builder builder = Mock WebResource.Builder + def ClientResponse response = Mock ClientResponse + def ClusterEntity clusterEntity = Mock ClusterEntity + + when: + def entity = NiFiClientUtil.getCluster(client, niFiProperties, []) + + then: + + 3 * niFiProperties.getProperty(_) + 1 * client.resource(_ as String) >> resource + 1 * resource.type(_) >> builder + 1 * builder.get(_) >> response + 1 * response.getStatus() >> 200 + 1 * response.getEntity(ClusterEntity.class) >> clusterEntity + entity == clusterEntity + + } + + def "get cluster info fails"(){ + + given: + def Client client = Mock Client + def NiFiProperties niFiProperties = Mock NiFiProperties + def WebResource resource = Mock WebResource + def WebResource.Builder builder = Mock WebResource.Builder + def ClientResponse response = Mock ClientResponse + def Response.StatusType statusType = Mock Response.StatusType + + when: + + NiFiClientUtil.getCluster(client, niFiProperties, []) + + then: + + 3 * niFiProperties.getProperty(_) + 1 * client.resource(_ as String) >> resource + 1 * resource.type(_) >> builder + 1 * builder.get(_) >> response + 1 * response.getStatus() >> 500 + 1 * response.getStatusInfo() >> statusType + 1 * statusType.getReasonPhrase() >> "Only a node connected to a cluster can process the request." + def e = thrown(RuntimeException) + e.message == "Unable to obtain cluster information" + + } + + +} diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/groovy/org/apache/nifi/toolkit/admin/nodemanager/NodeManagerToolSpec.groovy b/nifi-toolkit/nifi-toolkit-admin/src/test/groovy/org/apache/nifi/toolkit/admin/nodemanager/NodeManagerToolSpec.groovy new file mode 100644 index 0000000000..e482bbc071 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/groovy/org/apache/nifi/toolkit/admin/nodemanager/NodeManagerToolSpec.groovy @@ -0,0 +1,414 @@ + +/* + * 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.nodemanager + +import com.sun.jersey.api.client.Client +import com.sun.jersey.api.client.ClientResponse +import com.sun.jersey.api.client.WebResource +import org.apache.commons.cli.ParseException +import org.apache.nifi.toolkit.admin.client.ClientFactory +import org.apache.nifi.util.NiFiProperties +import org.apache.nifi.web.api.dto.ClusterDTO +import org.apache.nifi.web.api.dto.NodeDTO +import org.apache.nifi.web.api.entity.ClusterEntity +import org.apache.nifi.web.api.entity.NodeEntity +import org.junit.Rule +import org.junit.contrib.java.lang.system.ExpectedSystemExit +import org.junit.contrib.java.lang.system.SystemOutRule +import spock.lang.Specification + +class NodeManagerToolSpec 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 ClientFactory clientFactory = Mock ClientFactory + def config = new NodeManagerTool() + + when: + config.parse(clientFactory,["-h"] as String[]) + + then: + systemOutRule.getLog().contains("usage: org.apache.nifi.toolkit.admin.nodemanager.NodeManagerTool") + } + + def "throws exception missing bootstrap conf flag"() { + + given: + def ClientFactory clientFactory = Mock ClientFactory + def config = new NodeManagerTool() + + when: + config.parse(clientFactory,["-d", "/install/nifi"] as String[]) + + then: + def e = thrown(ParseException) + e.message == "Missing -b option" + } + + def "throws exception missing directory"(){ + + given: + def ClientFactory clientFactory = Mock ClientFactory + def config = new NodeManagerTool() + + when: + config.parse(clientFactory,["-b","src/test/resources/notify/conf/bootstrap.conf"] as String[]) + + then: + def e = thrown(ParseException) + e.message == "Missing -d option" + } + + def "throws exception missing operation"(){ + + given: + def ClientFactory clientFactory = Mock ClientFactory + def config = new NodeManagerTool() + + when: + config.parse(clientFactory,["-b","src/test/resources/notify/conf/bootstrap.conf","-d", "/install/nifi"] as String[]) + + then: + def e = thrown(ParseException) + e.message == "Missing -o option" + } + + def "throws exception invalid operation"(){ + + given: + def NiFiProperties niFiProperties = Mock NiFiProperties + def ClientFactory clientFactory = Mock ClientFactory + def Client client = Mock Client + def WebResource resource = Mock WebResource + def WebResource.Builder builder = Mock WebResource.Builder + def ClientResponse response = Mock ClientResponse + def ClusterEntity clusterEntity = Mock ClusterEntity + def ClusterDTO clusterDTO = Mock ClusterDTO + def NodeDTO nodeDTO = new NodeDTO() + nodeDTO.address = "localhost" + nodeDTO.nodeId = "1" + nodeDTO.status = "CONNECTED" + nodeDTO.apiPort = 8080 + def List nodeDTOs = [nodeDTO] + def NodeEntity nodeEntity = new NodeEntity() + nodeEntity.node = nodeDTO + def config = new NodeManagerTool() + + + niFiProperties.getProperty(_) >> "localhost" + clientFactory.getClient(_,_) >> client + client.resource(_ as String) >> resource + resource.type(_) >> builder + builder.get(ClientResponse.class) >> response + builder.put(_,_) >> response + builder.delete(ClientResponse.class,_) >> response + response.getStatus() >> 200 + response.getEntity(ClusterEntity.class) >> clusterEntity + response.getEntity(NodeEntity.class) >> nodeEntity + clusterEntity.getCluster() >> clusterDTO + clusterDTO.getNodes() >> nodeDTOs + nodeDTO.address >> "localhost" + + when: + config.parse(clientFactory,["-b","src/test/resources/notify/conf/bootstrap.conf","-d","/install/nifi","-o","fake"] as String[]) + + then: + def e = thrown(ParseException) + e.message == "Invalid operation provided: fake" + } + + def "get node info successfully"(){ + + given: + def NiFiProperties niFiProperties = Mock NiFiProperties + def ClusterEntity clusterEntity = Mock ClusterEntity + def ClusterDTO clusterDTO = Mock ClusterDTO + def NodeDTO nodeDTO = new NodeDTO() + nodeDTO.address = "1" + def List nodeDTOs = [nodeDTO] + def config = new NodeManagerTool() + + when: + def entity = config.getCurrentNode(clusterEntity,niFiProperties) + + then: + + 1 * clusterEntity.getCluster() >> clusterDTO + 1 * clusterDTO.getNodes() >> nodeDTOs + 2 * niFiProperties.getProperty(_) >> "1" + entity == nodeDTO + + } + + def "delete node successfully"(){ + + given: + def String url = "http://locahost:8080/nifi-api/controller" + def Client client = Mock Client + def WebResource resource = Mock WebResource + def WebResource.Builder builder = Mock WebResource.Builder + def ClientResponse response = Mock ClientResponse + def config = new NodeManagerTool() + + when: + config.deleteNode(url,client) + + then: + + 1 * client.resource(_ as String) >> resource + 1 * resource.type(_) >> builder + 1 * builder.delete(_) >> response + 1 * response.getStatus() >> 200 + + } + + def "delete node failed"(){ + + given: + def String url = "http://locahost:8080/nifi-api/controller" + def Client client = Mock Client + def WebResource resource = Mock WebResource + def WebResource.Builder builder = Mock WebResource.Builder + def ClientResponse response = Mock ClientResponse + def config = new NodeManagerTool() + + when: + config.deleteNode(url,client) + + then: + 1 * client.resource(_ as String) >> resource + 1 * resource.type(_) >> builder + 1 * builder.delete(_) >> response + 2 * response.getStatus() >> 403 + def e = thrown(RuntimeException) + e.message == "Failed with HTTP error code: 403" + + } + + def "update node successfully"(){ + + given: + def String url = "http://locahost:8080/nifi-api/controller" + def Client client = Mock Client + def WebResource resource = Mock WebResource + def WebResource.Builder builder = Mock WebResource.Builder + def ClientResponse response = Mock ClientResponse + def NodeDTO nodeDTO = new NodeDTO() + def NodeEntity nodeEntity = Mock NodeEntity + def config = new NodeManagerTool() + + when: + def entity = config.updateNode(url,client,nodeDTO,NodeManagerTool.STATUS.DISCONNECTING) + + then: + 1 * client.resource(_ as String) >> resource + 1 * resource.type(_) >> builder + 1 * builder.put(_,_) >> response + 1 * response.getStatus() >> 200 + 1 * response.getEntity(NodeEntity.class) >> nodeEntity + entity == nodeEntity + + } + + def "update node fails"(){ + + given: + def String url = "http://locahost:8080/nifi-api/controller" + def Client client = Mock Client + def WebResource resource = Mock WebResource + def WebResource.Builder builder = Mock WebResource.Builder + def ClientResponse response = Mock ClientResponse + def NodeDTO nodeDTO = new NodeDTO() + def config = new NodeManagerTool() + + when: + config.updateNode(url,client,nodeDTO,NodeManagerTool.STATUS.DISCONNECTING) + + then: + 1 * client.resource(_ as String) >> resource + 1 * resource.type(_) >> builder + 1 * builder.put(_,_) >> response + 2 * response.getStatus() >> 403 + def e = thrown(RuntimeException) + e.message == "Failed with HTTP error code: 403" + + } + + def "disconnect node successfully"(){ + + setup: + def NiFiProperties niFiProperties = Mock NiFiProperties + def Client client = Mock Client + def WebResource resource = Mock WebResource + def WebResource.Builder builder = Mock WebResource.Builder + def ClientResponse response = Mock ClientResponse + def ClusterEntity clusterEntity = Mock ClusterEntity + def ClusterDTO clusterDTO = Mock ClusterDTO + def NodeDTO nodeDTO = new NodeDTO() + nodeDTO.address = "localhost" + nodeDTO.nodeId = "1" + nodeDTO.status = "CONNECTED" + def List nodeDTOs = [nodeDTO] + def NodeEntity nodeEntity = new NodeEntity() + nodeEntity.node = nodeDTO + def config = new NodeManagerTool() + + + niFiProperties.getProperty(_) >> "localhost" + client.resource(_ as String) >> resource + resource.type(_) >> builder + builder.get(ClientResponse.class) >> response + builder.put(_,_) >> response + response.getStatus() >> 200 + response.getEntity(ClusterEntity.class) >> clusterEntity + response.getEntity(NodeEntity.class) >> nodeEntity + clusterEntity.getCluster() >> clusterDTO + clusterDTO.getNodes() >> nodeDTOs + nodeDTO.address >> "localhost" + + expect: + config.disconnectNode(client, niFiProperties,["http://localhost:8080"]) + + } + + def "connect node successfully"(){ + + setup: + def NiFiProperties niFiProperties = Mock NiFiProperties + def Client client = Mock Client + def WebResource resource = Mock WebResource + def WebResource.Builder builder = Mock WebResource.Builder + def ClientResponse response = Mock ClientResponse + def ClusterEntity clusterEntity = Mock ClusterEntity + def ClusterDTO clusterDTO = Mock ClusterDTO + def NodeDTO nodeDTO = new NodeDTO() + nodeDTO.address = "localhost" + nodeDTO.nodeId = "1" + nodeDTO.status = "DISCONNECTED" + def List nodeDTOs = [nodeDTO] + def NodeEntity nodeEntity = new NodeEntity() + nodeEntity.node = nodeDTO + def config = new NodeManagerTool() + + + niFiProperties.getProperty(_) >> "localhost" + client.resource(_ as String) >> resource + resource.type(_) >> builder + builder.get(ClientResponse.class) >> response + builder.put(_,_) >> response + response.getStatus() >> 200 + response.getEntity(ClusterEntity.class) >> clusterEntity + response.getEntity(NodeEntity.class) >> nodeEntity + clusterEntity.getCluster() >> clusterDTO + clusterDTO.getNodes() >> nodeDTOs + nodeDTO.address >> "localhost" + + expect: + config.connectNode(client, niFiProperties,["http://localhost:8080"]) + + } + + def "remove node successfully"(){ + + setup: + def NiFiProperties niFiProperties = Mock NiFiProperties + def Client client = Mock Client + def WebResource resource = Mock WebResource + def WebResource.Builder builder = Mock WebResource.Builder + def ClientResponse response = Mock ClientResponse + def ClusterEntity clusterEntity = Mock ClusterEntity + def ClusterDTO clusterDTO = Mock ClusterDTO + def NodeDTO nodeDTO = new NodeDTO() + nodeDTO.address = "localhost" + nodeDTO.nodeId = "1" + nodeDTO.status = "CONNECTED" + def List nodeDTOs = [nodeDTO] + def NodeEntity nodeEntity = new NodeEntity() + nodeEntity.node = nodeDTO + def config = new NodeManagerTool() + + + niFiProperties.getProperty(_) >> "localhost" + client.resource(_ as String) >> resource + resource.type(_) >> builder + builder.get(ClientResponse.class) >> response + builder.put(_,_) >> response + builder.delete(ClientResponse.class,_) >> response + response.getStatus() >> 200 + response.getEntity(ClusterEntity.class) >> clusterEntity + response.getEntity(NodeEntity.class) >> nodeEntity + clusterEntity.getCluster() >> clusterDTO + clusterDTO.getNodes() >> nodeDTOs + nodeDTO.address >> "localhost" + + expect: + config.removeNode(client, niFiProperties,["http://localhost:8080"]) + + } + + def "parse args and delete node"(){ + + setup: + def NiFiProperties niFiProperties = Mock NiFiProperties + def ClientFactory clientFactory = Mock ClientFactory + def Client client = Mock Client + def WebResource resource = Mock WebResource + def WebResource.Builder builder = Mock WebResource.Builder + def ClientResponse response = Mock ClientResponse + def ClusterEntity clusterEntity = Mock ClusterEntity + def ClusterDTO clusterDTO = Mock ClusterDTO + def NodeDTO nodeDTO = new NodeDTO() + nodeDTO.address = "localhost" + nodeDTO.nodeId = "1" + nodeDTO.status = "CONNECTED" + def List nodeDTOs = [nodeDTO] + def NodeEntity nodeEntity = new NodeEntity() + nodeEntity.node = nodeDTO + def config = new NodeManagerTool() + + + niFiProperties.getProperty(_) >> "localhost" + clientFactory.getClient(_,_) >> client + client.resource(_ as String) >> resource + resource.type(_) >> builder + builder.get(ClientResponse.class) >> response + builder.put(_,_) >> response + builder.delete(ClientResponse.class,_) >> response + response.getStatus() >> 200 + response.getEntity(ClusterEntity.class) >> clusterEntity + response.getEntity(NodeEntity.class) >> nodeEntity + clusterEntity.getCluster() >> clusterDTO + clusterDTO.getNodes() >> nodeDTOs + nodeDTO.address >> "localhost" + + + expect: + config.parse(clientFactory,["-b","src/test/resources/notify/conf/bootstrap.conf","-d","/bogus/nifi/dir","-o","remove","-u","http://localhost:8080,http://localhost1:8080"] as String[]) + + } + + +} diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/groovy/org/apache/nifi/toolkit/admin/notify/NotificationToolSpec.groovy b/nifi-toolkit/nifi-toolkit-admin/src/test/groovy/org/apache/nifi/toolkit/admin/notify/NotificationToolSpec.groovy new file mode 100644 index 0000000000..57468c075d --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/groovy/org/apache/nifi/toolkit/admin/notify/NotificationToolSpec.groovy @@ -0,0 +1,171 @@ +/* + * 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.notify + +import com.sun.jersey.api.client.Client +import com.sun.jersey.api.client.ClientResponse +import com.sun.jersey.api.client.WebResource +import org.apache.commons.cli.ParseException +import org.apache.nifi.toolkit.admin.client.ClientFactory +import org.junit.Rule +import org.junit.contrib.java.lang.system.ExpectedSystemExit +import org.junit.contrib.java.lang.system.SystemOutRule +import spock.lang.Specification + +class NotificationToolSpec 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 ClientFactory clientFactory = Mock ClientFactory + def config = new NotificationTool() + + when: + config.parse(clientFactory,["-h"] as String[]) + + then: + systemOutRule.getLog().contains("usage: org.apache.nifi.toolkit.admin.notify.NotificationTool") + } + + def "throws exception missing bootstrap conf flag"() { + + given: + def ClientFactory clientFactory = Mock ClientFactory + def config = new NotificationTool() + + when: + config.parse(clientFactory,["-d", "/missing/bootstrap/conf"] as String[]) + + then: + def e = thrown(ParseException) + e.message == "Missing -b option" + } + + def "throws exception missing message"(){ + + given: + def ClientFactory clientFactory = Mock ClientFactory + def config = new NotificationTool() + + when: + config.parse(clientFactory,["-b","/tmp/fake/upgrade/conf","-v","-d","/bogus/nifi/dir"] as String[]) + + then: + def e = thrown(ParseException) + e.message == "Missing -m option" + } + + def "throws exception missing directory"(){ + + given: + def ClientFactory clientFactory = Mock ClientFactory + def config = new NotificationTool() + + when: + config.parse(clientFactory,["-b","src/test/resources/notify/conf/bootstrap.conf","-m","shutting down in 30 seconds"] as String[]) + + then: + def e = thrown(ParseException) + e.message == "Missing -d option" + } + + + def "send cluster message successfully"(){ + + given: + def ClientFactory clientFactory = Mock ClientFactory + def Client client = Mock Client + def WebResource resource = Mock WebResource + def WebResource.Builder builder = Mock WebResource.Builder + def ClientResponse response = Mock ClientResponse + + def config = new NotificationTool() + + when: + config.notifyCluster(clientFactory,"src/test/resources/notify/conf/nifi.properties","src/test/resources/notify/conf/bootstrap.conf","/bogus/nifi/dir","shutting down in 30 seconds","WARN") + + then: + + 1 * clientFactory.getClient(_,_) >> client + 1 * client.resource(_ as String) >> resource + 1 * resource.type(_) >> builder + 1 * builder.post(_,_) >> response + 1 * response.getStatus() >> 200 + + } + + def "cluster message failed"(){ + + given: + def ClientFactory clientFactory = Mock ClientFactory + def Client client = Mock Client + def WebResource resource = Mock WebResource + def WebResource.Builder builder = Mock WebResource.Builder + def ClientResponse response = Mock ClientResponse + + def config = new NotificationTool() + + when: + config.notifyCluster(clientFactory,"src/test/resources/notify/conf/nifi.properties","src/test/resources/notify/conf/bootstrap.conf","/bogus/nifi/dir","shutting down in 30 seconds","WARN") + + then: + + 1 * clientFactory.getClient(_,_) >> client + 1 * client.resource(_ as String) >> resource + 1 * resource.type(_) >> builder + 1 * builder.post(_,_) >> response + 1 * response.getStatus() >> 403 + def e = thrown(RuntimeException) + e.message == "Failed with HTTP error code: 403" + + } + + def "parse comment and send cluster message successfully"(){ + + given: + def ClientFactory clientFactory = Mock ClientFactory + def Client client = Mock Client + def WebResource resource = Mock WebResource + def WebResource.Builder builder = Mock WebResource.Builder + def ClientResponse response = Mock ClientResponse + + def config = new NotificationTool() + + when: + config.parse(clientFactory,["-b","src/test/resources/notify/conf/bootstrap.conf","-d","/bogus/nifi/dir","-m","shutting down in 30 seconds","-l","ERROR"] as String[]) + + then: + + 1 * clientFactory.getClient(_,_) >> client + 1 * client.resource(_ as String) >> resource + 1 * resource.type(_) >> builder + 1 * builder.post(_,_) >> response + 1 * response.getStatus() >> 200 + + } + + + +} 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 new file mode 100644 index 0000000000..854eefb83d --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/groovy/org/apache/nifi/toolkit/admin/util/AdminUtilSpec.groovy @@ -0,0 +1,54 @@ +/* + * 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.util + +import spock.lang.Specification + +class AdminUtilSpec extends Specification{ + + def "get nifi version with version in properties"(){ + + setup: + + def nifiConfDir = new File("src/test/resources/conf") + def nifiLibDir = new File("src/test/resources/lib") + + when: + + def version = AdminUtil.getNiFiVersion(nifiConfDir,nifiLibDir) + + then: + version == "1.1.0" + } + + def "get nifi version with version in nar"(){ + + setup: + + def nifiConfDir = new File("src/test/resources/upgrade/conf") + def nifiLibDir = new File("src/test/resources/lib") + + when: + + def version = AdminUtil.getNiFiVersion(nifiConfDir,nifiLibDir) + + then: + version == "1.2.0" + } + + +} diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/resources/conf/bootstrap.conf b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/conf/bootstrap.conf new file mode 100644 index 0000000000..a4a59f192a --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/conf/bootstrap.conf @@ -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. +# + +# Java command to use when running NiFi +java=java + +# Username to use when running NiFi. This value will be ignored on Windows. +run.as= + +# Configure where NiFi's lib and conf directories live +lib.dir=./lib +conf.dir=./conf + +# How long to wait after telling NiFi to shutdown before explicitly killing the Process +graceful.shutdown.seconds=20 + +# Disable JSR 199 so that we can use JSP's without running a JDK +java.arg.1=-Dorg.apache.jasper.compiler.disablejsr199=true diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/resources/conf/login-identity-providers.xml b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/conf/login-identity-providers.xml new file mode 100644 index 0000000000..7666152865 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/conf/login-identity-providers.xml @@ -0,0 +1,112 @@ + + + + + + + + + + \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/resources/conf/nifi.properties b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/conf/nifi.properties new file mode 100644 index 0000000000..c38a30a6cc --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/conf/nifi.properties @@ -0,0 +1,28 @@ +# +# 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. +# +nifi.version=1.1.0 +nifi.cluster.is.node=true +nifi.cluster.node.address=localhost +nifi.cluster.node.protocol.port=8300 +nifi.cluster.node.protocol.threads=2 +nifi.cluster.node.event.history.size= +nifi.cluster.node.connection.timeout= +nifi.cluster.node.read.timeout=30 +nifi.cluster.firewall.file= +nifi.cluster.flow.election.max.wait.time=1 +nifi.cluster.flow.election.max.candidates= +nifi.fluster.an.old.variable=true \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/resources/external/conf/bootstrap.conf b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/external/conf/bootstrap.conf new file mode 100644 index 0000000000..5ff5cdda96 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/external/conf/bootstrap.conf @@ -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. +# + +# Java command to use when running NiFi +java=java + +# Username to use when running NiFi. This value will be ignored on Windows. +run.as= + +# Configure where NiFi's lib and conf directories live +lib.dir=./lib +conf.dir=target/tmp/conf + +# How long to wait after telling NiFi to shutdown before explicitly killing the Process +graceful.shutdown.seconds=20 + +# Disable JSR 199 so that we can use JSP's without running a JDK +java.arg.1=-Dorg.apache.jasper.compiler.disablejsr199=true diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/resources/external/conf/login-identity-providers.xml b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/external/conf/login-identity-providers.xml new file mode 100644 index 0000000000..7666152865 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/external/conf/login-identity-providers.xml @@ -0,0 +1,112 @@ + + + + + + + + + + \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/resources/external/conf/nifi.properties b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/external/conf/nifi.properties new file mode 100644 index 0000000000..c38a30a6cc --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/external/conf/nifi.properties @@ -0,0 +1,28 @@ +# +# 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. +# +nifi.version=1.1.0 +nifi.cluster.is.node=true +nifi.cluster.node.address=localhost +nifi.cluster.node.protocol.port=8300 +nifi.cluster.node.protocol.threads=2 +nifi.cluster.node.event.history.size= +nifi.cluster.node.connection.timeout= +nifi.cluster.node.read.timeout=30 +nifi.cluster.firewall.file= +nifi.cluster.flow.election.max.wait.time=1 +nifi.cluster.flow.election.max.candidates= +nifi.fluster.an.old.variable=true \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/resources/filemanager/bootstrap.conf b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/filemanager/bootstrap.conf new file mode 100644 index 0000000000..a4a59f192a --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/filemanager/bootstrap.conf @@ -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. +# + +# Java command to use when running NiFi +java=java + +# Username to use when running NiFi. This value will be ignored on Windows. +run.as= + +# Configure where NiFi's lib and conf directories live +lib.dir=./lib +conf.dir=./conf + +# How long to wait after telling NiFi to shutdown before explicitly killing the Process +graceful.shutdown.seconds=20 + +# Disable JSR 199 so that we can use JSP's without running a JDK +java.arg.1=-Dorg.apache.jasper.compiler.disablejsr199=true diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/resources/filemanager/myid b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/filemanager/myid new file mode 100644 index 0000000000..56a6051ca2 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/filemanager/myid @@ -0,0 +1 @@ +1 \ No newline at end of file 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 new file mode 100644 index 0000000000000000000000000000000000000000..c7bcdf413477ef5cbbc6ec34d1c02f3143f2d089 GIT binary patch literal 27167 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 An*aa+ literal 0 HcmV?d00001 diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/resources/filemanager/nifi.properties b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/filemanager/nifi.properties new file mode 100644 index 0000000000..0aebbe8380 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/filemanager/nifi.properties @@ -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. +# +nifi.version=1.1.0 +nifi.cluster.is.node=true +nifi.cluster.node.address=localhost +nifi.cluster.node.protocol.port=8300 +nifi.cluster.node.protocol.threads=2 +nifi.cluster.node.event.history.size= +nifi.cluster.node.connection.timeout= +nifi.cluster.node.read.timeout=30 +nifi.cluster.firewall.file= +nifi.cluster.flow.election.max.wait.time=1 +nifi.cluster.flow.election.max.candidates= +nifi.fluster.an.old.variable=true +nifi.content.repository.directory.default=./content_repository +nifi.provenance.repository.directory.default=./provenance_repository +nifi.flowfile.repository.directory=./flowfile_repository +nifi.database.directory=./database_repository \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/resources/lib/nifi-framework-nar-1.2.0.nar b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/lib/nifi-framework-nar-1.2.0.nar new file mode 100644 index 0000000000000000000000000000000000000000..08faed30e2b7f44e3df0f468a43877148a6c5ee9 GIT binary patch literal 1385 zcmWIWW@h1H0DS@G#7_c^c6M#9tXj7y^LWL>M@L zFbYkZ5VAHsm*Du|lKi4nkbaQf9;p7eGpzXz83?$f_i}ri=xN?9V8}~wJ;}L)fi--= z_lt_R1UHEU1*kSvPWl?wRi7#2&v4_zj`#*1g@e7uc9U+1wX8Ecr`RIk)KQzfuH;Uc zv{Q$y(R}{abtT2p^VzM+l8s_tc$shC7{5Sy?h7CBU6;fP-tp2zX@u z*aKb`tA4$BoNkd*Mj*C*@)e^^IA1JeWvW_1B+#*NYf%|ApP&M<^49%x{; zXlr3{oGEG$`vT-^c8)KS*Y}tJL!knQ5$V7%2{rkMAg2RgM?X(D*WeI6UpG)n$OQ(* zJPAmE`1T*nYjO~1d#|$R@V)~%5eo#rx7`Yvozyu?t}0W_;DuYnnROR_eT!AR68C@R z&u71ludGt4Zsj)$+ZttbkokUe$tu0X!~k*ilVy>7o)-Sg{w$CxZ#wI`$w0uEBZ*@k zpVE{hwZ%U!e-8?t6XO5O@oe_5mpZfAlwWdn&raDcRGnIz-_2&WG{w)uF?GYj_t)k) zF1{UiZLL~FsoP8CoGsPsxSLXYuN{i+UfQj?dc!u8qJLXCD;a+0&U%0Sefxf<7uT+z zf0@Nq_C%zu*`uW}s8jHB(yen-bIwkD%-Svdard0dhArV*?0-unHtNa=(Fo>!5bgPG0|X8m=v1Uep`d_yB3&H<%bd`2KU=dWV{ zvJn_wLzso|8gf#UMe*7OtY)F)26SISj0L4* + + + + + + + + + \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/resources/no_rules/conf/nifi.properties b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/no_rules/conf/nifi.properties new file mode 100644 index 0000000000..f85b5671be --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/no_rules/conf/nifi.properties @@ -0,0 +1,29 @@ +# +# 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. +# +#upgrade test properties +nifi.version=1.1.0 +nifi.cluster.is.node= +nifi.cluster.node.address= +nifi.cluster.node.protocol.port= +nifi.cluster.node.protocol.threads= +nifi.cluster.node.event.history.size= +nifi.cluster.node.connection.timeout= +nifi.cluster.node.read.timeout= +nifi.cluster.firewall.file= +nifi.cluster.flow.election.max.wait.time= +nifi.cluster.flow.election.max.candidates= +nifi.cluster.a.new.variable= \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/resources/notify/conf/bootstrap.conf b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/notify/conf/bootstrap.conf new file mode 100644 index 0000000000..3125a17919 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/notify/conf/bootstrap.conf @@ -0,0 +1,74 @@ +# +# 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. +# + +# Java command to use when running NiFi +java=java + +# Username to use when running NiFi. This value will be ignored on Windows. +run.as= + +# Configure where NiFi's lib and conf directories live + lib.dir=./lib +conf.dir=./conf + +# How long to wait after telling NiFi to shutdown before explicitly killing the Process +graceful.shutdown.seconds=20 + +# Disable JSR 199 so that we can use JSP's without running a JDK +java.arg.1=-Dorg.apache.jasper.compiler.disablejsr199=true + +# JVM memory settings +java.arg.2=-Xms1024m +java.arg.3=-Xmx1024m + +# Enable Remote Debugging +java.arg.debug=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000 + +java.arg.4=-Djava.net.preferIPv4Stack=true + +# allowRestrictedHeaders is required for Cluster/Node communications to work properly +java.arg.5=-Dsun.net.http.allowRestrictedHeaders=true +java.arg.6=-Djava.protocol.handler.pkgs=sun.net.www.protocol + +# The G1GC is still considered experimental but has proven to be very advantageous in providing great +# performance without significant "stop-the-world" delays. +java.arg.13=-XX:+UseG1GC + +#Set headless mode by default +java.arg.14=-Djava.awt.headless=true + +# Master key in hexadecimal format for encrypted sensitive configuration values +nifi.bootstrap.sensitive.key= + +### +# Notification Services for notifying interested parties when NiFi is stopped, started, dies +### + +# XML File that contains the definitions of the notification services + notification.services.file=./conf/bootstrap-notification-services.xml + +# In the case that we are unable to send a notification for an event, how many times should we retry? +notification.max.attempts=5 + +# Comma-separated list of identifiers that are present in the notification.services.file; which services should be used to notify when NiFi is started? +#nifi.start.notification.services=email-notification + +# Comma-separated list of identifiers that are present in the notification.services.file; which services should be used to notify when NiFi is stopped? +#nifi.stop.notification.services=email-notification + +# Comma-separated list of identifiers that are present in the notification.services.file; which services should be used to notify when NiFi dies? +#nifi.dead.notification.services=email-notification diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/resources/notify/conf/nifi-secured.properties b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/notify/conf/nifi-secured.properties new file mode 100644 index 0000000000..e498c75c9f --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/notify/conf/nifi-secured.properties @@ -0,0 +1,107 @@ +# 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. + +# core properties # +nifi.flow.configuration.file=./conf/flow.xml.gz +nifi.flow.configuration.archive.dir=./conf/archive/ +nifi.task.configuration.file=./conf/reporting-tasks.xml +nifi.service.configuration.file=./conf/controller-services.xml +nifi.database.directory=./database_repository +nifi.flowfile.repository.directory=./flowfile_repository +nifi.flowfile.repository.partitions=4096 +nifi.flowfile.repository.checkpoint.millis=120000 +nifi.content.repository.directory.default=./content_repository +nifi.provenance.repository.capacity=25000 +nifi.templates.directory=./conf/templates +nifi.version=nifi 0.2.1-SNAPSHOT +nifi.ui.banner.text=DEFAULT BANNER +nifi.ui.autorefresh.interval.seconds=30 +nifi.flowcontroller.autoStartProcessors=true +nifi.flowcontroller.schedulestrategy=delay +nifi.flowcontroller.minimum.nanoseconds=1000000 +nifi.flowcontroller.graceful.shutdown.seconds=10 +nifi.nar.library.directory=./lib +nifi.nar.working.directory=./work/nar/ +nifi.flowservice.writedelay.seconds=2 +nifi.sensitive.props.key=REPLACE_ME +nifi.sensitive.props.algorithm=PBEWITHMD5AND256BITAES-CBC-OPENSSL +nifi.sensitive.props.provider=BC +nifi.h2.repository.maxmemoryrows=100000 +nifi.h2.url.append=;LOCK_TIMEOUT=25000;WRITE_DELAY=0;AUTO_SERVER=FALSE +nifi.h2.max.connections=20 +nifi.h2.login.timeout=500 +#For testing purposes. Default value should actually be empty! +nifi.remote.input.socket.port=5000 +nifi.remote.input.secure=true + +# web properties # +nifi.web.war.directory=./lib +nifi.web.http.host= +nifi.web.http.port= +nifi.web.https.host= +nifi.web.https.port=5050 +nifi.web.jetty.working.directory=./work/jetty + +# security properties # +nifi.security.keystore=target/tmp/keys/localhost/keystore.jks +nifi.security.keystoreType=JKS +nifi.security.keystorePasswd=badKeyPass +nifi.security.keyPasswd=badKeyPass +nifi.security.truststore=target/tmp/keys/localhost/truststore.jks +nifi.security.truststoreType=JKS +nifi.security.truststorePasswd=badTrustPass +nifi.security.needClientAuth=true +nifi.security.user.authorizer= + +# cluster common properties (cluster manager and nodes must have same values) # +nifi.cluster.protocol.heartbeat.tick.seconds=10 +nifi.cluster.protocol.is.secure=true +nifi.cluster.protocol.socket.timeout.ms=30000 +nifi.cluster.protocol.connection.handshake.timeout.seconds=45 +# if multicast is used, then nifi.cluster.protocol.multicast.xxx properties must be configured # +nifi.cluster.protocol.use.multicast=false +nifi.cluster.protocol.multicast.address= +nifi.cluster.protocol.multicast.port= +nifi.cluster.protocol.multicast.service.broadcast.delay.ms=500 +nifi.cluster.protocol.multicast.service.locator.attempts=3 +nifi.cluster.protocol.multicast.service.locator.attempts.delay.seconds=1 +#For testing purposes. Default value should actually be empty! +nifi.cluster.remote.input.socket.port=5000 +nifi.cluster.remote.input.secure=true + +# cluster node properties (only configure for cluster nodes) # +nifi.cluster.is.node=false +nifi.cluster.node.address= +nifi.cluster.node.protocol.port= +nifi.cluster.node.protocol.threads=2 +# if multicast is not used, nifi.cluster.node.unicast.xxx must have same values as nifi.cluster.manager.xxx # +nifi.cluster.node.unicast.manager.address= +nifi.cluster.node.unicast.manager.protocol.port= +nifi.cluster.node.unicast.manager.authority.provider.port= + +# cluster manager properties (only configure for cluster manager) # +nifi.cluster.is.manager=true +nifi.cluster.manager.address=localhost +nifi.cluster.manager.protocol.port=3030 +nifi.cluster.manager.authority.provider.port=4040 +nifi.cluster.manager.authority.provider.threads=10 +nifi.cluster.manager.node.firewall.file= +nifi.cluster.manager.node.event.history.size=10 +nifi.cluster.manager.node.api.connection.timeout.ms=30000 +nifi.cluster.manager.node.api.read.timeout.ms=30000 +nifi.cluster.manager.node.api.request.threads=10 +nifi.cluster.manager.flow.retrieval.delay.seconds=5 +nifi.cluster.manager.protocol.threads=10 +nifi.cluster.manager.safemode.seconds=0 diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/resources/notify/conf/nifi.properties b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/notify/conf/nifi.properties new file mode 100644 index 0000000000..0841c290d6 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/notify/conf/nifi.properties @@ -0,0 +1,204 @@ +# 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. + +# Core Properties # +nifi.version=1.2.0-SNAPSHOT +nifi.flow.configuration.file=./conf/flow.xml.gz +nifi.flow.configuration.archive.enabled=true +nifi.flow.configuration.archive.dir=./conf/archive/ +nifi.flow.configuration.archive.max.time=30 days +nifi.flow.configuration.archive.max.storage=500 MB +nifi.flowcontroller.autoResumeState=true +nifi.flowcontroller.graceful.shutdown.period=10 sec +nifi.flowservice.writedelay.interval=500 ms +nifi.administrative.yield.duration=30 sec +# If a component has no work to do (is "bored"), how long should we wait before checking again for work? +nifi.bored.yield.duration=10 millis + +nifi.authorizer.configuration.file=./conf/authorizers.xml +nifi.login.identity.provider.configuration.file=./conf/login-identity-providers.xml +nifi.templates.directory=./conf/templates +nifi.ui.banner.text= +nifi.ui.autorefresh.interval=30 sec +nifi.nar.library.directory=./lib +nifi.nar.working.directory=./work/nar/ +nifi.documentation.working.directory=./work/docs/components + +#################### +# State Management # +#################### +nifi.state.management.configuration.file=./conf/state-management.xml +# The ID of the local state provider +nifi.state.management.provider.local=local-provider +# The ID of the cluster-wide state provider. This will be ignored if NiFi is not clustered but must be populated if running in a cluster. +nifi.state.management.provider.cluster=zk-provider +# Specifies whether or not this instance of NiFi should run an embedded ZooKeeper server +nifi.state.management.embedded.zookeeper.start=false +# Properties file that provides the ZooKeeper properties to use if is set to true +nifi.state.management.embedded.zookeeper.properties=./conf/zookeeper.properties + + +# H2 Settings +nifi.database.directory=./database_repository +nifi.h2.url.append=;LOCK_TIMEOUT=25000;WRITE_DELAY=0;AUTO_SERVER=FALSE + +# FlowFile Repository +nifi.flowfile.repository.implementation=org.apache.nifi.controller.repository.WriteAheadFlowFileRepository +nifi.flowfile.repository.directory=./flowfile_repository +nifi.flowfile.repository.partitions=256 +nifi.flowfile.repository.checkpoint.interval=2 mins +nifi.flowfile.repository.always.sync=false + +nifi.swap.manager.implementation=org.apache.nifi.controller.FileSystemSwapManager +nifi.queue.swap.threshold=20000 +nifi.swap.in.period=5 sec +nifi.swap.in.threads=1 +nifi.swap.out.period=5 sec +nifi.swap.out.threads=4 + +# Content Repository +nifi.content.repository.implementation=org.apache.nifi.controller.repository.FileSystemRepository +nifi.content.claim.max.appendable.size=10 MB +nifi.content.claim.max.flow.files=100 +nifi.content.repository.directory.default=./content_repository +nifi.content.repository.archive.max.retention.period=12 hours +nifi.content.repository.archive.max.usage.percentage=50% +nifi.content.repository.archive.enabled=true +nifi.content.repository.always.sync=false +nifi.content.viewer.url=/nifi-content-viewer/ + +# Provenance Repository Properties +nifi.provenance.repository.implementation=org.apache.nifi.provenance.PersistentProvenanceRepository + +# Persistent Provenance Repository Properties +nifi.provenance.repository.directory.default=./provenance_repository +nifi.provenance.repository.max.storage.time=24 hours +nifi.provenance.repository.max.storage.size=1 GB +nifi.provenance.repository.rollover.time=30 secs +nifi.provenance.repository.rollover.size=100 MB +nifi.provenance.repository.query.threads=2 +nifi.provenance.repository.index.threads=1 +nifi.provenance.repository.compress.on.rollover=true +nifi.provenance.repository.always.sync=false +nifi.provenance.repository.journal.count=16 +# Comma-separated list of fields. Fields that are not indexed will not be searchable. Valid fields are: +# EventType, FlowFileUUID, Filename, TransitURI, ProcessorID, AlternateIdentifierURI, Relationship, Details +nifi.provenance.repository.indexed.fields=EventType, FlowFileUUID, Filename, ProcessorID, Relationship +# FlowFile Attributes that should be indexed and made searchable. Some examples to consider are filename, uuid, mime.type +nifi.provenance.repository.indexed.attributes= +# Large values for the shard size will result in more Java heap usage when searching the Provenance Repository +# but should provide better performance +nifi.provenance.repository.index.shard.size=500 MB +# Indicates the maximum length that a FlowFile attribute can be when retrieving a Provenance Event from +# the repository. If the length of any attribute exceeds this value, it will be truncated when the event is retrieved. +nifi.provenance.repository.max.attribute.length=65536 + +# Volatile Provenance Respository Properties +nifi.provenance.repository.buffer.size=100000 + +# Component Status Repository +nifi.components.status.repository.implementation=org.apache.nifi.controller.status.history.VolatileComponentStatusRepository +nifi.components.status.repository.buffer.size=1440 +nifi.components.status.snapshot.frequency=1 min + +# Site to Site properties +nifi.remote.input.host=localhost +nifi.remote.input.secure=false +nifi.remote.input.socket.port=8090 +nifi.remote.input.http.enabled=true +nifi.remote.input.http.transaction.ttl=30 sec + +# web properties # +nifi.web.war.directory=./lib +nifi.web.http.host= +nifi.web.http.port=8080 +nifi.web.https.host= +nifi.web.https.port= +nifi.web.jetty.working.directory=./work/jetty +nifi.web.jetty.threads=200 + +# security properties # +nifi.sensitive.props.key= +nifi.sensitive.props.key.protected= +nifi.sensitive.props.algorithm=PBEWITHMD5AND256BITAES-CBC-OPENSSL +nifi.sensitive.props.provider=BC +nifi.sensitive.props.additional.keys= + +nifi.security.keystore= +nifi.security.keystoreType= +nifi.security.keystorePasswd= +nifi.security.keyPasswd= +nifi.security.truststore= +nifi.security.truststoreType= +nifi.security.truststorePasswd= +nifi.security.needClientAuth= +nifi.security.user.authorizer=file-provider +nifi.security.user.login.identity.provider= +nifi.security.ocsp.responder.url= +nifi.security.ocsp.responder.certificate= + +# Identity Mapping Properties # +# These properties allow normalizing user identities such that identities coming from different identity providers +# (certificates, LDAP, Kerberos) can be treated the same internally in NiFi. The following example demonstrates normalizing +# DNs from certificates and principals from Kerberos into a common identity string: +# +# nifi.security.identity.mapping.pattern.dn=^CN=(.*?), OU=(.*?), O=(.*?), L=(.*?), ST=(.*?), C=(.*?)$ +# nifi.security.identity.mapping.value.dn=$1@$2 +# nifi.security.identity.mapping.pattern.kerb=^(.*?)/instance@(.*?)$ +# nifi.security.identity.mapping.value.kerb=$1@$2 + +# cluster common properties (all nodes must have same values) # +nifi.cluster.protocol.heartbeat.interval=5 sec +nifi.cluster.protocol.is.secure=false + +# cluster node properties (only configure for cluster nodes) # +nifi.cluster.is.node=true +nifi.cluster.node.address= +nifi.cluster.node.protocol.port= +nifi.cluster.node.protocol.threads=10 +nifi.cluster.node.event.history.size=25 +nifi.cluster.node.connection.timeout=5 sec +nifi.cluster.node.read.timeout=5 sec +nifi.cluster.firewall.file= +nifi.cluster.flow.election.max.wait.time=5 mins +nifi.cluster.flow.election.max.candidates= + +# zookeeper properties, used for cluster management # +nifi.zookeeper.connect.string= +nifi.zookeeper.connect.timeout=3 secs +nifi.zookeeper.session.timeout=3 secs +nifi.zookeeper.root.node=/nifi + +# kerberos # +nifi.kerberos.krb5.file= + +# kerberos service principal # +nifi.kerberos.service.principal= +nifi.kerberos.service.keytab.location= + +# kerberos spnego principal # +nifi.kerberos.spnego.principal= +nifi.kerberos.spnego.keytab.location= +nifi.kerberos.spnego.authentication.expiration=12 hours + +# external properties files for variable registry +# supports a comma delimited list of file locations +nifi.variable.registry.properties= + +# Build info +nifi.build.tag=HEAD +nifi.build.branch=master +nifi.build.revision=868795c +nifi.build.timestamp=2016-12-13T15:41:36Z diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/resources/overlay.properties b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/overlay.properties new file mode 100644 index 0000000000..2084bf3663 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/overlay.properties @@ -0,0 +1,41 @@ +# +# 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. +# + +# This properties file specifies how to update the provided nifi.properties + +# Comma separated list of properties to put the hostname into +hostname.properties= \ + nifi.remote.input.host, \ + nifi.web.https.host, \ + nifi.cluster.node.address + +# Comma separated list of properties to increment (must also be defined in this file) +incrementing.properties= \ + nifi.web.https.port, \ + nifi.remote.input.socket.port, \ + nifi.cluster.node.protocol.port + +nifi.web.https.port=9443 +nifi.remote.input.socket.port=10443 +nifi.cluster.node.protocol.port=11443 + +# Properties to set verbatim +nifi.remote.input.secure=true +nifi.cluster.protocol.is.secure=true + +nifi.web.http.host= +nifi.web.http.port= \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/resources/upgrade/conf/bootstrap.conf b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/upgrade/conf/bootstrap.conf new file mode 100644 index 0000000000..744bfe915d --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/upgrade/conf/bootstrap.conf @@ -0,0 +1,21 @@ +# +# 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. +# + + +# JVM memory settings +java.arg.2=-Xms512m +java.arg.3=-Xmx512m diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/resources/upgrade/conf/login-identity-providers.xml b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/upgrade/conf/login-identity-providers.xml new file mode 100644 index 0000000000..7666152865 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/upgrade/conf/login-identity-providers.xml @@ -0,0 +1,112 @@ + + + + + + + + + + \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/resources/upgrade/conf/nifi.properties b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/upgrade/conf/nifi.properties new file mode 100644 index 0000000000..520599d531 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/upgrade/conf/nifi.properties @@ -0,0 +1,28 @@ +# +# 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. +# +#upgrade test properties +nifi.cluster.is.node= +nifi.cluster.node.address= +nifi.cluster.node.protocol.port= +nifi.cluster.node.protocol.threads= +nifi.cluster.node.event.history.size= +nifi.cluster.node.connection.timeout= +nifi.cluster.node.read.timeout= +nifi.cluster.firewall.file= +nifi.cluster.flow.election.max.wait.time= +nifi.cluster.flow.election.max.candidates= +nifi.cluster.a.new.variable= \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/resources/upgrade/lib/nifi-framework-nar-1.2.0.nar b/nifi-toolkit/nifi-toolkit-admin/src/test/resources/upgrade/lib/nifi-framework-nar-1.2.0.nar new file mode 100644 index 0000000000000000000000000000000000000000..08faed30e2b7f44e3df0f468a43877148a6c5ee9 GIT binary patch literal 1385 zcmWIWW@h1H0DS@G#7_c^c6M#9tXj7y^LWL>M@L zFbYkZ5VAHsm*Du|lKi4nkbaQf9;p7eGpzXz83?$f_i}ri=xN?9V8}~wJ;}L)fi--= z_lt_R1UHEU1*kSvPWl?wRi7#2&v4_zj`#*1g@e7uc9U+1wX8Ecr`RIk)KQzfuH;Uc zv{Q$y(R}{abtT2p^VzM+l8s_tc$shC7{5Sy?h7CBU6;fP-tp2zX@u z*aKb`tA4$BoNkd*Mj*C*@)e^^IA1JeWvW_1B+#*NYf%|ApP&M<^49%x{; zXlr3{oGEG$`vT-^c8)KS*Y}tJL!knQ5$V7%2{rkMAg2RgM?X(D*WeI6UpG)n$OQ(* zJPAmE`1T*nYjO~1d#|$R@V)~%5eo#rx7`Yvozyu?t}0W_;DuYnnROR_eT!AR68C@R z&u71ludGt4Zsj)$+ZttbkokUe$tu0X!~k*ilVy>7o)-Sg{w$CxZ#wI`$w0uEBZ*@k zpVE{hwZ%U!e-8?t6XO5O@oe_5mpZfAlwWdn&raDcRGnIz-_2&WG{w)uF?GYj_t)k) zF1{UiZLL~FsoP8CoGsPsxSLXYuN{i+UfQj?dc!u8qJLXCD;a+0&U%0Sefxf<7uT+z zf0@Nq_C%zu*`uW}s8jHB(yen-bIwkD%-Svdard0dhArV*?0-unHtNa=(Fo>!5bgPG0|X8m=v1Uep`d_yB3&H<%bd`2KU=dWV{ zvJn_wLzso|8gf#UMe*7OtY)F)26SISj0L4* org.apache.nifi nifi-toolkit-s2s + + org.apache.nifi + nifi-toolkit-admin + org.apache.nifi nifi-toolkit-zookeeper-migrator diff --git a/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/node-manager.bat b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/node-manager.bat new file mode 100644 index 0000000000..4c94ce5d37 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/node-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=%~sdp0..\classpath;%~sdp0..\lib + +SET JAVA_PARAMS=-cp %LIB_DIR%\* -Xms12m -Xmx24m %JAVA_ARGS% org.apache.nifi.toolkit.admin.nodemanager.NodeManagerTool + +cmd.exe /C ""%JAVA_EXE%" %JAVA_PARAMS% %* "" + diff --git a/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/node-manager.sh b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/node-manager.sh new file mode 100644 index 0000000000..c80d9667bf --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/node-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="$(cygpath --path --windows "${LIBS}")" + else + 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.nodemanager.NodeManagerTool "$@" + return $? +} + + +init "$1" +run "$@" \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/notify.bat b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/notify.bat new file mode 100644 index 0000000000..191519f194 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/notify.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=%~sdp0..\classpath;%~sdp0..\lib + +SET JAVA_PARAMS=-cp %LIB_DIR%\* -Xms12m -Xmx24m %JAVA_ARGS% org.apache.nifi.toolkit.admin.notify.NotificationTool + +cmd.exe /C ""%JAVA_EXE%" %JAVA_PARAMS% %* "" + diff --git a/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/notify.sh b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/notify.sh new file mode 100644 index 0000000000..746c78ae55 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/notify.sh @@ -0,0 +1,120 @@ +#!/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="$(cygpath --path --windows "${LIBS}")" + else + 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.notify.NotificationTool "$@" + return $? +} + + +init "$1" +run "$@" \ No newline at end of file diff --git a/nifi-toolkit/pom.xml b/nifi-toolkit/pom.xml index 75661c7884..393415c07d 100644 --- a/nifi-toolkit/pom.xml +++ b/nifi-toolkit/pom.xml @@ -20,12 +20,16 @@ nifi 1.2.0-SNAPSHOT + + 1.2.0-SNAPSHOT + nifi-toolkit pom nifi-toolkit-tls nifi-toolkit-encrypt-config nifi-toolkit-s2s + nifi-toolkit-admin nifi-toolkit-zookeeper-migrator nifi-toolkit-flowfile-repo nifi-toolkit-assembly diff --git a/pom.xml b/pom.xml index a735e13ea3..58fa379dde 100644 --- a/pom.xml +++ b/pom.xml @@ -936,6 +936,11 @@ nifi-toolkit-zookeeper-migrator 1.2.0-SNAPSHOT + + org.apache.nifi + nifi-toolkit-admin + 1.2.0-SNAPSHOT + org.apache.nifi nifi-registry-service