org.apache.hadoop.registry.server.services.RegistryAdminService.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.hadoop.registry.server.services.RegistryAdminService.java

Source

/*
 * 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.hadoop.registry.server.services;

import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang.StringUtils;
import org.apache.curator.framework.api.BackgroundCallback;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.PathIsNotEmptyDirectoryException;
import org.apache.hadoop.fs.PathNotFoundException;
import org.apache.hadoop.service.ServiceStateException;
import org.apache.hadoop.registry.client.binding.RegistryUtils;
import org.apache.hadoop.registry.client.binding.RegistryPathUtils;
import org.apache.hadoop.registry.client.exceptions.InvalidRecordException;
import org.apache.hadoop.registry.client.exceptions.NoPathPermissionsException;
import org.apache.hadoop.registry.client.exceptions.NoRecordException;
import org.apache.hadoop.registry.client.impl.zk.RegistryBindingSource;
import org.apache.hadoop.registry.client.impl.zk.RegistryOperationsService;
import org.apache.hadoop.registry.client.impl.zk.RegistrySecurity;
import org.apache.hadoop.registry.client.types.RegistryPathStatus;
import org.apache.hadoop.registry.client.types.ServiceRecord;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.data.ACL;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.EOFException;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Administrator service for the registry. This is the one with
 * permissions to create the base directories and those for users.
 *
 * It also includes support for asynchronous operations, so that
 * zookeeper connectivity problems do not hold up the server code
 * performing the actions.
 *
 * Any action queued via {@link #submit(Callable)} will be
 * run asynchronously. The {@link #createDirAsync(String, List, boolean)}
 * is an example of such an an action
 *
 * A key async action is the depth-first tree purge, which supports
 * pluggable policies for deleting entries. The method
 * {@link #purge(String, NodeSelector, PurgePolicy, BackgroundCallback)}
 * implements the recursive purge operation the class
 * {{AsyncPurge}} provides the asynchronous scheduling of this.
 */
public class RegistryAdminService extends RegistryOperationsService {

    private static final Logger LOG = LoggerFactory.getLogger(RegistryAdminService.class);
    /**
     * The ACL permissions for the user's homedir ACL.
     */
    public static final int USER_HOMEDIR_ACL_PERMISSIONS = ZooDefs.Perms.READ | ZooDefs.Perms.WRITE
            | ZooDefs.Perms.CREATE | ZooDefs.Perms.DELETE;

    /**
     * Executor for async operations
     */
    protected final ExecutorService executor;

    /**
     * Construct an instance of the service
     * @param name service name
     */
    public RegistryAdminService(String name) {
        this(name, null);
    }

