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 <[email protected]>


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/c0f0462e
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/c0f0462e
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/c0f0462e

Branch: refs/heads/master
Commit: c0f0462e8bc829d5e4ac2415a9fa77d3f5d6bdc3
Parents: ba2bdf8
Author: Yolanda M. Davis <[email protected]>
Authored: Tue Feb 7 10:28:15 2017 -0500
Committer: Andy LoPresto <[email protected]>
Committed: Mon Apr 24 23:08:37 2017 -0700

----------------------------------------------------------------------
 .../nifi/web/api/entity/BulletinEntity.java     |   2 +
 .../nifi/authorization/FileAuthorizer.java      |   4 +
 .../nifi/cluster/manager/BulletinMerger.java    |  12 +-
 .../org/apache/nifi/web/NiFiServiceFacade.java  |  11 +
 .../nifi/web/StandardNiFiServiceFacade.java     |   7 +
 .../apache/nifi/web/api/ControllerResource.java |  68 +++
 .../web/StandardNiFiServiceFacadeSpec.groovy    |  56 ++-
 nifi-toolkit/nifi-toolkit-admin/pom.xml         | 186 +++++++++
 .../nifi/toolkit/admin/AbstractAdminTool.groovy | 110 +++++
 .../toolkit/admin/client/ClientFactory.groovy   |  27 ++
 .../admin/client/NiFiClientFactory.groovy       | 172 ++++++++
 .../toolkit/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 ++
 .../resources/conf/login-identity-providers.xml | 112 +++++
 .../src/test/resources/conf/nifi.properties     |  28 ++
 .../test/resources/external/conf/bootstrap.conf |  32 ++
 .../external/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
 .../resources/filemanager/nifi-test-archive.zip | Bin 0 -> 32415 bytes
 .../test/resources/filemanager/nifi.properties  |  32 ++
 .../resources/lib/nifi-framework-nar-1.2.0.nar  | Bin 0 -> 1385 bytes
 .../test/resources/no_rules/conf/bootstrap.conf |  21 +
 .../no_rules/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 +++++
 .../test/resources/notify/conf/nifi.properties  | 204 +++++++++
 .../src/test/resources/overlay.properties       |  41 ++
 .../test/resources/upgrade/conf/bootstrap.conf  |  21 +
 .../upgrade/conf/login-identity-providers.xml   | 112 +++++
 .../test/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(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/c0f0462e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/BulletinEntity.java
----------------------------------------------------------------------
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 9e93a24..83f45d1 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;

http://git-wip-us.apache.org/repos/asf/nifi/blob/c0f0462e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-file-authorizer/src/main/java/org/apache/nifi/authorization/FileAuthorizer.java
----------------------------------------------------------------------
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 9a310a2..c7440e2 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);

http://git-wip-us.apache.org/repos/asf/nifi/blob/c0f0462e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/BulletinMerger.java
----------------------------------------------------------------------
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 79b1447..952edab 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<BulletinEntity> entities = Lists.newArrayList();
+
+        final Map<String,List<BulletinEntity>> 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;
     }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/c0f0462e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
----------------------------------------------------------------------
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 039cbf8..6f9ea98 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
     // ----------------------------------------

http://git-wip-us.apache.org/repos/asf/nifi/blob/c0f0462e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
----------------------------------------------------------------------
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 4179745..b9b208e 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) {

http://git-wip-us.apache.org/repos/asf/nifi/blob/c0f0462e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.java
----------------------------------------------------------------------
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 98400f2..cb87ca2 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
     // -------------------

http://git-wip-us.apache.org/repos/asf/nifi/blob/c0f0462e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/groovy/org/apache/nifi/web/StandardNiFiServiceFacadeSpec.groovy
----------------------------------------------------------------------
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 677b25d..29ab83a 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)
     }

