AMBARI-18587. Post user creation hook. (Laszlo Puskas via stoader)
Project: http://git-wip-us.apache.org/repos/asf/ambari/repo Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/a5fdae80 Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/a5fdae80 Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/a5fdae80 Branch: refs/heads/branch-2.5 Commit: a5fdae802210ae1f8d4fed2234f1651cbe61c2b5 Parents: 7ba1495 Author: Laszlo Puskas <lpus...@hortonworks.com> Authored: Mon Nov 14 18:43:53 2016 +0100 Committer: Toader, Sebastian <stoa...@hortonworks.com> Committed: Mon Nov 14 18:44:26 2016 +0100 ---------------------------------------------------------------------- ambari-server/src/main/assemblies/server.xml | 8 + .../server/configuration/Configuration.java | 17 ++ .../server/controller/ControllerModule.java | 42 ++- .../ambari/server/events/AmbariEvent.java | 12 +- .../ambari/server/hooks/AmbariEventFactory.java | 33 +++ .../apache/ambari/server/hooks/HookContext.java | 26 ++ .../ambari/server/hooks/HookContextFactory.java | 44 +++ .../apache/ambari/server/hooks/HookService.java | 36 +++ .../users/PostUserCreationHookContext.java | 55 ++++ .../server/hooks/users/UserCreatedEvent.java | 45 +++ .../server/hooks/users/UserHookParams.java | 49 ++++ .../server/hooks/users/UserHookService.java | 279 +++++++++++++++++++ .../server/security/authorization/Users.java | 119 +++++--- .../serveraction/AbstractServerAction.java | 2 +- .../server/serveraction/ServerAction.java | 4 +- .../users/CollectionPersisterService.java | 46 +++ .../CollectionPersisterServiceFactory.java | 24 ++ .../users/CsvFilePersisterService.java | 103 +++++++ .../users/PostUserCreationHookServerAction.java | 163 +++++++++++ .../users/ShellCommandCallableFactory.java | 26 ++ .../users/ShellCommandUtilityCallable.java | 48 ++++ .../users/ShellCommandUtilityWrapper.java | 57 ++++ .../server/topology/AsyncCallableService.java | 25 +- .../ambari/server/utils/ShellCommandUtil.java | 2 +- .../scripts/post-user-creation-hook.sh | 133 +++++++++ .../ActiveWidgetLayoutResourceProviderTest.java | 4 + .../StackUpgradeConfigurationMergeTest.java | 5 + .../UserAuthorizationResourceProviderTest.java | 4 + .../internal/UserResourceProviderTest.java | 4 + .../server/hooks/users/UserHookServiceTest.java | 224 +++++++++++++++ .../AmbariAuthorizationFilterTest.java | 4 + ...uthenticationProviderForDNWithSpaceTest.java | 28 +- .../security/authorization/UsersTest.java | 10 + .../PostUserCreationHookServerActionTest.java | 182 ++++++++++++ .../cluster/ClusterEffectiveVersionTest.java | 5 +- .../topology/AsyncCallableServiceTest.java | 59 ++-- .../server/upgrade/UpgradeCatalog240Test.java | 33 +++ 37 files changed, 1875 insertions(+), 85 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/assemblies/server.xml ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/assemblies/server.xml b/ambari-server/src/main/assemblies/server.xml index b6e0723..9a193a9 100644 --- a/ambari-server/src/main/assemblies/server.xml +++ b/ambari-server/src/main/assemblies/server.xml @@ -126,6 +126,9 @@ <fileSet> <directory>src/main/resources/scripts</directory> <outputDirectory>/var/lib/ambari-server/resources/scripts</outputDirectory> + <excludes> + <exclude>post-user-creation-hook.sh</exclude> + </excludes> </fileSet> <fileSet> <directory>${ambari-admin-dir}/target</directory> @@ -336,6 +339,11 @@ <source>${basedir}/target/version</source> <outputDirectory>/var/lib/ambari-server/resources</outputDirectory> </file> + <file> + <fileMode>755</fileMode> + <source>src/main/resources/scripts/post-user-creation-hook.sh</source> + <outputDirectory>/var/lib/ambari-server/resources/scripts</outputDirectory> + </file> </files> <dependencySets> <dependencySet> http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java b/ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java index 45e2ea3..8d0dcce 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java @@ -2408,6 +2408,14 @@ public class Configuration { public static final ConfigurationProperty<Boolean> ACTIVE_INSTANCE = new ConfigurationProperty<>( "active.instance", Boolean.TRUE); + @Markdown(description = "Indicates whether the post user creation is enabled or not. By default is false.") + public static final ConfigurationProperty<Boolean> POST_USER_CREATION_HOOK_ENABLED = new ConfigurationProperty<>( + "ambari.post.user.creation.hook.enabled", Boolean.FALSE); + + @Markdown(description = "The location of the post user creation hook on the ambari server hosting machine.") + public static final ConfigurationProperty<String> POST_USER_CREATION_HOOK = new ConfigurationProperty<>( + "ambari.post.user.creation.hook", "/var/lib/ambari-server/resources/scripts/post-user-creation-hook.sh"); + /** * PropertyConfigurator checks log4j.properties file change every LOG4JMONITOR_DELAY milliseconds. */ @@ -5028,6 +5036,15 @@ public class Configuration { } /** + * Indicates whether feature for user hook execution is enabled or not. + * + * @return true / false (defaults to false) + */ + public boolean isUserHookEnabled() { + return Boolean.parseBoolean(getProperty(POST_USER_CREATION_HOOK_ENABLED)); + } + + /** * @return the number of threads to use for parallel topology task creation if enabled */ public int getParallelTopologyTaskCreationThreadPoolSize() { http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/controller/ControllerModule.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/controller/ControllerModule.java b/ambari-server/src/main/java/org/apache/ambari/server/controller/ControllerModule.java index f593fc4..2c4bf5f 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/controller/ControllerModule.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/controller/ControllerModule.java @@ -62,6 +62,14 @@ import org.apache.ambari.server.controller.metrics.timeline.cache.TimelineMetric import org.apache.ambari.server.controller.metrics.timeline.cache.TimelineMetricCacheProvider; import org.apache.ambari.server.controller.spi.ResourceProvider; import org.apache.ambari.server.controller.utilities.KerberosChecker; +import org.apache.ambari.server.events.AmbariEvent; +import org.apache.ambari.server.hooks.AmbariEventFactory; +import org.apache.ambari.server.hooks.HookContext; +import org.apache.ambari.server.hooks.HookContextFactory; +import org.apache.ambari.server.hooks.HookService; +import org.apache.ambari.server.hooks.users.PostUserCreationHookContext; +import org.apache.ambari.server.hooks.users.UserCreatedEvent; +import org.apache.ambari.server.hooks.users.UserHookService; import org.apache.ambari.server.metadata.CachedRoleCommandOrderProvider; import org.apache.ambari.server.metadata.RoleCommandOrderProvider; import org.apache.ambari.server.notifications.DispatchFactory; @@ -80,6 +88,10 @@ import org.apache.ambari.server.security.authorization.AuthorizationHelper; import org.apache.ambari.server.security.encryption.CredentialStoreService; import org.apache.ambari.server.security.encryption.CredentialStoreServiceImpl; import org.apache.ambari.server.serveraction.kerberos.KerberosOperationHandlerFactory; +import org.apache.ambari.server.serveraction.users.CollectionPersisterService; +import org.apache.ambari.server.serveraction.users.CollectionPersisterServiceFactory; +import org.apache.ambari.server.serveraction.users.CsvFilePersisterService; +import org.apache.ambari.server.serveraction.users.ShellCommandCallableFactory; import org.apache.ambari.server.stack.StackManagerFactory; import org.apache.ambari.server.stageplanner.RoleGraphFactory; import org.apache.ambari.server.state.Cluster; @@ -343,14 +355,14 @@ public class ControllerModule extends AbstractModule { // Host role commands status summary max cache enable/disable bindConstant().annotatedWith(Names.named(HostRoleCommandDAO.HRC_STATUS_SUMMARY_CACHE_ENABLED)). - to(configuration.getHostRoleCommandStatusSummaryCacheEnabled()); + to(configuration.getHostRoleCommandStatusSummaryCacheEnabled()); // Host role commands status summary max cache size bindConstant().annotatedWith(Names.named(HostRoleCommandDAO.HRC_STATUS_SUMMARY_CACHE_SIZE)). - to(configuration.getHostRoleCommandStatusSummaryCacheSize()); + to(configuration.getHostRoleCommandStatusSummaryCacheSize()); // Host role command status summary cache expiry duration in minutes bindConstant().annotatedWith(Names.named(HostRoleCommandDAO.HRC_STATUS_SUMMARY_CACHE_EXPIRY_DURATION_MINUTES)). - to(configuration.getHostRoleCommandStatusSummaryCacheExpiryDuration()); + to(configuration.getHostRoleCommandStatusSummaryCacheExpiryDuration()); bind(AmbariManagementController.class).to( AmbariManagementControllerImpl.class); @@ -374,6 +386,7 @@ public class ControllerModule extends AbstractModule { bindByAnnotation(null); bindNotificationDispatchers(null); registerUpgradeChecks(null); + bind(HookService.class).to(UserHookService.class); } // ----- helper methods ---------------------------------------------------- @@ -423,7 +436,7 @@ public class ControllerModule extends AbstractModule { install(new FactoryModuleBuilder().implement( Host.class, HostImpl.class).build(HostFactory.class)); install(new FactoryModuleBuilder().implement( - Service.class, ServiceImpl.class).build(ServiceFactory.class)); + Service.class, ServiceImpl.class).build(ServiceFactory.class)); install(new FactoryModuleBuilder() .implement(ResourceProvider.class, Names.named("host"), HostResourceProvider.class) @@ -439,8 +452,8 @@ public class ControllerModule extends AbstractModule { .build(ResourceProviderFactory.class)); install(new FactoryModuleBuilder().implement( - ServiceComponent.class, ServiceComponentImpl.class).build( - ServiceComponentFactory.class)); + ServiceComponent.class, ServiceComponentImpl.class).build( + ServiceComponentFactory.class)); install(new FactoryModuleBuilder().implement( ServiceComponentHost.class, ServiceComponentHostImpl.class).build( ServiceComponentHostFactory.class)); @@ -449,7 +462,7 @@ public class ControllerModule extends AbstractModule { install(new FactoryModuleBuilder().implement( ConfigGroup.class, ConfigGroupImpl.class).build(ConfigGroupFactory.class)); install(new FactoryModuleBuilder().implement(RequestExecution.class, - RequestExecutionImpl.class).build(RequestExecutionFactory.class)); + RequestExecutionImpl.class).build(RequestExecutionFactory.class)); bind(StageFactory.class).to(StageFactoryImpl.class); bind(RoleCommandOrderProvider.class).to(CachedRoleCommandOrderProvider.class); @@ -464,6 +477,11 @@ public class ControllerModule extends AbstractModule { bind(HostRoleCommandFactory.class).to(HostRoleCommandFactoryImpl.class); bind(SecurityHelper.class).toInstance(SecurityHelperImpl.getInstance()); bind(BlueprintFactory.class); + + install(new FactoryModuleBuilder().implement(AmbariEvent.class, Names.named("userCreated"), UserCreatedEvent.class).build(AmbariEventFactory.class)); + install(new FactoryModuleBuilder().implement(HookContext.class, PostUserCreationHookContext.class).build(HookContextFactory.class)); + install(new FactoryModuleBuilder().implement(CollectionPersisterService.class, CsvFilePersisterService.class).build(CollectionPersisterServiceFactory.class)); + } /** @@ -482,11 +500,9 @@ public class ControllerModule extends AbstractModule { * * @param beanDefinitions the set of bean definitions. If it is empty or * {@code null} scan will occur. - * * @return the set of bean definitions that was found during scan if - * {@code beanDefinitions} was null or empty. Else original - * {@code beanDefinitions} will be returned. - * + * {@code beanDefinitions} was null or empty. Else original + * {@code beanDefinitions} will be returned. */ // Method is protected and returns a set of bean definitions for testing convenience. @SuppressWarnings("unchecked") @@ -575,11 +591,11 @@ public class ControllerModule extends AbstractModule { if (null == beanDefinitions || beanDefinitions.isEmpty()) { ClassPathScanningCandidateComponentProvider scanner = - new ClassPathScanningCandidateComponentProvider(false); + new ClassPathScanningCandidateComponentProvider(false); // match all implementations of the dispatcher interface AssignableTypeFilter filter = new AssignableTypeFilter( - NotificationDispatcher.class); + NotificationDispatcher.class); scanner.addIncludeFilter(filter); http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/events/AmbariEvent.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/events/AmbariEvent.java b/ambari-server/src/main/java/org/apache/ambari/server/events/AmbariEvent.java index 1d3ec39..7ec5972 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/events/AmbariEvent.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/events/AmbariEvent.java @@ -125,7 +125,17 @@ public abstract class AmbariEvent { /** * Cluster configuration changed. */ - CLUSTER_CONFIG_CHANGED; + CLUSTER_CONFIG_CHANGED, + + /** + * Metrics Collector force refresh needed. + */ + METRICS_COLLECTOR_HOST_DOWN, + + /** + * Local user has been created. + */ + USER_CREATED; } /** http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/hooks/AmbariEventFactory.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/hooks/AmbariEventFactory.java b/ambari-server/src/main/java/org/apache/ambari/server/hooks/AmbariEventFactory.java new file mode 100644 index 0000000..57cbd37 --- /dev/null +++ b/ambari-server/src/main/java/org/apache/ambari/server/hooks/AmbariEventFactory.java @@ -0,0 +1,33 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ambari.server.hooks; + +import org.apache.ambari.server.events.AmbariEvent; + +import com.google.inject.name.Named; + +/** + * Factory interface definition for AmbariEvent implementations. + * Instances created using this interface are managed by the IoC (GUICE) framework. + */ +public interface AmbariEventFactory { + + @Named("userCreated") + AmbariEvent newUserCreatedEvent(HookContext context); +} http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/hooks/HookContext.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/hooks/HookContext.java b/ambari-server/src/main/java/org/apache/ambari/server/hooks/HookContext.java new file mode 100644 index 0000000..03bcc52 --- /dev/null +++ b/ambari-server/src/main/java/org/apache/ambari/server/hooks/HookContext.java @@ -0,0 +1,26 @@ +/** + * 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.ambari.server.hooks; + +/** + * Marks a context of a hook implementation. + */ +public interface HookContext { + +} http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/hooks/HookContextFactory.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/hooks/HookContextFactory.java b/ambari-server/src/main/java/org/apache/ambari/server/hooks/HookContextFactory.java new file mode 100644 index 0000000..0a56d85 --- /dev/null +++ b/ambari-server/src/main/java/org/apache/ambari/server/hooks/HookContextFactory.java @@ -0,0 +1,44 @@ +/** + * 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.ambari.server.hooks; + +import java.util.Map; +import java.util.Set; + +/** + * Factory interface definition to control creation of HookContext implementation. + * The stateless factory interface makes possible to leverage the IoC framework in managing instances. + */ +public interface HookContextFactory { + /** + * Factory method for HookContext implementations. + * + * @param userName the username to be inferred to the instance being created + * @return a HookContext instance + */ + HookContext createUserHookContext(String userName); + + /** + * Factory method for BatchUserHookContext instances. + * + * @param userGroups a map with userNames as keys and group list as values. + * @return a new BatchUserHookContext instance + */ + HookContext createBatchUserHookContext(Map<String, Set<String>> userGroups); +} http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/hooks/HookService.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/hooks/HookService.java b/ambari-server/src/main/java/org/apache/ambari/server/hooks/HookService.java new file mode 100644 index 0000000..f51ee1f --- /dev/null +++ b/ambari-server/src/main/java/org/apache/ambari/server/hooks/HookService.java @@ -0,0 +1,36 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.ambari.server.hooks; + +/** + * Interface defining a contract for hook services. Hook services are responsible for executing additional logic / hooks that can be tied + * to different events or steps in the application. + */ +public interface HookService { + + /** + * Entrypoint for the hook logic. + * + * @param hookContext the context on which the hook logic is to be executed + * + * @return true if the hook gets triggered, false otherwise + */ + boolean execute(HookContext hookContext); +} http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/hooks/users/PostUserCreationHookContext.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/hooks/users/PostUserCreationHookContext.java b/ambari-server/src/main/java/org/apache/ambari/server/hooks/users/PostUserCreationHookContext.java new file mode 100644 index 0000000..c9e2f74 --- /dev/null +++ b/ambari-server/src/main/java/org/apache/ambari/server/hooks/users/PostUserCreationHookContext.java @@ -0,0 +1,55 @@ +/** + * 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.ambari.server.hooks.users; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.apache.ambari.server.hooks.HookContext; + +import com.google.inject.assistedinject.Assisted; +import com.google.inject.assistedinject.AssistedInject; + +public class PostUserCreationHookContext implements HookContext { + private Map<String, Set<String>> userGroups = new HashMap<>(); + + @AssistedInject + public PostUserCreationHookContext(@Assisted Map<String, Set<String>> userGroups) { + this.userGroups = userGroups; + } + + @AssistedInject + public PostUserCreationHookContext(@Assisted String userName) { + userGroups.put(userName, Collections.<String>emptySet()); + } + + + public Map<String, Set<String>> getUserGroups() { + return userGroups; + } + + @Override + public String toString() { + return "BatchUserHookContext{" + + "userGroups=" + userGroups + + '}'; + } +} http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/hooks/users/UserCreatedEvent.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/hooks/users/UserCreatedEvent.java b/ambari-server/src/main/java/org/apache/ambari/server/hooks/users/UserCreatedEvent.java new file mode 100644 index 0000000..8e914dc --- /dev/null +++ b/ambari-server/src/main/java/org/apache/ambari/server/hooks/users/UserCreatedEvent.java @@ -0,0 +1,45 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ambari.server.hooks.users; + +import javax.inject.Inject; + +import org.apache.ambari.server.events.AmbariEvent; +import org.apache.ambari.server.hooks.HookContext; + +import com.google.inject.assistedinject.Assisted; +import com.google.inject.assistedinject.AssistedInject; + +/** + * Event signaling a user creation. + */ +public class UserCreatedEvent extends AmbariEvent { + + private HookContext context; + + @AssistedInject + public UserCreatedEvent(@Assisted HookContext context) { + super(AmbariEventType.USER_CREATED); + this.context = context; + } + + public HookContext getContext() { + return context; + } +} http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/hooks/users/UserHookParams.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/hooks/users/UserHookParams.java b/ambari-server/src/main/java/org/apache/ambari/server/hooks/users/UserHookParams.java new file mode 100644 index 0000000..6970dcc --- /dev/null +++ b/ambari-server/src/main/java/org/apache/ambari/server/hooks/users/UserHookParams.java @@ -0,0 +1,49 @@ +/** + * 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.ambari.server.hooks.users; + +/** + * Command parameter identifier list for the post user creation hook. + */ +public enum UserHookParams { + + SCRIPT("hook-script"), + // the payload the hook operates on + PAYLOAD("cmd-payload"), + + CLUSTER_ID("cluster-id"), + CLUSTER_NAME("cluster-name"), + CMD_TIME_FRAME("cmd-timeframe"), + CMD_INPUT_FILE("cmd-input-file"), + // identify security related values + CLUSTER_SECURITY_TYPE("cluster-security-type"), + CMD_HDFS_PRINCIPAL("cmd-hdfs-principal"), + CMD_HDFS_KEYTAB("cmd-hdfs-keytab"); + + + private String param; + + UserHookParams(String param) { + this.param = param; + } + + public String param() { + return param; + } +} http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/hooks/users/UserHookService.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/hooks/users/UserHookService.java b/ambari-server/src/main/java/org/apache/ambari/server/hooks/users/UserHookService.java new file mode 100644 index 0000000..c4ff1e4 --- /dev/null +++ b/ambari-server/src/main/java/org/apache/ambari/server/hooks/users/UserHookService.java @@ -0,0 +1,279 @@ +/** + * 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.ambari.server.hooks.users; + +import java.io.File; +import java.io.IOException; +import java.util.Calendar; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.apache.ambari.server.AmbariException; +import org.apache.ambari.server.Role; +import org.apache.ambari.server.RoleCommand; +import org.apache.ambari.server.actionmanager.ActionManager; +import org.apache.ambari.server.actionmanager.RequestFactory; +import org.apache.ambari.server.actionmanager.Stage; +import org.apache.ambari.server.actionmanager.StageFactory; +import org.apache.ambari.server.configuration.Configuration; +import org.apache.ambari.server.controller.internal.RequestStageContainer; +import org.apache.ambari.server.events.publishers.AmbariEventPublisher; +import org.apache.ambari.server.hooks.AmbariEventFactory; +import org.apache.ambari.server.hooks.HookContext; +import org.apache.ambari.server.hooks.HookService; +import org.apache.ambari.server.serveraction.users.PostUserCreationHookServerAction; +import org.apache.ambari.server.state.Cluster; +import org.apache.ambari.server.state.Clusters; +import org.apache.ambari.server.state.svccomphost.ServiceComponentHostServerActionEvent; +import org.codehaus.jackson.map.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.eventbus.Subscribe; + +/** + * Service in charge for handling user initialization related logic. + * It's expected that this implementation encapsulates all the logic around the user initialization hook: + * 1. validates the context (all the input is available) + * 2. checks if prerequisites are satisfied for the hook execution + * 3. triggers the hook execution flow + * 4. executes the flow (on a separate thread) + */ +@Singleton +public class UserHookService implements HookService { + + private static final Logger LOGGER = LoggerFactory.getLogger(UserHookService.class); + + private static final String POST_USER_CREATION_REQUEST_CONTEXT = "Post user creation hook for [ %s ] users"; + private static final String INPUT_FILE_PREFIX = "user_hook_input_%s.csv"; + + // constants for getting security related properties + private static final String HADOOP_ENV = "hadoop-env"; + private static final String HDFS_USER_KEYTAB = "hdfs_user_keytab"; + private static final String HDFS_PRINCIPAL_NAME = "hdfs_principal_name"; + + @Inject + private AmbariEventFactory eventFactory; + + @Inject + private AmbariEventPublisher ambariEventPublisher; + + @Inject + private ActionManager actionManager; + + @Inject + private RequestFactory requestFactory; + + @Inject + private StageFactory stageFactory; + + @Inject + private Configuration configuration; + + @Inject + private Clusters clusters; + + @Inject + private ObjectMapper objectMapper; + + // executed by the IoC framework after creating the object (guice) + @Inject + private void register() { + ambariEventPublisher.register(this); + } + + @Override + public boolean execute(HookContext hookContext) { + LOGGER.info("Executing user hook for {}. ", hookContext); + + PostUserCreationHookContext hookCtx = validateHookInput(hookContext); + + if (!checkUserHookPrerequisites()) { + LOGGER.warn("Prerequisites for user hook are not satisfied. Hook not triggered"); + return false; + } + + if (hookCtx.getUserGroups().isEmpty()) { + LOGGER.info("No users found for executing user hook for"); + return false; + } + + UserCreatedEvent userCreatedEvent = (UserCreatedEvent) eventFactory.newUserCreatedEvent(hookCtx); + + LOGGER.info("Triggering user hook for user: {}", hookContext); + ambariEventPublisher.publish(userCreatedEvent); + + return true; + } + + @Subscribe + public void onUserCreatedEvent(UserCreatedEvent event) throws AmbariException { + LOGGER.info("Preparing hook execution for event: {}", event); + + try { + RequestStageContainer requestStageContainer = new RequestStageContainer(actionManager.getNextRequestId(), null, requestFactory, actionManager); + ClusterData clsData = getClusterData(); + + PostUserCreationHookContext ctx = (PostUserCreationHookContext) event.getContext(); + + String stageContextText = String.format(POST_USER_CREATION_REQUEST_CONTEXT, ctx.getUserGroups().size()); + + Stage stage = stageFactory.createNew(requestStageContainer.getId(), configuration.getServerTempDir() + File.pathSeparatorChar + requestStageContainer.getId(), clsData.getClusterName(), + clsData.getClusterId(), stageContextText, "{}", "{}", "{}"); + stage.setStageId(requestStageContainer.getLastStageId()); + + ServiceComponentHostServerActionEvent serverActionEvent = new ServiceComponentHostServerActionEvent("ambari-server-host", System.currentTimeMillis()); + Map<String, String> commandParams = prepareCommandParams(ctx, clsData); + + stage.addServerActionCommand(PostUserCreationHookServerAction.class.getName(), "ambari", Role.AMBARI_SERVER_ACTION, + RoleCommand.EXECUTE, clsData.getClusterName(), serverActionEvent, commandParams, stageContextText, null, null, false, false); + + requestStageContainer.addStages(Collections.singletonList(stage)); + requestStageContainer.persist(); + + } catch (IOException e) { + LOGGER.error("Failed to assemble stage for server action. Event: {}", event); + throw new AmbariException("Failed to assemble stage for server action", e); + } + + } + + private Map<String, String> prepareCommandParams(PostUserCreationHookContext context, ClusterData clusterData) throws IOException { + + Map<String, String> commandParams = new HashMap<>(); + + commandParams.put(UserHookParams.SCRIPT.param(), configuration.getProperty(Configuration.POST_USER_CREATION_HOOK)); + + commandParams.put(UserHookParams.CLUSTER_ID.param(), String.valueOf(clusterData.getClusterId())); + commandParams.put(UserHookParams.CLUSTER_NAME.param(), clusterData.getClusterName()); + commandParams.put(UserHookParams.CLUSTER_SECURITY_TYPE.param(), clusterData.getSecurityType()); + + commandParams.put(UserHookParams.CMD_HDFS_KEYTAB.param(), clusterData.getKeytab()); + commandParams.put(UserHookParams.CMD_HDFS_PRINCIPAL.param(), clusterData.getPrincipal()); + commandParams.put(UserHookParams.CMD_INPUT_FILE.param(), generateInputFileName()); + + commandParams.put(UserHookParams.PAYLOAD.param(), objectMapper.writeValueAsString(context.getUserGroups())); + + return commandParams; + } + + private String generateInputFileName() { + String inputFileName = String.format(INPUT_FILE_PREFIX, Calendar.getInstance().getTimeInMillis()); + LOGGER.debug("Command input file name: {}", inputFileName); + + return configuration.getServerTempDir() + File.separator + inputFileName; + } + + private boolean checkUserHookPrerequisites() { + + if (!configuration.isUserHookEnabled()) { + LOGGER.warn("Post user creation hook disabled."); + return false; + } + + if (clusters.getClusters().isEmpty()) { + LOGGER.warn("There's no cluster found. Post user creation hook won't be executed."); + return false; + } + + return true; + } + + private PostUserCreationHookContext validateHookInput(HookContext hookContext) { + // perform any other validation steps, such as existence of fields etc... + return (PostUserCreationHookContext) hookContext; + } + + private ClusterData getClusterData() { + //default value for unsecure clusters + String keyTab = "NA"; + String principal = "NA"; + + // cluster data is needed multiple times during the stage creation, cached it locally ... + Map.Entry<String, Cluster> clustersMapEntry = clusters.getClusters().entrySet().iterator().next(); + + Cluster cluster = clustersMapEntry.getValue(); + + switch (cluster.getSecurityType()) { + case KERBEROS: + // get the principal + Map<String, String> hadoopEnv = cluster.getDesiredConfigByType(HADOOP_ENV).getProperties(); + keyTab = hadoopEnv.get(HDFS_USER_KEYTAB); + principal = hadoopEnv.get(HDFS_PRINCIPAL_NAME); + break; + case NONE: + // break; let the flow enter the default case + default: + LOGGER.debug("The cluster security is not set. Security type: {}", cluster.getSecurityType()); + break; + } + + return new ClusterData(cluster.getClusterName(), cluster.getClusterId(), cluster.getSecurityType().name(), principal, keyTab); + } + + private void getSecurityData(Configuration configuraiton) { + //principal + + //keytab + } + + /** + * Local representation of cluster data. + */ + private static final class ClusterData { + private String clusterName; + private Long clusterId; + private String securityType; + private String principal; + private String keytab; + + public ClusterData(String clusterName, Long clusterId, String securityType, String principal, String keytab) { + this.clusterName = clusterName; + this.clusterId = clusterId; + this.securityType = securityType; + this.principal = principal; + this.keytab = keytab; + } + + public String getClusterName() { + return clusterName; + } + + public Long getClusterId() { + return clusterId; + } + + public String getSecurityType() { + return securityType; + } + + public String getPrincipal() { + return principal; + } + + public String getKeytab() { + return keytab; + } + } + +} http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/Users.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/Users.java b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/Users.java index 5c45306..fb61449 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/Users.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/Users.java @@ -17,11 +17,23 @@ */ package org.apache.ambari.server.security.authorization; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.inject.Inject; import javax.persistence.EntityManager; import org.apache.ambari.server.AmbariException; import org.apache.ambari.server.configuration.Configuration; +import org.apache.ambari.server.hooks.HookContextFactory; +import org.apache.ambari.server.hooks.HookService; import org.apache.ambari.server.orm.dao.GroupDAO; import org.apache.ambari.server.orm.dao.MemberDAO; import org.apache.ambari.server.orm.dao.PermissionDAO; @@ -47,7 +59,6 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.password.PasswordEncoder; -import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import com.google.inject.persist.Transactional; @@ -83,7 +94,13 @@ public class Users { @Inject protected Configuration configuration; @Inject - private AmbariLdapAuthenticationProvider ldapAuthenticationProvider; + private AmbariLdapAuthenticationProvider ldapAuthenticationProvider; + + @Inject + private Provider<HookService> hookServiceProvider; + + @Inject + private HookContextFactory hookContextFactory; public List<User> getAllUsers() { List<UserEntity> userEntities = userDAO.findAll(); @@ -98,6 +115,7 @@ public class Users { /** * This method works incorrectly, userName is not unique if users have different types + * * @return One user. Priority is LOCAL -> LDAP -> JWT */ @Deprecated @@ -125,6 +143,7 @@ public class Users { /** * Retrieves User then userName is unique in users DB. Will return null if there no user with provided userName or * there are some users with provided userName but with different types. + * * @param userName * @return User if userName is unique in DB, null otherwise */ @@ -147,6 +166,7 @@ public class Users { /** * Modifies password of local user + * * @throws AmbariException */ public synchronized void modifyPassword(String userName, String currentUserPassword, String newPassword) throws AmbariException { @@ -166,14 +186,14 @@ public class Users { try { ldapAuthenticationProvider.authenticate( new UsernamePasswordAuthenticationToken(currentUserName, currentUserPassword)); - isLdapUser = true; + isLdapUser = true; } catch (InvalidUsernamePasswordCombinationException ex) { throw new AmbariException(ex.getMessage()); } } boolean isCurrentUserAdmin = false; - for (PrivilegeEntity privilegeEntity: currentUserEntity.getPrincipal().getPrivileges()) { + for (PrivilegeEntity privilegeEntity : currentUserEntity.getPrincipal().getPrivileges()) { if (privilegeEntity.getPermission().getPermissionName().equals(PermissionEntity.AMBARI_ADMINISTRATOR_PERMISSION_NAME)) { isCurrentUserAdmin = true; break; @@ -270,8 +290,8 @@ public class Users { * @param userName user name * @param password password * @param userType user type - * @param active is user active - * @param admin is user admin + * @param active is user active + * @param admin is user admin * @throws AmbariException if user already exists */ public synchronized void createUser(String userName, String password, UserType userType, Boolean active, Boolean @@ -319,14 +339,17 @@ public class Users { if (admin != null && admin) { grantAdminPrivilege(userEntity.getUserId()); } + + // execute user initialization hook if required () + hookServiceProvider.get().execute(hookContextFactory.createUserHookContext(userName)); } public synchronized void removeUser(User user) throws AmbariException { UserEntity userEntity = userDAO.findByPK(user.getUserId()); if (userEntity != null) { - if (!isUserCanBeRemoved(userEntity)){ + if (!isUserCanBeRemoved(userEntity)) { throw new AmbariException("Could not remove user " + userEntity.getUserName() + - ". System should have at least one administrator."); + ". System should have at least one administrator."); } userDAO.remove(userEntity); } else { @@ -357,12 +380,12 @@ public class Users { return null; } else { final Set<User> users = new HashSet<User>(); - for (MemberEntity memberEntity: groupEntity.getMemberEntities()) { + for (MemberEntity memberEntity : groupEntity.getMemberEntities()) { if (memberEntity.getUser() != null) { users.add(new User(memberEntity.getUser())); } else { LOG.error("Wrong state, not found user for member '{}' (group: '{}')", - memberEntity.getMemberId(), memberEntity.getGroup().getGroupName()); + memberEntity.getMemberId(), memberEntity.getGroup().getGroupName()); } } return users; @@ -402,7 +425,7 @@ public class Users { final List<GroupEntity> groupEntities = groupDAO.findAll(); final List<Group> groups = new ArrayList<Group>(groupEntities.size()); - for (GroupEntity groupEntity: groupEntities) { + for (GroupEntity groupEntity : groupEntities) { groups.add(new Group(groupEntity)); } @@ -421,7 +444,7 @@ public class Users { if (groupEntity == null) { throw new AmbariException("Group " + groupName + " doesn't exist"); } - for (MemberEntity member: groupEntity.getMemberEntities()) { + for (MemberEntity member : groupEntity.getMemberEntities()) { members.add(member.getUser().getUserName()); } return members; @@ -463,7 +486,7 @@ public class Users { */ public synchronized void revokeAdminPrivilege(Integer userId) { final UserEntity user = userDAO.findByPK(userId); - for (PrivilegeEntity privilege: user.getPrincipal().getPrivileges()) { + for (PrivilegeEntity privilege : user.getPrincipal().getPrivileges()) { if (privilege.getPermission().getPermissionName().equals(PermissionEntity.AMBARI_ADMINISTRATOR_PERMISSION_NAME)) { user.getPrincipal().getPrivileges().remove(privilege); principalDAO.merge(user.getPrincipal()); //explicit merge for Derby support @@ -518,7 +541,7 @@ public class Users { if (isUserInGroup(userEntity, groupEntity)) { MemberEntity memberEntity = null; - for (MemberEntity entity: userEntity.getMemberEntities()) { + for (MemberEntity entity : userEntity.getMemberEntities()) { if (entity.getGroup().equals(groupEntity)) { memberEntity = entity; break; @@ -541,7 +564,7 @@ public class Users { * @param userEntity user to be checked * @return true if user can be removed */ - public synchronized boolean isUserCanBeRemoved(UserEntity userEntity){ + public synchronized boolean isUserCanBeRemoved(UserEntity userEntity) { List<PrincipalEntity> adminPrincipals = principalDAO.findByPermissionId(PermissionEntity.AMBARI_ADMINISTRATOR_PERMISSION); Set<UserEntity> userEntitysSet = new HashSet<UserEntity>(userDAO.findUsersByPrincipal(adminPrincipals)); return (userEntitysSet.contains(userEntity) && userEntitysSet.size() < 2) ? false : true; @@ -550,12 +573,12 @@ public class Users { /** * Performs a check if given user belongs to given group. * - * @param userEntity user entity + * @param userEntity user entity * @param groupEntity group entity * @return true if user presents in group */ private boolean isUserInGroup(UserEntity userEntity, GroupEntity groupEntity) { - for (MemberEntity memberEntity: userEntity.getMemberEntities()) { + for (MemberEntity memberEntity : userEntity.getMemberEntities()) { if (memberEntity.getGroup().equals(groupEntity)) { return true; } @@ -574,11 +597,11 @@ public class Users { // prefetch all user and group data to avoid heavy queries in membership creation - for (UserEntity userEntity: userDAO.findAll()) { + for (UserEntity userEntity : userDAO.findAll()) { allUsers.put(userEntity.getUserName(), userEntity); } - for (GroupEntity groupEntity: groupDAO.findAll()) { + for (GroupEntity groupEntity : groupDAO.findAll()) { allGroups.put(groupEntity.getGroupName(), groupEntity); } @@ -589,7 +612,7 @@ public class Users { // remove users final Set<UserEntity> usersToRemove = new HashSet<UserEntity>(); - for (String userName: batchInfo.getUsersToBeRemoved()) { + for (String userName : batchInfo.getUsersToBeRemoved()) { UserEntity userEntity = userDAO.findUserByName(userName); if (userEntity == null) { continue; @@ -601,7 +624,7 @@ public class Users { // remove groups final Set<GroupEntity> groupsToRemove = new HashSet<GroupEntity>(); - for (String groupName: batchInfo.getGroupsToBeRemoved()) { + for (String groupName : batchInfo.getGroupsToBeRemoved()) { final GroupEntity groupEntity = groupDAO.findGroupByName(groupName); allGroups.remove(groupEntity.getGroupName()); groupsToRemove.add(groupEntity); @@ -610,7 +633,7 @@ public class Users { // update users final Set<UserEntity> usersToBecomeLdap = new HashSet<UserEntity>(); - for (String userName: batchInfo.getUsersToBecomeLdap()) { + for (String userName : batchInfo.getUsersToBecomeLdap()) { UserEntity userEntity = userDAO.findLocalUserByName(userName); if (userEntity == null) { userEntity = userDAO.findLdapUserByName(userName); @@ -626,7 +649,7 @@ public class Users { // update groups final Set<GroupEntity> groupsToBecomeLdap = new HashSet<GroupEntity>(); - for (String groupName: batchInfo.getGroupsToBecomeLdap()) { + for (String groupName : batchInfo.getGroupsToBecomeLdap()) { final GroupEntity groupEntity = groupDAO.findGroupByName(groupName); groupEntity.setLdapGroup(true); allGroups.put(groupEntity.getGroupName(), groupEntity); @@ -639,7 +662,7 @@ public class Users { // prepare create users final Set<UserEntity> usersToCreate = new HashSet<UserEntity>(); - for (String userName: batchInfo.getUsersToBeCreated()) { + for (String userName : batchInfo.getUsersToBeCreated()) { final PrincipalEntity principalEntity = new PrincipalEntity(); principalEntity.setPrincipalType(userPrincipalType); principalsToCreate.add(principalEntity); @@ -656,7 +679,7 @@ public class Users { // prepare create groups final Set<GroupEntity> groupsToCreate = new HashSet<GroupEntity>(); - for (String groupName: batchInfo.getGroupsToBeCreated()) { + for (String groupName : batchInfo.getGroupsToBeCreated()) { final PrincipalEntity principalEntity = new PrincipalEntity(); principalEntity.setPrincipalType(groupPrincipalType); principalsToCreate.add(principalEntity); @@ -678,7 +701,7 @@ public class Users { // create membership final Set<MemberEntity> membersToCreate = new HashSet<MemberEntity>(); final Set<GroupEntity> groupsToUpdate = new HashSet<GroupEntity>(); - for (LdapUserGroupMemberDto member: batchInfo.getMembershipToAdd()) { + for (LdapUserGroupMemberDto member : batchInfo.getMembershipToAdd()) { final MemberEntity memberEntity = new MemberEntity(); final GroupEntity groupEntity = allGroups.get(member.getGroupName()); memberEntity.setGroup(groupEntity); @@ -692,7 +715,7 @@ public class Users { // remove membership final Set<MemberEntity> membersToRemove = new HashSet<MemberEntity>(); - for (LdapUserGroupMemberDto member: batchInfo.getMembershipToRemove()) { + for (LdapUserGroupMemberDto member : batchInfo.getMembershipToRemove()) { MemberEntity memberEntity = memberDAO.findByUserAndGroup(member.getUserName(), member.getGroupName()); if (memberEntity != null) { membersToRemove.add(memberEntity); @@ -702,6 +725,36 @@ public class Users { // clear cached entities entityManagerProvider.get().getEntityManagerFactory().getCache().evictAll(); + + if (!usersToCreate.isEmpty()) { + // entry point in the hook logic + hookServiceProvider.get().execute(hookContextFactory.createBatchUserHookContext(getUsersToGroupMap(usersToCreate))); + } + + } + + /** + * Assembles a map where the keys are usernames and values are Lists with groups associated with users. + * + * @param usersToCreate a list with user entities + * @return the a populated map instance + */ + private Map<String, Set<String>> getUsersToGroupMap(Set<UserEntity> usersToCreate) { + Map<String, Set<String>> usersToGroups = new HashMap<>(); + + for (UserEntity userEntity : usersToCreate) { + + // make sure user entities are refreshed so that membership is updated + userEntity = userDAO.findByPK(userEntity.getUserId()); + + usersToGroups.put(userEntity.getUserName(), new HashSet<String>()); + + for (MemberEntity memberEntity : userEntity.getMemberEntities()) { + usersToGroups.get(userEntity.getUserName()).add(memberEntity.getGroup().getGroupName()); + } + } + + return usersToGroups; } /** @@ -740,10 +793,9 @@ public class Users { List<PrivilegeEntity> implicitPrivilegeEntities = getImplicitPrivileges(explicitPrivilegeEntities); List<PrivilegeEntity> privilegeEntities; - if(implicitPrivilegeEntities.isEmpty()) { + if (implicitPrivilegeEntities.isEmpty()) { privilegeEntities = explicitPrivilegeEntities; - } - else { + } else { privilegeEntities = new LinkedList<PrivilegeEntity>(); privilegeEntities.addAll(explicitPrivilegeEntities); privilegeEntities.addAll(implicitPrivilegeEntities); @@ -782,10 +834,9 @@ public class Users { List<PrivilegeEntity> implicitPrivilegeEntities = getImplicitPrivileges(explicitPrivilegeEntities); List<PrivilegeEntity> privilegeEntities; - if(implicitPrivilegeEntities.isEmpty()) { + if (implicitPrivilegeEntities.isEmpty()) { privilegeEntities = explicitPrivilegeEntities; - } - else { + } else { privilegeEntities = new LinkedList<PrivilegeEntity>(); privilegeEntities.addAll(explicitPrivilegeEntities); privilegeEntities.addAll(implicitPrivilegeEntities); http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/serveraction/AbstractServerAction.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/AbstractServerAction.java b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/AbstractServerAction.java index ca4a92c..c1f2aaf 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/AbstractServerAction.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/AbstractServerAction.java @@ -35,7 +35,7 @@ import com.google.inject.Inject; import com.google.inject.Injector; /** - * AbstractServerActionImpl is an abstract implementation of a ServerAction. + * AbstractServerAction is an abstract implementation of a ServerAction. * <p/> * This abstract implementation provides common facilities for all ServerActions, such as * maintaining the ExecutionCommand and HostRoleCommand properties. It also provides a convenient http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/serveraction/ServerAction.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/ServerAction.java b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/ServerAction.java index 7c69f52..4137ed1 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/ServerAction.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/ServerAction.java @@ -30,8 +30,8 @@ import java.util.concurrent.ConcurrentMap; */ public interface ServerAction { - public static final String ACTION_NAME = "ACTION_NAME"; - public static final String ACTION_USER_NAME = "ACTION_USER_NAME"; + String ACTION_NAME = "ACTION_NAME"; + String ACTION_USER_NAME = "ACTION_USER_NAME"; /** * The default timeout (in seconds) to use for potentially long running tasks such as creating http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/CollectionPersisterService.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/CollectionPersisterService.java b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/CollectionPersisterService.java new file mode 100644 index 0000000..85f9671 --- /dev/null +++ b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/CollectionPersisterService.java @@ -0,0 +1,46 @@ +/** + * 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.ambari.server.serveraction.users; + +import java.util.Collection; +import java.util.Map; + +/** + * Contract defining operations that persist collections of data. + */ +public interface CollectionPersisterService<K, V> { + + /** + * Persists the provided collection of data. + * + * @param collectionData the data to be persisted + * @return true if all the records persisted successfully, false otherwise. + */ + boolean persist(Collection<V> collectionData); + + + /** + * Persists the provided map of data. + * + * @param mapData the data to be persisted. + * @return true if all the records persisted successfully, false otherwise. + */ + boolean persistMap(Map<K, V> mapData); + +} http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/CollectionPersisterServiceFactory.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/CollectionPersisterServiceFactory.java b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/CollectionPersisterServiceFactory.java new file mode 100644 index 0000000..5906e30 --- /dev/null +++ b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/CollectionPersisterServiceFactory.java @@ -0,0 +1,24 @@ +/** + * 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.ambari.server.serveraction.users; + +public interface CollectionPersisterServiceFactory { + + CsvFilePersisterService createCsvFilePersisterService(String csvFile); +} http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/CsvFilePersisterService.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/CsvFilePersisterService.java b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/CsvFilePersisterService.java new file mode 100644 index 0000000..d8ffe98 --- /dev/null +++ b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/CsvFilePersisterService.java @@ -0,0 +1,103 @@ +/** + * 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.ambari.server.serveraction.users; + +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.inject.assistedinject.Assisted; +import com.google.inject.assistedinject.AssistedInject; + +@Singleton +public class CsvFilePersisterService implements CollectionPersisterService<String, List<String>> { + + private static final Logger LOGGER = LoggerFactory.getLogger(CsvFilePersisterService.class); + private String NEW_LINE_SEPARATOR = "\n"; + + private String csvFile; + private CSVPrinter csvPrinter; + private FileWriter fileWriter; + + @AssistedInject + public CsvFilePersisterService(@Assisted String csvFile) { + this.csvFile = csvFile; + } + + @Inject + public void init() throws IOException { + // make 3rd party dependencies be managed by the container (probably constructor binding or factory is needed) + fileWriter = new FileWriter(csvFile); + csvPrinter = new CSVPrinter(fileWriter, CSVFormat.DEFAULT.withRecordSeparator(NEW_LINE_SEPARATOR)); + } + + + @Override + public boolean persist(Collection<List<String>> collectionData) { + + try { + LOGGER.info("Persisting collection to csv file"); + + csvPrinter.printRecords(collectionData); + + LOGGER.info("Collection successfully persisted to csv file."); + + return true; + } catch (IOException e) { + LOGGER.error("Failed to persist the collection to csv file", e); + return false; + } finally { + try { + fileWriter.flush(); + fileWriter.close(); + csvPrinter.close(); + } catch (IOException e) { + LOGGER.error("Error while flushing/closing fileWriter/csvPrinter", e); + } + } + } + + @Override + public boolean persistMap(Map<String, List<String>> mapData) { + + LOGGER.info("Persisting map data to csv file"); + Collection<List<String>> collectionData = new ArrayList<>(); + + for (String key : mapData.keySet()) { + List<String> record = new ArrayList<>(); + record.add(key); + record.addAll(mapData.get(key)); + collectionData.add(record); + } + + return persist(collectionData); + } + +} http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/PostUserCreationHookServerAction.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/PostUserCreationHookServerAction.java b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/PostUserCreationHookServerAction.java new file mode 100644 index 0000000..e713128 --- /dev/null +++ b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/PostUserCreationHookServerAction.java @@ -0,0 +1,163 @@ +/** + * 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.ambari.server.serveraction.users; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.apache.ambari.server.AmbariException; +import org.apache.ambari.server.actionmanager.HostRoleStatus; +import org.apache.ambari.server.agent.CommandReport; +import org.apache.ambari.server.hooks.users.UserHookParams; +import org.apache.ambari.server.serveraction.AbstractServerAction; +import org.apache.ambari.server.topology.AsyncCallableService; +import org.apache.ambari.server.utils.ShellCommandUtil; +import org.codehaus.jackson.map.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Splitter; + +@Singleton +public class PostUserCreationHookServerAction extends AbstractServerAction { + private static final Logger LOGGER = LoggerFactory.getLogger(PostUserCreationHookServerAction.class); + private static final int MAX_SYMBOLS_PER_LOG_MESSAGE = 7900; + + @Inject + private ShellCommandUtilityWrapper shellCommandUtilityWrapper; + + @Inject + private ObjectMapper objectMapper; + + @Inject + private CollectionPersisterServiceFactory collectionPersisterServiceFactory; + + @Inject + public PostUserCreationHookServerAction() { + super(); + } + + @Override + public CommandReport execute(ConcurrentMap<String, Object> requestSharedDataContext) throws AmbariException, InterruptedException { + LOGGER.debug("Executing custom script server action; Context: {}", requestSharedDataContext); + ShellCommandUtil.Result result = null; + CommandReport cmdReport = null; + + try { + + Map<String, String> commandParams = getCommandParameters(); + validateCommandParams(commandParams); + + //persist user data to csv + CollectionPersisterService csvPersisterService = collectionPersisterServiceFactory.createCsvFilePersisterService(commandParams.get(UserHookParams.CMD_INPUT_FILE.param())); + csvPersisterService.persistMap(getPayload(commandParams)); + + String[] cmd = assembleCommand(commandParams); + + result = shellCommandUtilityWrapper.runCommand(cmd); + + // long command results need to be split to chunks to feed external log processors (eg.: syslog) + logCommandResult(Arrays.asList(cmd).toString(), result); + + cmdReport = createCommandReport(result.getExitCode(), result.isSuccessful() ? + HostRoleStatus.COMPLETED : HostRoleStatus.FAILED, "{}", result.getStdout(), result.getStderr()); + + LOGGER.debug("Command report: {}", cmdReport); + + + } catch (InterruptedException e) { + LOGGER.error("The server action thread has been interrupted", e); + throw e; + } catch (Exception e) { + LOGGER.error("Server action is about to quit due to an exception.", e); + throw new AmbariException("Server action execution failed to complete!", e); + } + + return cmdReport; + } + + private void logCommandResult(String command, ShellCommandUtil.Result result) { + LOGGER.info("Execution of command [ {} ] - {}", command, result.isSuccessful() ? "succeeded" : "failed"); + String stringToLog = result.isSuccessful() ? result.getStdout() : result.getStderr(); + if (stringToLog == null) stringToLog = ""; + List<String> logLines = Splitter.fixedLength(MAX_SYMBOLS_PER_LOG_MESSAGE).splitToList(stringToLog); + LOGGER.info("BEGIN - {} for command {}", result.isSuccessful() ? "stdout" : "stderr", command); + for (String line : logLines) { + LOGGER.info("command output *** : {}", line); + } + LOGGER.info("END - {} for command {}", result.isSuccessful() ? "stdout" : "stderr", command); + } + + + private String[] assembleCommand(Map<String, String> params) { + String[] cmdArray = new String[]{ + params.get(UserHookParams.SCRIPT.param()), + params.get(UserHookParams.CMD_INPUT_FILE.param()), + params.get(UserHookParams.CLUSTER_SECURITY_TYPE.param()), + params.get(UserHookParams.CMD_HDFS_PRINCIPAL.param()), + params.get(UserHookParams.CMD_HDFS_KEYTAB.param()) + }; + LOGGER.debug("Server action command to be executed: {}", cmdArray); + return cmdArray; + } + + /** + * Validates command parameters, throws exception in case required parameters are missing + */ + private void validateCommandParams(Map<String, String> commandParams) { + + LOGGER.info("Validating command parameters ..."); + + if (!commandParams.containsKey(UserHookParams.PAYLOAD.param())) { + LOGGER.error("Missing command parameter: {}; Failing the server action.", UserHookParams.PAYLOAD.param()); + throw new IllegalArgumentException("Missing command parameter: [" + UserHookParams.PAYLOAD.param() + "]"); + } + + if (!commandParams.containsKey(UserHookParams.SCRIPT.param())) { + LOGGER.error("Missing command parameter: {}; Failing the server action.", UserHookParams.SCRIPT.param()); + throw new IllegalArgumentException("Missing command parameter: [" + UserHookParams.SCRIPT.param() + "]"); + } + + if (!commandParams.containsKey(UserHookParams.CMD_INPUT_FILE.param())) { + LOGGER.error("Missing command parameter: {}; Failing the server action.", UserHookParams.CMD_INPUT_FILE.param()); + throw new IllegalArgumentException("Missing command parameter: [" + UserHookParams.CMD_INPUT_FILE.param() + "]"); + } + + if (!commandParams.containsKey(UserHookParams.CLUSTER_SECURITY_TYPE.param())) { + LOGGER.error("Missing command parameter: {}; Failing the server action.", UserHookParams.CLUSTER_SECURITY_TYPE.param()); + throw new IllegalArgumentException("Missing command parameter: [" + UserHookParams.CLUSTER_SECURITY_TYPE.param() + "]"); + } + + LOGGER.info("Command parameter validation passed."); + } + + private Map<String, List<String>> getPayload(Map<String, String> commandParams) throws IOException { + Map<String, List<String>> payload = objectMapper.readValue(commandParams.get(UserHookParams.PAYLOAD.param()), Map.class); + return payload; + } + +} http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/ShellCommandCallableFactory.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/ShellCommandCallableFactory.java b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/ShellCommandCallableFactory.java new file mode 100644 index 0000000..e34a88c --- /dev/null +++ b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/ShellCommandCallableFactory.java @@ -0,0 +1,26 @@ +/** + * 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.ambari.server.serveraction.users; + +public interface ShellCommandCallableFactory { + + ShellCommandUtilityCallable createRunCommandCallable(String[] arguments); + +} http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/ShellCommandUtilityCallable.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/ShellCommandUtilityCallable.java b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/ShellCommandUtilityCallable.java new file mode 100644 index 0000000..a59bc75 --- /dev/null +++ b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/ShellCommandUtilityCallable.java @@ -0,0 +1,48 @@ +/** + * 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.ambari.server.serveraction.users; + +import java.util.concurrent.Callable; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.apache.ambari.server.utils.ShellCommandUtil; + +import com.google.inject.assistedinject.Assisted; +import com.google.inject.assistedinject.AssistedInject; + +/** + * Wraps a shell command execution into a callable. + */ +@Singleton +public class ShellCommandUtilityCallable implements Callable<ShellCommandUtil.Result> { + + private String[] args; + + @AssistedInject + public ShellCommandUtilityCallable(@Assisted String[] args) { + this.args = args; + } + + @Override + public ShellCommandUtil.Result call() throws Exception { + return ShellCommandUtil.runCommand(args); + } +} http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/ShellCommandUtilityWrapper.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/ShellCommandUtilityWrapper.java b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/ShellCommandUtilityWrapper.java new file mode 100644 index 0000000..0539f76 --- /dev/null +++ b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/ShellCommandUtilityWrapper.java @@ -0,0 +1,57 @@ +/** + * 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.ambari.server.serveraction.users; + +import java.io.IOException; +import java.util.Map; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.apache.ambari.server.utils.ShellCommandUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Wraps the {@link org.apache.ambari.server.utils.ShellCommandUtil} utility. + * Simply delegates to the utility. It's intended to be used instead of directly invoking the utility class. + * + * Classes using the wrapper will be decoupled from the static utility; thus can be used and tested easily. + */ +@Singleton +public class ShellCommandUtilityWrapper { + + private static final Logger LOGGER = LoggerFactory.getLogger(ShellCommandUtilityWrapper.class); + + @Inject + public ShellCommandUtilityWrapper() { + super(); + } + + public ShellCommandUtil.Result runCommand(String[] args) throws IOException, InterruptedException { + LOGGER.info("Running command: {}", args); + return ShellCommandUtil.runCommand(args); + } + + public ShellCommandUtil.Result runCommand(String[] args, Map<String, String> vars) throws IOException, InterruptedException { + LOGGER.info("Running command: {}, variables: {}", args, vars); + return ShellCommandUtil.runCommand(args, vars); + } + +} http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/topology/AsyncCallableService.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/topology/AsyncCallableService.java b/ambari-server/src/main/java/org/apache/ambari/server/topology/AsyncCallableService.java index fc7f190..95ab6b0 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/topology/AsyncCallableService.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/topology/AsyncCallableService.java @@ -18,15 +18,17 @@ package org.apache.ambari.server.topology; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.util.Calendar; +import java.util.HashSet; +import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * Callable service implementation for executing tasks asynchronously. * The service repeatedly tries to execute the provided task till it successfully completes, or the provided timeout @@ -34,7 +36,7 @@ import java.util.concurrent.TimeUnit; * * @param <T> the type returned by the task to be executed */ -public class AsyncCallableService<T> implements Callable<Boolean> { +public class AsyncCallableService<T> implements Callable<T> { private static final Logger LOG = LoggerFactory.getLogger(AsyncCallableService.class); @@ -51,8 +53,9 @@ public class AsyncCallableService<T> implements Callable<Boolean> { // the delay between two consecutive execution trials in milliseconds private final long delay; + private T serviceResult; - private Boolean serviceResult = Boolean.FALSE; + private final Set<Exception> errors = new HashSet<>(); public AsyncCallableService(Callable<T> task, long timeout, long delay, ScheduledExecutorService executorService) { @@ -63,7 +66,7 @@ public class AsyncCallableService<T> implements Callable<Boolean> { } @Override - public Boolean call() { + public T call() { long startTimeInMillis = Calendar.getInstance().getTimeInMillis(); LOG.info("Task execution started at: {}", startTimeInMillis); @@ -104,11 +107,13 @@ public class AsyncCallableService<T> implements Callable<Boolean> { // task failures are expected to be reportesd as exceptions LOG.debug("Task successfully executed: {}", taskResult); - setServiceResult(Boolean.TRUE); + setServiceResult(taskResult); + errors.clear(); completed = true; } catch (Exception e) { // Future.isDone always true here! LOG.info("Exception during task execution: ", e); + errors.add(e); } return completed; } @@ -117,8 +122,12 @@ public class AsyncCallableService<T> implements Callable<Boolean> { return timeout < Calendar.getInstance().getTimeInMillis() - startTimeInMillis; } - private void setServiceResult(Boolean serviceResult) { + private void setServiceResult(T serviceResult) { this.serviceResult = serviceResult; } + public Set<Exception> getErrors() { + return errors; + } + } http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/utils/ShellCommandUtil.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/utils/ShellCommandUtil.java b/ambari-server/src/main/java/org/apache/ambari/server/utils/ShellCommandUtil.java index 8aa9c08..99f47c5 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/utils/ShellCommandUtil.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/utils/ShellCommandUtil.java @@ -502,7 +502,7 @@ public class ShellCommandUtil { public static class Result { - Result(int exitCode, String stdout, String stderr) { + public Result(int exitCode, String stdout, String stderr) { this.exitCode = exitCode; this.stdout = stdout; this.stderr = stderr; http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/resources/scripts/post-user-creation-hook.sh ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/resources/scripts/post-user-creation-hook.sh b/ambari-server/src/main/resources/scripts/post-user-creation-hook.sh new file mode 100755 index 0000000..34169c1 --- /dev/null +++ b/ambari-server/src/main/resources/scripts/post-user-creation-hook.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +# +# 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. +# + +: "${DEBUG:=0}" + +validate_input_arguments(){ + +# the first argument is the csv file | username, group1, group2 +CSV_FILE="$1" +: "${CSV_FILE:?"Missing csv file input for the post-user creation hook"}" + +# the second argument is the cluster security type +SECURITY_TYPE=$2 +: "${SECURITY_TYPE:?"Missing security type input for the post-user creation hook"}" + +} + + +# wraps the command passed in as argument to be called via the ambari sudo +ambari_sudo(){ + +ARG_STR="$1" +CMD_STR="/var/lib/ambari-server/ambari-sudo.sh su hdfs -l -s /bin/bash -c '$ARG_STR'" + +eval "$CMD_STR" +} + +setup_security(){ +if [ "$SECURITY_TYPE" == "KERBEROS" ] +then + HDFS_PRINCIPAL=$3 +: "${HDFS_PRINCIPAL:?"Missing hdfs principal for the post-user creation hook"}" + + HDFS_KEYTAB=$4 +: "${HDFS_KEYTAB:?"Missing hdfs principal for the post-user creation hook"}" + + echo "The cluster is secure, calling kinit ..." + kinit_cmd="/usr/bin/kinit -kt $HDFS_KEYTAB $HDFS_PRINCIPAL" + + ambari_sudo "$kinit_cmd" +else + echo "The cluster security type is $SECURITY_TYPE" +fi +} + +check_tools(){ +echo "Checking for required tools ..." + +# check for hadoop +ambari_sudo "type hadoop > /dev/null 2>&1 || { echo >&2 \"hadoop client not installed\"; exit 1; }" + +# check for the hdfs +ambari_sudo "hadoop fs -ls / > /dev/null 2>&1 || { echo >&2 \"hadoop dfs not available\"; exit 1; }" + +echo "Checking for required tools ... DONE." + +} + +prepare_input(){ +# perform any specific logic on the arguments +echo "Processing post user creation hook payload ..." + +JSON_INPUT="$CSV_FILE.json" +echo "Generating json file $JSON_INPUT ..." + +echo "[" | cat > "$JSON_INPUT" +while read -r LINE +do + USR_NAME=$(echo "$LINE" | awk -F, '{print $1}') + + cat <<EOF >> "$JSON_INPUT" + { + "target":"/user/$USR_NAME", + "type":"directory", + "action":"create", + "owner":"$USR_NAME", + "group":"hadoop", + "manageIfExists": "true" + }, +EOF +done <"$CSV_FILE" + +sed -i '$ d' "$JSON_INPUT" +echo $'}\n]' | cat >> "$JSON_INPUT" +echo "Generating file $JSON_INPUT ... DONE." +echo "Processing post user creation hook payload ... DONE." + +} + + +# encapsulates the logic of the post user creation script +main(){ + +echo $DEBUG; + +if [ "$DEBUG" != "0" ]; then echo "Switch debug ON";set -x; else echo "debug: OFF"; fi + +echo "Executing user hook with parameters: $*" + +validate_input_arguments "$@" + +setup_security "$@" + +check_tools + +prepare_input + +# the default implementation creates user home folders; the first argument must be the username +ambari_sudo "yarn jar /var/lib/ambari-server/resources/stacks/HDP/2.0.6/hooks/before-START/files/fast-hdfs-resource.jar $JSON_INPUT" + +if [ "$DEBUG" -gt "0" ]; then echo "Switch debug OFF";set -x;unset DEBUG; else echo "debug: OFF"; fi +unset DEBUG +} + + +main "$@" http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/ActiveWidgetLayoutResourceProviderTest.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/ActiveWidgetLayoutResourceProviderTest.java b/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/ActiveWidgetLayoutResourceProviderTest.java index c592a20..28a8bc8 100644 --- a/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/ActiveWidgetLayoutResourceProviderTest.java +++ b/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/ActiveWidgetLayoutResourceProviderTest.java @@ -38,6 +38,8 @@ import org.apache.ambari.server.controller.spi.ResourceProvider; import org.apache.ambari.server.controller.spi.SystemException; import org.apache.ambari.server.controller.utilities.PredicateBuilder; import org.apache.ambari.server.controller.utilities.PropertyHelper; +import org.apache.ambari.server.hooks.HookContextFactory; +import org.apache.ambari.server.hooks.HookService; import org.apache.ambari.server.metadata.CachedRoleCommandOrderProvider; import org.apache.ambari.server.metadata.RoleCommandOrderProvider; import org.apache.ambari.server.orm.DBAccessor; @@ -400,6 +402,8 @@ public class ActiveWidgetLayoutResourceProviderTest extends EasyMockSupport { bind(UserDAO.class).toInstance(createMock(UserDAO.class)); bind(WidgetLayoutDAO.class).toInstance(createMock(WidgetLayoutDAO.class)); bind(HostRoleCommandDAO.class).toInstance(createMock(HostRoleCommandDAO.class)); + bind(HookContextFactory.class).toInstance(createMock(HookContextFactory.class)); + bind(HookService.class).toInstance(createMock(HookService.class)); } }); }