/**
* Copyright 2016 Yahoo Inc.
*
* Licensed 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 com.yahoo.pulsar.broker.admin;
import static com.google.common.base.Preconditions.checkArgument;
import java.net.MalformedURLException;
import java.net.URI;
import java.util.List;
import java.util.Set;
import javax.servlet.ServletContext;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriBuilder;
import org.apache.bookkeeper.util.ZkUtils;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.ZooKeeper.States;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import com.yahoo.pulsar.broker.PulsarService;
import com.yahoo.pulsar.broker.cache.LocalZooKeeperCacheService;
import com.yahoo.pulsar.broker.web.PulsarWebResource;
import com.yahoo.pulsar.broker.web.RestException;
import com.yahoo.pulsar.common.naming.NamespaceBundle;
import com.yahoo.pulsar.common.naming.NamespaceBundleFactory;
import com.yahoo.pulsar.common.naming.NamespaceBundles;
import com.yahoo.pulsar.common.naming.NamespaceName;
import com.yahoo.pulsar.common.policies.data.BundlesData;
import com.yahoo.pulsar.common.policies.data.ClusterData;
import com.yahoo.pulsar.common.policies.data.Policies;
import com.yahoo.pulsar.common.policies.data.PropertyAdmin;
import com.yahoo.pulsar.common.policies.impl.NamespaceIsolationPolicies;
import com.yahoo.pulsar.common.util.ObjectMapperFactory;
import com.yahoo.pulsar.zookeeper.ZooKeeperCache;
import com.yahoo.pulsar.zookeeper.ZooKeeperChildrenCache;
import com.yahoo.pulsar.zookeeper.ZooKeeperDataCache;
public abstract class AdminResource extends PulsarWebResource {
private static final Logger log = LoggerFactory.getLogger(AdminResource.class);
private static final String POLICIES_READONLY_FLAG_PATH = "/admin/flags/policies-readonly";
public static final String LOAD_SHEDDING_UNLOAD_DISABLED_FLAG_PATH = "/admin/flags/load-shedding-unload-disabled";
protected ZooKeeper globalZk() {
return pulsar().getGlobalZkCache().getZooKeeper();
}
protected ZooKeeperCache globalZkCache() {
return pulsar().getGlobalZkCache();
}
protected ZooKeeper localZk() {
return pulsar().getZkClient();
}
protected ZooKeeperCache localZkCache() {
return pulsar().getLocalZkCache();
}
protected LocalZooKeeperCacheService localCacheService() {
return pulsar().getLocalZkCacheService();
}
protected void zkCreate(String path, byte[] content) throws Exception {
globalZk().create(path, content, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
protected void zkCreateOptimistic(String path, byte[] content) throws Exception {
ZkUtils.createFullPathOptimistic(globalZk(), path, content, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
/**
* Get the domain of the destination (whether it's queue or topic)
*/
protected String domain() {
if (uri.getPath().startsWith("queues/")) {
return "queue";
} else if (uri.getPath().startsWith("topics/")) {
return "topic";
} else if (uri.getPath().startsWith("persistent/")) {
return "persistent";
} else {
throw new RestException(Status.INTERNAL_SERVER_ERROR, "domain() invoked from wrong resource");
}
}
// This is a stub method for Mockito
@Override
protected void validateSuperUserAccess() {
super.validateSuperUserAccess();
}
// This is a stub method for Mockito
@Override
protected void validateAdminAccessOnProperty(String property) {
super.validateAdminAccessOnProperty(property);
}
// This is a stub method for Mockito
@Override
protected void validateNamespaceOwnershipWithBundles(String property, String cluster, String namespace,
boolean authoritative, boolean readOnly, BundlesData bundleData) {
super.validateNamespaceOwnershipWithBundles(property, cluster, namespace, authoritative, readOnly, bundleData);
}
// This is a stub method for Mockito
@Override
protected void validateBundleOwnership(String property, String cluster, String namespace, boolean authoritative,
boolean readOnly, NamespaceBundle bundle) {
super.validateBundleOwnership(property, cluster, namespace, authoritative, readOnly, bundle);
}
// This is a stub method for Mockito
@Override
protected boolean isLeaderBroker() {
return super.isLeaderBroker();
}
/**
* Checks whether the broker is allowed to do read-write operations based on the existence of a node in global
* zookeeper.
*
* @throws WebApplicationException
* if broker has a read only access if broker is not connected to the global zookeeper
*/
public void validatePoliciesReadOnlyAccess() {
boolean arePoliciesReadOnly = true;
try {
arePoliciesReadOnly = globalZkCache().exists(POLICIES_READONLY_FLAG_PATH);
} catch (Exception e) {
log.warn("Unable to fetch contents of [{}] from global zookeeper", POLICIES_READONLY_FLAG_PATH, e);
throw new RestException(e);
}
if (arePoliciesReadOnly) {
log.debug("Policies are read-only. Broker cannot do read-write operations");
throw new RestException(Status.FORBIDDEN, "Broker is forbidden to do read-write operations");
} else {
// Make sure the broker is connected to the global zookeeper before writing. If not, throw an exception.
if (globalZkCache().getZooKeeper().getState() != States.CONNECTED) {
log.debug("Broker is not connected to the global zookeeper");
throw new RestException(Status.PRECONDITION_FAILED,
"Broker needs to be connected to global zookeeper before making a read-write operation");
} else {
// Do nothing, just log the message.
log.debug("Broker is allowed to make read-write operations");
}
}
}
/**
* Get the list of namespaces (on every cluster) for a given property
*
* @param property
* the property name
* @return the list of namespaces
*/
protected List<String> getListOfNamespaces(String property) throws Exception {
List<String> namespaces = Lists.newArrayList();
// First get the list of cluster nodes
log.info("Children of {} : {}", path("policies", property),
globalZk().getChildren(path("policies", property), null));
for (String cluster : globalZk().getChildren(path("policies", property), false)) {
// Then get the list of namespaces
try {
for (String namespace : globalZk().getChildren(path("policies", property, cluster), false)) {
namespaces.add(String.format("%s/%s/%s", property, cluster, namespace));
}
} catch (KeeperException.NoNodeException e) {
// A cluster was deleted between the 2 getChildren() calls, ignoring
}
}
namespaces.sort(null);
return namespaces;
}
/**
* Redirect the call to the specified broker
*
* @param broker
* Broker name
* @throws MalformedURLException
* In case the redirect happens
*/
protected void validateBrokerName(String broker) throws MalformedURLException {
String brokerUrl = String.format("http://%s", broker);
if (!pulsar().getWebServiceAddress().equals(brokerUrl)) {
String[] parts = broker.split(":");
checkArgument(parts.length == 2);
String host = parts[0];
int port = Integer.parseInt(parts[1]);
URI redirect = UriBuilder.fromUri(uri.getRequestUri()).host(host).port(port).build();
log.debug("[{}] Redirecting the rest call to {}: broker={}", clientAppId(), redirect, broker);
throw new WebApplicationException(Response.temporaryRedirect(redirect).build());
}
}
protected Policies getNamespacePolicies(String property, String cluster, String namespace) {
try {
Policies policies = policiesCache().get(AdminResource.path("policies", property, cluster, namespace))
.orElseThrow(() -> new RestException(Status.NOT_FOUND, "Namespace does not exist"));
// fetch bundles from LocalZK-policies
NamespaceBundles bundles = pulsar().getNamespaceService().getNamespaceBundleFactory()
.getBundles(new NamespaceName(property, cluster, namespace));
BundlesData bundleData = NamespaceBundleFactory.getBundlesData(bundles);
policies.bundles = bundleData != null ? bundleData : policies.bundles;
return policies;
} catch (RestException re) {
throw re;
} catch (Exception e) {
log.error("[{}] Failed to get namespace policies {}/{}/{}", clientAppId(), property, cluster, namespace, e);
throw new RestException(e);
}
}
public static ObjectMapper jsonMapper() {
return ObjectMapperFactory.getThreadLocal();
}
ZooKeeperDataCache<PropertyAdmin> propertiesCache() {
return pulsar().getConfigurationCache().propertiesCache();
}
ZooKeeperDataCache<Policies> policiesCache() {
return pulsar().getConfigurationCache().policiesCache();
}
ZooKeeperDataCache<ClusterData> clustersCache() {
return pulsar().getConfigurationCache().clustersCache();
}
ZooKeeperChildrenCache managedLedgerListCache() {
return pulsar().getLocalZkCacheService().managedLedgerListCache();
}
Set<String> clusters() {
try {
return pulsar().getConfigurationCache().clustersListCache().get();
} catch (Exception e) {
throw new RestException(e);
}
}
ZooKeeperChildrenCache clustersListCache() {
return pulsar().getConfigurationCache().clustersListCache();
}
protected void setServletContext(ServletContext servletContext) {
this.servletContext = servletContext;
}
protected ZooKeeperDataCache<NamespaceIsolationPolicies> namespaceIsolationPoliciesCache() {
return pulsar().getConfigurationCache().namespaceIsolationPoliciesCache();
}
}