http://git-wip-us.apache.org/repos/asf/nifi/blob/c0f0462e/nifi-toolkit/nifi-toolkit-admin/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-toolkit/nifi-toolkit-admin/pom.xml 
b/nifi-toolkit/nifi-toolkit-admin/pom.xml
new file mode 100644
index 0000000..37500c6
--- /dev/null
+++ b/nifi-toolkit/nifi-toolkit-admin/pom.xml
@@ -0,0 +1,186 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+    <parent>
+        <groupId>org.apache.nifi</groupId>
+        <artifactId>nifi-toolkit</artifactId>
+        <version>1.2.0-SNAPSHOT</version>
+    </parent>
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>nifi-toolkit-admin</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>commons-cli</groupId>
+            <artifactId>commons-cli</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-toolkit-tls</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.jersey</groupId>
+            <artifactId>jersey-client</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-client-dto</artifactId>
+            <version>${client.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-properties</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-properties-loader</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>ch.qos.logback</groupId>
+                    <artifactId>logback-classic</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-security-utils</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.codehaus.jackson</groupId>
+            <artifactId>jackson-mapper-asl</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.jaxrs</groupId>
+            <artifactId>jackson-jaxrs-json-provider</artifactId>
+            <version>RELEASE</version>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.jersey</groupId>
+            <artifactId>jersey-bundle</artifactId>
+            <version>RELEASE</version>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.jersey</groupId>
+            <artifactId>jersey-json</artifactId>
+            <version>RELEASE</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-compress</artifactId>
+        </dependency>
+        <!-- Spock testing dependencies-->
+        <dependency>
+            <groupId>com.github.stefanbirkner</groupId>
+            <artifactId>system-rules</artifactId>
+            <version>1.16.0</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.spockframework</groupId>
+            <artifactId>spock-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>cglib</groupId>
+            <artifactId>cglib-nodep</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+    </dependencies>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>compile</goal>
+                            <goal>testCompile</goal>
+                        </goals>
+                        <configuration>
+                            <compilerId>groovy-eclipse-compiler</compilerId>
+                        </configuration>
+                    </execution>
+                </executions>
+                <configuration>
+                    <source>1.8</source>
+                    <target>1.8</target>
+                </configuration>
+                <dependencies>
+                    <dependency>
+                        <groupId>org.codehaus.groovy</groupId>
+                        <artifactId>groovy-eclipse-compiler</artifactId>
+                        <version>2.9.2-01</version>
+                    </dependency>
+                    <dependency>
+                        <groupId>org.codehaus.groovy</groupId>
+                        <artifactId>groovy-eclipse-batch</artifactId>
+                        <version>2.4.3-01</version>
+                    </dependency>
+                </dependencies>
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>build-helper-maven-plugin</artifactId>
+                <version>1.5</version>
+                <executions>
+                    <execution>
+                        <id>add-source</id>
+                        <phase>generate-sources</phase>
+                        <goals>
+                            <goal>add-source</goal>
+                        </goals>
+                        <configuration>
+                            <sources>
+                                <source>src/main/groovy</source>
+                            </sources>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>add-test-source</id>
+                        <phase>generate-test-sources</phase>
+                        <goals>
+                            <goal>add-test-source</goal>
+                        </goals>
+                        <configuration>
+                            <sources>
+                                <source>src/test/groovy</source>
+                            </sources>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.rat</groupId>
+                <artifactId>apache-rat-plugin</artifactId>
+                <configuration>
+                    <excludes combine.children="append">
+                        <exclude>src/test/resources/filemanager/myid</exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/c0f0462e/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/AbstractAdminTool.groovy
----------------------------------------------------------------------
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 0000000..aed3027
--- /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)
+    }
+
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/c0f0462e/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/client/ClientFactory.groovy
----------------------------------------------------------------------
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 0000000..960ac6a
--- /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

http://git-wip-us.apache.org/repos/asf/nifi/blob/c0f0462e/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/client/NiFiClientFactory.groovy
----------------------------------------------------------------------
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 0000000..5c0333a
--- /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<String> getSubjectAlternativeNames(final X509Certificate 
certificate) throws CertificateParsingException {
+            final Collection<List<?>> altNames = 
certificate.getSubjectAlternativeNames()
+
+            if (altNames == null) {
+                return new ArrayList<>()
+            }
+
+            final List<String> 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
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/c0f0462e/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/client/NiFiClientUtil.groovy
----------------------------------------------------------------------
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 0000000..d4e5ff6
--- /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<String> 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<String> getActiveClusterUrls(final Client client, 
NiFiProperties niFiProperties){
+
+        final ClusterEntity clusterEntity = getCluster(client, niFiProperties, 
Lists.newArrayList())
+        final List<NodeDTO> activeNodes = clusterEntity.cluster.nodes.findAll{ 
it.status == "CONNECTED" }
+        final List<String> 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)
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/c0f0462e/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/nodemanager/NodeManagerTool.groovy
----------------------------------------------------------------------
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 0000000..3a1c4df
--- /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<NodeDTO> 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<String> 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<String> 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<String> 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<String> 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)
+    }
+
+
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/c0f0462e/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/notify/NotificationTool.groovy
----------------------------------------------------------------------
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 0000000..ce87499
--- /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)
+    }
+
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/c0f0462e/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/util/AdminUtil.groovy
----------------------------------------------------------------------
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 0000000..9dc0090
--- /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","")
+
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/c0f0462e/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/util/Version.groovy
----------------------------------------------------------------------
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 0000000..db5dc04
--- /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> VERSION_COMPARATOR = new 
Comparator<Version>() {
+        @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)
+    }
+}

Reply via email to