    /**
     * construct an instance of the service, using the
     * specified binding source to bond to ZK
     * @param name service name
     * @param bindingSource provider of ZK binding information
     */
    public RegistryAdminService(String name, RegistryBindingSource bindingSource) {
        super(name, bindingSource);
        executor = Executors.newCachedThreadPool(new ThreadFactory() {
            private AtomicInteger counter = new AtomicInteger(1);

            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "RegistryAdminService " + counter.getAndIncrement());
            }
        });
    }

    /**
     * Stop the service: halt the executor.
     * @throws Exception exception.
     */
    @Override
    protected void serviceStop() throws Exception {
        stopExecutor();
        super.serviceStop();
    }

    /**
     * Stop the executor if it is not null.
     * This uses {@link ExecutorService#shutdownNow()}
     * and so does not block until they have completed.
     */
    protected synchronized void stopExecutor() {
        if (executor != null) {
            executor.shutdownNow();
        }
    }

    /**
     * Get the executor
     * @return the executor
     */
    protected ExecutorService getExecutor() {
        return executor;
    }

    /**
     * Submit a callable
     * @param callable callable
     * @param <V> type of the final get
     * @return a future to wait on
     */
    public <V> Future<V> submit(Callable<V> callable) {
        if (LOG.isDebugEnabled()) {
            LOG.debug("Submitting {}", callable);
        }
        return getExecutor().submit(callable);
    }

    /**
     * Asynchronous operation to create a directory
     * @param path path
     * @param acls ACL list
     * @param createParents flag to indicate parent dirs should be created
     * as needed
     * @return the future which will indicate whether or not the operation
     * succeeded and propagate any exceptions
     * @throws IOException
     */
    public Future<Boolean> createDirAsync(final String path, final List<ACL> acls, final boolean createParents)
            throws IOException {
        return submit(new Callable<Boolean>() {
            @Override
            public Boolean call() throws Exception {
                return maybeCreate(path, CreateMode.PERSISTENT, acls, createParents);
            }
        });
    }

    /**
     * Init operation sets up the system ACLs.
     * @param conf configuration of the service
     * @throws Exception
     */
    @Override
    protected void serviceInit(Configuration conf) throws Exception {
        super.serviceInit(conf);
        RegistrySecurity registrySecurity = getRegistrySecurity();
        if (registrySecurity.isSecureRegistry()) {
            ACL sasl = registrySecurity.createSaslACLFromCurrentUser(ZooDefs.Perms.ALL);
            registrySecurity.addSystemACL(sasl);
            LOG.info("Registry System ACLs:", RegistrySecurity.aclsToString(registrySecurity.getSystemACLs()));
        }
    }

    /**
     * Start the service, including creating base directories with permissions
     * @throws Exception
     */
    @Override
    protected void serviceStart() throws Exception {
        super.serviceStart();
        // create the root directories
        try {
            createRootRegistryPaths();
        } catch (NoPathPermissionsException e) {

            String message = String.format(
                    Locale.ENGLISH, "Failed to create root paths {%s};" + "%ndiagnostics={%s}"
                            + "%ncurrent registry is:" + "%n{%s}",
                    e, bindingDiagnosticDetails(), dumpRegistryRobustly(true));

            LOG.error(" Failure {}", e, e);
            LOG.error(message);

            // TODO: this is something temporary to deal with the problem
            // that jenkins is failing this test
            throw new NoPathPermissionsException(e.getPath().toString(), message, e);
        }
    }

    /**
     * Create the initial registry paths
     * @throws IOException any failure
     */
    @VisibleForTesting
    public void createRootRegistryPaths() throws IOException {

        List<ACL> systemACLs = getRegistrySecurity().getSystemACLs();
        LOG.info("System ACLs {}", RegistrySecurity.aclsToString(systemACLs));
        maybeCreate("", CreateMode.PERSISTENT, systemACLs, false);
        maybeCreate(PATH_USERS, CreateMode.PERSISTENT, systemACLs, false);
        maybeCreate(PATH_SYSTEM_SERVICES, CreateMode.PERSISTENT, systemACLs, false);
    }

    /**
     * Get the path to a user's home dir
     * @param username username
     * @return a path for services underneath
     */
    protected String homeDir(String username) {
        return RegistryUtils.homePathForUser(username);
    }

    /**
     * Set up the ACL for the user.
     * <b>Important: this must run client-side as it needs
     * to know the id:pass tuple for a user</b>
     * @param username user name
     * @param perms permissions
     * @return an ACL list
     * @throws IOException ACL creation/parsing problems
     */
    public List<ACL> aclsForUser(String username, int perms) throws IOException {
        List<ACL> clientACLs = getClientAcls();
        RegistrySecurity security = getRegistrySecurity();
        if (security.isSecureRegistry()) {
            clientACLs.add(security.createACLfromUsername(username, perms));
        }
        return clientACLs;
    }

    /**
     * Start an async operation to create the home path for a user
     * if it does not exist
     * @param shortname username, without any @REALM in kerberos
     * @return the path created
     * @throws IOException any failure while setting up the operation
     *
     */
    public Future<Boolean> initUserRegistryAsync(final String shortname) throws IOException {

        String homeDir = homeDir(shortname);
        if (!exists(homeDir)) {
            // create the directory. The user does not
            return createDirAsync(homeDir, aclsForUser(shortname, USER_HOMEDIR_ACL_PERMISSIONS), false);
        }
        return null;
    }

    /**
     * Create the home path for a user if it does not exist.
     *
     * This uses {@link #initUserRegistryAsync(String)} and then waits for the
     * result ... the code path is the same as the async operation; this just
     * picks up and relays/converts exceptions
     * @param username username
     * @return the path created
     * @throws IOException any failure
     *
     */
    public String initUserRegistry(final String username) throws IOException {

        try {
            Future<Boolean> future = initUserRegistryAsync(username);
            future.get();
        } catch (InterruptedException e) {
            throw (InterruptedIOException) (new InterruptedIOException(e.toString()).initCause(e));
        } catch (ExecutionException e) {
            Throwable cause = e.getCause();
            if (cause instanceof IOException) {
                throw (IOException) (cause);
            } else {
                throw new IOException(cause.toString(), cause);
            }
        }

        return homeDir(username);
    }

    /**
     * Method to validate the validity of the kerberos realm.
     * <ul>
     *   <li>Insecure: not needed.</li>
     *   <li>Secure: must have been determined.</li>
     * </ul>
     */
    protected void verifyRealmValidity() throws ServiceStateException {
        if (isSecure()) {
            String realm = getRegistrySecurity().getKerberosRealm();
            if (StringUtils.isEmpty(realm)) {
                throw new ServiceStateException("Cannot determine service realm");
            }
            if (LOG.isDebugEnabled()) {
                LOG.debug("Started Registry operations in realm {}", realm);
            }
        }
    }

    /**
     * Policy to purge entries
     */
    public enum PurgePolicy {
        PurgeAll, FailOnChildren, SkipOnChildren
    }

    /**
     * Recursive operation to purge all matching records under a base path.
     * <ol>
     *   <li>Uses a depth first search</li>
     *   <li>A match is on ID and persistence policy, or, if policy==-1, any match</li>
     *   <li>If a record matches then it is deleted without any child searches</li>
     *   <li>Deletions will be asynchronous if a callback is provided</li>
     * </ol>
     *
     * The code is designed to be robust against parallel deletions taking place;
     * in such a case it will stop attempting that part of the tree. This
     * avoid the situation of more than 1 purge happening in parallel and
     * one of the purge operations deleteing the node tree above the other.
     * @param path base path
     * @param selector selector for the purge policy
     * @param purgePolicy what to do if there is a matching record with children
     * @param callback optional curator callback
     * @return the number of delete operations perfomed. As deletes may be for
     * everything under a path, this may be less than the number of records
     * actually deleted
     * @throws IOException problems
     * @throws PathIsNotEmptyDirectoryException if an entry cannot be deleted
     * as it has children and the purge policy is FailOnChildren
     */
    @VisibleForTesting
    public int purge(String path, NodeSelector selector, PurgePolicy purgePolicy, BackgroundCallback callback)
            throws IOException {

        boolean toDelete = false;
        // look at self to see if it has a service record
        Map<String, RegistryPathStatus> childEntries;
        Collection<RegistryPathStatus> entries;
        try {
            // list this path's children
            childEntries = RegistryUtils.statChildren(this, path);
            entries = childEntries.values();
        } catch (PathNotFoundException e) {
            // there's no record here, it may have been deleted already.
            // exit
            return 0;
        }

        try {
            RegistryPathStatus registryPathStatus = stat(path);
            ServiceRecord serviceRecord = resolve(path);
            // there is now an entry here.
            toDelete = selector.shouldSelect(path, registryPathStatus, serviceRecord);
        } catch (EOFException ignored) {
            // ignore
        } catch (InvalidRecordException ignored) {
            // ignore
        } catch (NoRecordException ignored) {
            // ignore
        } catch (PathNotFoundException e) {
            // there's no record here, it may have been deleted already.
            // exit
            return 0;
        }

        if (toDelete && !entries.isEmpty()) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Match on record @ {} with children ", path);
            }
            // there's children
            switch (purgePolicy) {
            case SkipOnChildren:
                // don't do the deletion... continue to next record
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Skipping deletion");
                }
                toDelete = false;
                break;
            case PurgeAll:
                // mark for deletion
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Scheduling for deletion with children");
                }
                toDelete = true;
                entries = new ArrayList<RegistryPathStatus>(0);
                break;
            case FailOnChildren:
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Failing deletion operation");
                }
                throw new PathIsNotEmptyDirectoryException(path);
            }
        }

        int deleteOps = 0;
        if (toDelete) {
            try {
                zkDelete(path, true, callback);
            } catch (PathNotFoundException e) {
                // sign that the path was deleted during the operation.
                // this is a no-op, and all children can be skipped
                return deleteOps;
            }
            deleteOps++;
        }

        // now go through the children
        for (RegistryPathStatus status : entries) {
            String childname = status.path;
            String childpath = RegistryPathUtils.join(path, childname);
            deleteOps += purge(childpath, selector, purgePolicy, callback);
        }

        return deleteOps;
    }

    /**
     * Comparator used for purge logic
     */
    public interface NodeSelector {

        boolean shouldSelect(String path, RegistryPathStatus registryPathStatus, ServiceRecord serviceRecord);
    }

    /**
     * An async registry purge action taking
     * a selector which decides what to delete
     */
    public class AsyncPurge implements Callable<Integer> {

        private final BackgroundCallback callback;
        private final NodeSelector selector;
        private final String path;
        private final PurgePolicy purgePolicy;

        public AsyncPurge(String path, NodeSelector selector, PurgePolicy purgePolicy,
                BackgroundCallback callback) {
            this.callback = callback;
            this.selector = selector;
            this.path = path;
            this.purgePolicy = purgePolicy;
        }

        @Override
        public Integer call() throws Exception {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Executing {}", this);
            }
            return purge(path, selector, purgePolicy, callback);
        }

        @Override
        public String toString() {
            return String.format("Record purge under %s with selector %s", path, selector);
        }
    }

}