/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.nifi.remote; import static java.util.Objects.requireNonNull; import java.io.File; import java.io.IOException; import java.net.InetAddress; import java.net.NetworkInterface; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import javax.net.ssl.SSLContext; import javax.ws.rs.core.Response; import org.apache.commons.lang3.StringUtils; import org.apache.nifi.authorization.Resource; import org.apache.nifi.authorization.resource.Authorizable; import org.apache.nifi.authorization.resource.ResourceFactory; import org.apache.nifi.authorization.resource.ResourceType; import org.apache.nifi.components.ValidationResult; import org.apache.nifi.connectable.ConnectableType; import org.apache.nifi.connectable.Connection; import org.apache.nifi.connectable.Port; import org.apache.nifi.connectable.Position; import org.apache.nifi.controller.FlowController; import org.apache.nifi.controller.ProcessScheduler; import org.apache.nifi.controller.ScheduledState; import org.apache.nifi.controller.exception.CommunicationsException; import org.apache.nifi.engine.FlowEngine; import org.apache.nifi.events.BulletinFactory; import org.apache.nifi.events.EventReporter; import org.apache.nifi.groups.ProcessGroup; import org.apache.nifi.groups.ProcessGroupCounts; import org.apache.nifi.groups.RemoteProcessGroup; import org.apache.nifi.groups.RemoteProcessGroupPortDescriptor; import org.apache.nifi.remote.protocol.SiteToSiteTransportProtocol; import org.apache.nifi.remote.protocol.http.HttpProxy; import org.apache.nifi.remote.util.SiteToSiteRestApiClient; import org.apache.nifi.reporting.BulletinRepository; import org.apache.nifi.reporting.ComponentType; import org.apache.nifi.reporting.Severity; import org.apache.nifi.util.FormatUtils; import org.apache.nifi.util.NiFiProperties; import org.apache.nifi.web.api.dto.ControllerDTO; import org.apache.nifi.web.api.dto.PortDTO; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.sun.jersey.api.client.ClientHandlerException; import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.ClientResponse.Status; import com.sun.jersey.api.client.UniformInterfaceException; /** * Represents the Root Process Group of a remote NiFi Instance. Holds * information about that remote instance, as well as {@link IncomingPort}s and * {@link OutgoingPort}s for communicating with the remote instance. */ public class StandardRemoteProcessGroup implements RemoteProcessGroup { private static final Logger logger = LoggerFactory.getLogger(StandardRemoteProcessGroup.class); // status codes private static final int UNAUTHORIZED_STATUS_CODE = Status.UNAUTHORIZED.getStatusCode(); private static final int FORBIDDEN_STATUS_CODE = Status.FORBIDDEN.getStatusCode(); private final String id; private final String targetUris; private final ProcessScheduler scheduler; private final EventReporter eventReporter; private final NiFiProperties nifiProperties; private final AtomicReference<String> name = new AtomicReference<>(); private final AtomicReference<Position> position = new AtomicReference<>(new Position(0D, 0D)); private final AtomicReference<String> comments = new AtomicReference<>(); private final AtomicReference<ProcessGroup> processGroup; private final AtomicBoolean transmitting = new AtomicBoolean(false); private final SSLContext sslContext; private volatile String communicationsTimeout = "30 sec"; private volatile String targetId; private volatile String yieldDuration = "10 sec"; private volatile SiteToSiteTransportProtocol transportProtocol = SiteToSiteTransportProtocol.RAW; private volatile String proxyHost; private volatile Integer proxyPort; private volatile String proxyUser; private volatile String proxyPassword; private String networkInterfaceName; private InetAddress localAddress; private ValidationResult nicValidationResult; private final ReadWriteLock rwLock = new ReentrantReadWriteLock(); private final Lock readLock = rwLock.readLock(); private final Lock writeLock = rwLock.writeLock(); // the following variables are all protected by the read/write lock above. // Maps a Port Name to an OutgoingPort that can be used to push files to that port private final Map<String, StandardRemoteGroupPort> inputPorts = new HashMap<>(); // Maps a Port Name to a PullingPort that can be used to receive files from that port private final Map<String, StandardRemoteGroupPort> outputPorts = new HashMap<>(); private ProcessGroupCounts counts = new ProcessGroupCounts(0, 0, 0, 0, 0, 0, 0, 0); private Long refreshContentsTimestamp = null; private Boolean destinationSecure; private Integer listeningPort; private Integer listeningHttpPort; private volatile String authorizationIssue; private final ScheduledExecutorService backgroundThreadExecutor; public StandardRemoteProcessGroup(final String id, final String targetUris, final ProcessGroup processGroup, final FlowController flowController, final SSLContext sslContext, final NiFiProperties nifiProperties) { this.nifiProperties = nifiProperties; this.id = requireNonNull(id); this.targetUris = targetUris; this.targetId = null; this.processGroup = new AtomicReference<>(processGroup); this.sslContext = sslContext; this.scheduler = flowController.getProcessScheduler(); this.authorizationIssue = "Establishing connection to " + targetUris; final BulletinRepository bulletinRepository = flowController.getBulletinRepository(); eventReporter = new EventReporter() { private static final long serialVersionUID = 1L; @Override public void reportEvent(final Severity severity, final String category, final String message) { final String groupId = StandardRemoteProcessGroup.this.getProcessGroup().getIdentifier(); final String sourceId = StandardRemoteProcessGroup.this.getIdentifier(); final String sourceName = StandardRemoteProcessGroup.this.getName(); bulletinRepository.addBulletin(BulletinFactory.createBulletin(groupId, sourceId, ComponentType.REMOTE_PROCESS_GROUP, sourceName, category, severity.name(), message)); } }; final Runnable checkAuthorizations = new InitializationTask(); backgroundThreadExecutor = new FlowEngine(1, "Remote Process Group " + id + ": " + targetUris); backgroundThreadExecutor.scheduleWithFixedDelay(checkAuthorizations, 5L, 30L, TimeUnit.SECONDS); } @Override public void reinitialize(boolean isClustered) { backgroundThreadExecutor.submit(new InitializationTask()); } @Override public void onRemove() { backgroundThreadExecutor.shutdown(); final File file = getPeerPersistenceFile(); if (file.exists() && !file.delete()) { logger.warn("Failed to remove {}. This file should be removed manually.", file); } } @Override public void shutdown() { backgroundThreadExecutor.shutdown(); } @Override public String getIdentifier() { return id; } @Override public String getProcessGroupIdentifier() { final ProcessGroup procGroup = getProcessGroup(); return procGroup == null ? null : procGroup.getIdentifier(); } @Override public Authorizable getParentAuthorizable() { return getProcessGroup(); } @Override public Resource getResource() { return ResourceFactory.getComponentResource(ResourceType.RemoteProcessGroup, getIdentifier(), getName()); } @Override public ProcessGroup getProcessGroup() { return processGroup.get(); } @Override public void setProcessGroup(final ProcessGroup group) { this.processGroup.set(group); for (final RemoteGroupPort port : getInputPorts()) { port.setProcessGroup(group); } for (final RemoteGroupPort port : getOutputPorts()) { port.setProcessGroup(group); } } public void setTargetId(final String targetId) { this.targetId = targetId; } @Override public void setTransportProtocol(final SiteToSiteTransportProtocol transportProtocol) { this.transportProtocol = transportProtocol; } @Override public SiteToSiteTransportProtocol getTransportProtocol() { return transportProtocol; } @Override public String getProxyHost() { return proxyHost; } @Override public void setProxyHost(String proxyHost) { this.proxyHost = proxyHost; } @Override public Integer getProxyPort() { return proxyPort; } @Override public void setProxyPort(Integer proxyPort) { this.proxyPort = proxyPort; } @Override public String getProxyUser() { return proxyUser; } @Override public void setProxyUser(String proxyUser) { this.proxyUser = proxyUser; } @Override public String getProxyPassword() { return proxyPassword; } @Override public void setProxyPassword(String proxyPassword) { this.proxyPassword = proxyPassword; } /** * @return the ID of the Root Group on the remote instance */ public String getTargetId() { return targetId; } @Override public String getName() { final String name = this.name.get(); return name == null ? getTargetUri() : name; } @Override public void setName(final String name) { this.name.set(name); } @Override public String getCommunicationsTimeout() { return communicationsTimeout; } @Override public void setCommunicationsTimeout(final String timePeriod) throws IllegalArgumentException { // verify the timePeriod is legit try { final long millis = FormatUtils.getTimeDuration(timePeriod, TimeUnit.MILLISECONDS); if (millis <= 0) { throw new IllegalArgumentException("Time Period must be more than 0 milliseconds; Invalid Time Period: " + timePeriod); } if (millis > Integer.MAX_VALUE) { throw new IllegalArgumentException("Timeout is too long; cannot be greater than " + Integer.MAX_VALUE + " milliseconds"); } this.communicationsTimeout = timePeriod; } catch (final Exception e) { throw new IllegalArgumentException("Invalid Time Period: " + timePeriod); } } @Override public int getCommunicationsTimeout(final TimeUnit timeUnit) { return (int) FormatUtils.getTimeDuration(communicationsTimeout, timeUnit); } @Override public String getComments() { return comments.get(); } @Override public void setComments(final String comments) { this.comments.set(comments); } @Override public Position getPosition() { return position.get(); } @Override public void setPosition(final Position position) { this.position.set(position); } @Override public String getTargetUri() { return SiteToSiteRestApiClient.getFirstUrl(targetUris); } @Override public String getTargetUris() { return targetUris; } @Override public String getAuthorizationIssue() { return authorizationIssue; } @Override public Collection<ValidationResult> validate() { return (nicValidationResult == null) ? Collections.emptyList() : Collections.singletonList(nicValidationResult); } public int getInputPortCount() { readLock.lock(); try { return inputPorts.size(); } finally { readLock.unlock(); } } public int getOutputPortCount() { readLock.lock(); try { return outputPorts.size(); } finally { readLock.unlock(); } } public boolean containsInputPort(final String id) { readLock.lock(); try { return inputPorts.containsKey(id); } finally { readLock.unlock(); } } /** * Changes the currently configured input ports to the ports described in * the given set. If any port is currently configured that is not in the set * given, that port will be shutdown and removed. If any port is currently * not configured and is in the set given, that port will be instantiated * and started. * * @param ports the new ports * * @throws NullPointerException if the given argument is null */ @Override public void setInputPorts(final Set<RemoteProcessGroupPortDescriptor> ports) { writeLock.lock(); try { final List<String> newPortIds = new ArrayList<>(); for (final RemoteProcessGroupPortDescriptor descriptor : ports) { newPortIds.add(descriptor.getId()); if (!inputPorts.containsKey(descriptor.getId())) { addInputPort(descriptor); } // set the comments to ensure current description final StandardRemoteGroupPort sendPort = inputPorts.get(descriptor.getId()); sendPort.setTargetExists(true); sendPort.setName(descriptor.getName()); if (descriptor.isTargetRunning() != null) { sendPort.setTargetRunning(descriptor.isTargetRunning()); } sendPort.setComments(descriptor.getComments()); } // See if we have any ports that no longer exist; cannot be removed within the loop because it would cause // a ConcurrentModificationException. final Iterator<Map.Entry<String, StandardRemoteGroupPort>> itr = inputPorts.entrySet().iterator(); while (itr.hasNext()) { final Map.Entry<String, StandardRemoteGroupPort> entry = itr.next(); if (!newPortIds.contains(entry.getKey())) { final StandardRemoteGroupPort port = entry.getValue(); port.setTargetExists(false); port.setTargetRunning(false); // If port has incoming connection, it will be cleaned up when the connection is removed if (!port.hasIncomingConnection()) { itr.remove(); } } } } finally { writeLock.unlock(); } } /** * Returns a boolean indicating whether or not an Output Port exists with * the given ID * * @param id identifier of port * @return <code>true</code> if an Output Port exists with the given ID, * <code>false</code> otherwise. */ public boolean containsOutputPort(final String id) { readLock.lock(); try { return outputPorts.containsKey(id); } finally { readLock.unlock(); } } /** * Changes the currently configured output ports to the ports described in * the given set. If any port is currently configured that is not in the set * given, that port will be shutdown and removed. If any port is currently * not configured and is in the set given, that port will be instantiated * and started. * * @param ports the new ports * * @throws NullPointerException if the given argument is null */ @Override public void setOutputPorts(final Set<RemoteProcessGroupPortDescriptor> ports) { writeLock.lock(); try { final List<String> newPortIds = new ArrayList<>(); for (final RemoteProcessGroupPortDescriptor descriptor : requireNonNull(ports)) { newPortIds.add(descriptor.getId()); if (!outputPorts.containsKey(descriptor.getId())) { addOutputPort(descriptor); } // set the comments to ensure current description final StandardRemoteGroupPort receivePort = outputPorts.get(descriptor.getId()); receivePort.setTargetExists(true); receivePort.setName(descriptor.getName()); if (descriptor.isTargetRunning() != null) { receivePort.setTargetRunning(descriptor.isTargetRunning()); } receivePort.setComments(descriptor.getComments()); } // See if we have any ports that no longer exist; cannot be removed within the loop because it would cause // a ConcurrentModificationException. final Iterator<Map.Entry<String, StandardRemoteGroupPort>> itr = outputPorts.entrySet().iterator(); while (itr.hasNext()) { final Map.Entry<String, StandardRemoteGroupPort> entry = itr.next(); if (!newPortIds.contains(entry.getKey())) { final StandardRemoteGroupPort port = entry.getValue(); port.setTargetExists(false); port.setTargetRunning(false); // If port has connections, it will be cleaned up when connections are removed if (port.getConnections().isEmpty()) { itr.remove(); } } } } finally { writeLock.unlock(); } } /** * Shuts down and removes the given port * * * @throws NullPointerException if the given output Port is null * @throws IllegalStateException if the port does not belong to this remote * process group */ @Override public void removeNonExistentPort(final RemoteGroupPort port) { writeLock.lock(); try { if (requireNonNull(port).getTargetExists()) { throw new IllegalStateException("Cannot remove Remote Port " + port.getIdentifier() + " because it still exists on the Remote Instance"); } if (!port.getConnections().isEmpty() || port.hasIncomingConnection()) { throw new IllegalStateException("Cannot remove Remote Port because it is connected to other components"); } scheduler.stopPort(port); if (outputPorts.containsKey(port.getIdentifier())) { outputPorts.remove(port.getIdentifier()); } else { if (!inputPorts.containsKey(port.getIdentifier())) { throw new IllegalStateException("Cannot remove Remote Port because it does not belong to this Remote Process Group"); } inputPorts.remove(port.getIdentifier()); } } finally { writeLock.unlock(); } } @Override public void removeAllNonExistentPorts() { writeLock.lock(); try { final Set<String> inputPortIds = new HashSet<>(); final Set<String> outputPortIds = new HashSet<>(); for (final Map.Entry<String, StandardRemoteGroupPort> entry : inputPorts.entrySet()) { final RemoteGroupPort port = entry.getValue(); if (port.getTargetExists()) { continue; } // If there's a connection, we don't remove it. if (port.hasIncomingConnection()) { continue; } inputPortIds.add(entry.getKey()); } for (final Map.Entry<String, StandardRemoteGroupPort> entry : outputPorts.entrySet()) { final RemoteGroupPort port = entry.getValue(); if (port.getTargetExists()) { continue; } // If there's a connection, we don't remove it. if (!port.getConnections().isEmpty()) { continue; } outputPortIds.add(entry.getKey()); } for (final String id : inputPortIds) { inputPorts.remove(id); } for (final String id : outputPortIds) { outputPorts.remove(id); } } finally { writeLock.unlock(); } } /** * Adds an Output Port to this Remote Process Group that is described by * this DTO. * * @param descriptor * * @throws IllegalStateException if an Output Port already exists with the * ID given by dto.getId() */ private void addOutputPort(final RemoteProcessGroupPortDescriptor descriptor) { writeLock.lock(); try { if (outputPorts.containsKey(requireNonNull(descriptor).getId())) { throw new IllegalStateException("Output Port with ID " + descriptor.getId() + " already exists"); } final StandardRemoteGroupPort port = new StandardRemoteGroupPort(descriptor.getId(), descriptor.getName(), getProcessGroup(), this, TransferDirection.RECEIVE, ConnectableType.REMOTE_OUTPUT_PORT, sslContext, scheduler, nifiProperties); outputPorts.put(descriptor.getId(), port); if (descriptor.getConcurrentlySchedulableTaskCount() != null) { port.setMaxConcurrentTasks(descriptor.getConcurrentlySchedulableTaskCount()); } if (descriptor.getUseCompression() != null) { port.setUseCompression(descriptor.getUseCompression()); } if (descriptor.getBatchCount() != null && descriptor.getBatchCount() > 0) { port.setBatchCount(descriptor.getBatchCount()); } if (!StringUtils.isBlank(descriptor.getBatchSize())) { port.setBatchSize(descriptor.getBatchSize()); } if (!StringUtils.isBlank(descriptor.getBatchDuration())) { port.setBatchDuration(descriptor.getBatchDuration()); } } finally { writeLock.unlock(); } } /** * @param portIdentifier the ID of the Port to send FlowFiles to * @return {@link RemoteGroupPort} that can be used to send FlowFiles to the * port whose ID is given on the remote instance */ @Override public RemoteGroupPort getInputPort(final String portIdentifier) { readLock.lock(); try { if (requireNonNull(portIdentifier).startsWith(id + "-")) { return inputPorts.get(portIdentifier.substring(id.length() + 1)); } else { return inputPorts.get(portIdentifier); } } finally { readLock.unlock(); } } /** * @return a set of {@link OutgoingPort}s used for transmitting FlowFiles to * the remote instance */ @Override public Set<RemoteGroupPort> getInputPorts() { readLock.lock(); try { final Set<RemoteGroupPort> set = new HashSet<>(); set.addAll(inputPorts.values()); return set; } finally { readLock.unlock(); } } /** * Adds an InputPort to this ProcessGroup that is described by the given * DTO. * * @param descriptor port descriptor * * @throws IllegalStateException if an Input Port already exists with the ID * given by the ID of the DTO. */ private void addInputPort(final RemoteProcessGroupPortDescriptor descriptor) { writeLock.lock(); try { if (inputPorts.containsKey(descriptor.getId())) { throw new IllegalStateException("Input Port with ID " + descriptor.getId() + " already exists"); } final StandardRemoteGroupPort port = new StandardRemoteGroupPort(descriptor.getId(), descriptor.getName(), getProcessGroup(), this, TransferDirection.SEND, ConnectableType.REMOTE_INPUT_PORT, sslContext, scheduler, nifiProperties); if (descriptor.getConcurrentlySchedulableTaskCount() != null) { port.setMaxConcurrentTasks(descriptor.getConcurrentlySchedulableTaskCount()); } if (descriptor.getUseCompression() != null) { port.setUseCompression(descriptor.getUseCompression()); } if (descriptor.getBatchCount() != null && descriptor.getBatchCount() > 0) { port.setBatchCount(descriptor.getBatchCount()); } if (!StringUtils.isBlank(descriptor.getBatchSize())) { port.setBatchSize(descriptor.getBatchSize()); } if (!StringUtils.isBlank(descriptor.getBatchDuration())) { port.setBatchDuration(descriptor.getBatchDuration()); } inputPorts.put(descriptor.getId(), port); } finally { writeLock.unlock(); } } @Override public RemoteGroupPort getOutputPort(final String portIdentifier) { readLock.lock(); try { if (requireNonNull(portIdentifier).startsWith(id + "-")) { return outputPorts.get(portIdentifier.substring(id.length() + 1)); } else { return outputPorts.get(portIdentifier); } } finally { readLock.unlock(); } } /** * @return a set of {@link RemoteGroupPort}s used for receiving FlowFiles * from the remote instance */ @Override public Set<RemoteGroupPort> getOutputPorts() { readLock.lock(); try { final Set<RemoteGroupPort> set = new HashSet<>(); set.addAll(outputPorts.values()); return set; } finally { readLock.unlock(); } } @Override public String toString() { return "RemoteProcessGroup[" + targetUris + "]"; } @Override public ProcessGroupCounts getCounts() { readLock.lock(); try { return counts; } finally { readLock.unlock(); } } private void setCounts(final ProcessGroupCounts counts) { writeLock.lock(); try { this.counts = counts; } finally { writeLock.unlock(); } } @Override public Date getLastRefreshTime() { readLock.lock(); try { return refreshContentsTimestamp == null ? null : new Date(refreshContentsTimestamp); } finally { readLock.unlock(); } } @Override public void refreshFlowContents() throws CommunicationsException { try { // perform the request final ControllerDTO dto; try (final SiteToSiteRestApiClient apiClient = getSiteToSiteRestApiClient()) { dto = apiClient.getController(targetUris); } catch (IOException e) { writeLock.lock(); try { for (final Iterator<StandardRemoteGroupPort> iter = inputPorts.values().iterator(); iter.hasNext();) { final StandardRemoteGroupPort inputPort = iter.next(); if (!inputPort.hasIncomingConnection()) { iter.remove(); } } for (final Iterator<StandardRemoteGroupPort> iter = outputPorts.values().iterator(); iter.hasNext();) { final StandardRemoteGroupPort outputPort = iter.next(); if (outputPort.getConnections().isEmpty()) { iter.remove(); } } } finally { writeLock.unlock(); } throw new CommunicationsException("Unable to communicate with Remote NiFi at URI " + targetUris + " due to: " + e.getMessage()); } writeLock.lock(); try { if (dto.getInputPorts() != null) { setInputPorts(convertRemotePort(dto.getInputPorts())); } if (dto.getOutputPorts() != null) { setOutputPorts(convertRemotePort(dto.getOutputPorts())); } // set the controller details setTargetId(dto.getId()); setName(dto.getName()); setComments(dto.getComments()); // get the component counts int inputPortCount = 0; if (dto.getInputPortCount() != null) { inputPortCount = dto.getInputPortCount(); } int outputPortCount = 0; if (dto.getOutputPortCount() != null) { outputPortCount = dto.getOutputPortCount(); } int runningCount = 0; if (dto.getRunningCount() != null) { runningCount = dto.getRunningCount(); } int stoppedCount = 0; if (dto.getStoppedCount() != null) { stoppedCount = dto.getStoppedCount(); } int invalidCount = 0; if (dto.getInvalidCount() != null) { invalidCount = dto.getInvalidCount(); } int disabledCount = 0; if (dto.getDisabledCount() != null) { disabledCount = dto.getDisabledCount(); } int activeRemotePortCount = 0; if (dto.getActiveRemotePortCount() != null) { activeRemotePortCount = dto.getActiveRemotePortCount(); } int inactiveRemotePortCount = 0; if (dto.getInactiveRemotePortCount() != null) { inactiveRemotePortCount = dto.getInactiveRemotePortCount(); } this.listeningPort = dto.getRemoteSiteListeningPort(); this.listeningHttpPort = dto.getRemoteSiteHttpListeningPort(); this.destinationSecure = dto.isSiteToSiteSecure(); final ProcessGroupCounts newCounts = new ProcessGroupCounts(inputPortCount, outputPortCount, runningCount, stoppedCount, invalidCount, disabledCount, activeRemotePortCount, inactiveRemotePortCount); setCounts(newCounts); this.refreshContentsTimestamp = System.currentTimeMillis(); } finally { writeLock.unlock(); } } catch (final ClientHandlerException | UniformInterfaceException e) { throw new CommunicationsException(e); } } @Override public String getNetworkInterface() { readLock.lock(); try { return networkInterfaceName; } finally { readLock.unlock(); } } @Override public void setNetworkInterface(final String interfaceName) { writeLock.lock(); try { this.networkInterfaceName = interfaceName; if (interfaceName == null) { this.localAddress = null; this.nicValidationResult = null; } else { try { final Enumeration<InetAddress> inetAddresses = NetworkInterface.getByName(interfaceName).getInetAddresses(); if (inetAddresses.hasMoreElements()) { this.localAddress = inetAddresses.nextElement(); this.nicValidationResult = null; } else { this.localAddress = null; this.nicValidationResult = new ValidationResult.Builder() .input(interfaceName) .subject("Network Interface Name") .valid(false) .explanation("No IP Address could be found that is bound to the interface with name " + interfaceName) .build(); } } catch (final Exception e) { this.localAddress = null; this.nicValidationResult = new ValidationResult.Builder() .input(interfaceName) .subject("Network Interface Name") .valid(false) .explanation("Could not obtain Network Interface with name " + interfaceName) .build(); } } } finally { writeLock.unlock(); } } @Override public InetAddress getLocalAddress() { readLock.lock(); try { if (nicValidationResult != null && !nicValidationResult.isValid()) { return null; } return localAddress; } finally { readLock.unlock(); } } private SiteToSiteRestApiClient getSiteToSiteRestApiClient() { SiteToSiteRestApiClient apiClient = new SiteToSiteRestApiClient(sslContext, new HttpProxy(proxyHost, proxyPort, proxyUser, proxyPassword), getEventReporter()); apiClient.setConnectTimeoutMillis(getCommunicationsTimeout(TimeUnit.MILLISECONDS)); apiClient.setReadTimeoutMillis(getCommunicationsTimeout(TimeUnit.MILLISECONDS)); apiClient.setLocalAddress(getLocalAddress()); return apiClient; } /** * Converts a set of ports into a set of remote process group ports. * * @param ports to convert * @return descriptors of ports */ private Set<RemoteProcessGroupPortDescriptor> convertRemotePort(final Set<PortDTO> ports) { Set<RemoteProcessGroupPortDescriptor> remotePorts = null; if (ports != null) { remotePorts = new LinkedHashSet<>(ports.size()); for (final PortDTO port : ports) { final StandardRemoteProcessGroupPortDescriptor descriptor = new StandardRemoteProcessGroupPortDescriptor(); final ScheduledState scheduledState = ScheduledState.valueOf(port.getState()); descriptor.setId(port.getId()); descriptor.setName(port.getName()); descriptor.setComments(port.getComments()); descriptor.setTargetRunning(ScheduledState.RUNNING.equals(scheduledState)); remotePorts.add(descriptor); } } return remotePorts; } @Override public boolean isTransmitting() { return transmitting.get(); } @Override public void startTransmitting() { writeLock.lock(); try { verifyCanStartTransmitting(); for (final Port port : getInputPorts()) { // if port is not valid, don't start it because it will never become valid. // Validation is based on connections and whether or not the remote target exists. if (port.isValid() && port.hasIncomingConnection()) { scheduler.startPort(port); } } for (final Port port : getOutputPorts()) { if (port.isValid() && !port.getConnections().isEmpty()) { scheduler.startPort(port); } } transmitting.set(true); } finally { writeLock.unlock(); } } @Override public void startTransmitting(final RemoteGroupPort port) { writeLock.lock(); try { if (!inputPorts.containsValue(port) && !outputPorts.containsValue(port)) { throw new IllegalArgumentException("Port does not belong to this Remote Process Group"); } port.verifyCanStart(); scheduler.startPort(port); transmitting.set(true); } finally { writeLock.unlock(); } } @Override public void stopTransmitting() { writeLock.lock(); try { verifyCanStopTransmitting(); for (final RemoteGroupPort port : getInputPorts()) { scheduler.stopPort(port); } for (final RemoteGroupPort port : getOutputPorts()) { scheduler.stopPort(port); } // Wait for the ports to stop for (final RemoteGroupPort port : getInputPorts()) { while (port.isRunning()) { try { Thread.sleep(50L); } catch (final InterruptedException e) { } } } for (final RemoteGroupPort port : getOutputPorts()) { while (port.isRunning()) { try { Thread.sleep(50L); } catch (final InterruptedException e) { } } } transmitting.set(false); } finally { writeLock.unlock(); } } @Override public void stopTransmitting(final RemoteGroupPort port) { writeLock.lock(); try { if (!inputPorts.containsValue(port) && !outputPorts.containsValue(port)) { throw new IllegalArgumentException("Port does not belong to this Remote Process Group"); } port.verifyCanStop(); scheduler.stopPort(port); // Wait for the port to stop while (port.isRunning()) { try { Thread.sleep(50L); } catch (final InterruptedException e) { } } // Determine if any other ports are still running boolean stillTransmitting = false; for (final Port inputPort : getInputPorts()) { if (inputPort.isRunning()) { stillTransmitting = true; break; } } if (!stillTransmitting) { for (final Port outputPort : getOutputPorts()) { if (outputPort.isRunning()) { stillTransmitting = true; break; } } } transmitting.set(stillTransmitting); } finally { writeLock.unlock(); } } @Override public boolean isSecure() throws CommunicationsException { Boolean secure; readLock.lock(); try { secure = this.destinationSecure; if (secure != null) { return secure; } } finally { readLock.unlock(); } refreshFlowContents(); readLock.lock(); try { secure = this.destinationSecure; if (secure == null) { throw new CommunicationsException("Unable to determine whether or not site-to-site communications with peer should be secure"); } return secure; } finally { readLock.unlock(); } } @Override public Boolean getSecureFlag() { readLock.lock(); try { return this.destinationSecure; } finally { readLock.unlock(); } } @Override public boolean isSiteToSiteEnabled() { readLock.lock(); try { return (this.listeningPort != null || this.listeningHttpPort != null); } finally { readLock.unlock(); } } @Override public EventReporter getEventReporter() { return eventReporter; } private class InitializationTask implements Runnable { @Override public void run() { try (final SiteToSiteRestApiClient apiClient = getSiteToSiteRestApiClient()) { try { final ControllerDTO dto = apiClient.getController(targetUris); if (dto.getRemoteSiteListeningPort() == null && SiteToSiteTransportProtocol.RAW.equals(transportProtocol)) { authorizationIssue = "Remote instance is not configured to allow RAW Site-to-Site communications at this time."; } else if (dto.getRemoteSiteHttpListeningPort() == null && SiteToSiteTransportProtocol.HTTP.equals(transportProtocol)) { authorizationIssue = "Remote instance is not configured to allow HTTP Site-to-Site communications at this time."; } else { authorizationIssue = null; } writeLock.lock(); try { listeningPort = dto.getRemoteSiteListeningPort(); listeningHttpPort = dto.getRemoteSiteHttpListeningPort(); destinationSecure = dto.isSiteToSiteSecure(); } finally { writeLock.unlock(); } } catch (SiteToSiteRestApiClient.HttpGetFailedException e) { if (e.getResponseCode() == UNAUTHORIZED_STATUS_CODE) { try { // attempt to issue a registration request in case the target instance is a 0.x final boolean isApiSecure = apiClient.getBaseUrl().toLowerCase().startsWith("https"); final RemoteNiFiUtils utils = new RemoteNiFiUtils(isApiSecure ? sslContext : null); final ClientResponse requestAccountResponse = utils.issueRegistrationRequest(apiClient.getBaseUrl()); if (Response.Status.Family.SUCCESSFUL.equals(requestAccountResponse.getStatusInfo().getFamily())) { logger.info("{} Issued a Request to communicate with remote instance", this); } else { logger.error("{} Failed to request account: got unexpected response code of {}:{}", this, requestAccountResponse.getStatus(), requestAccountResponse.getStatusInfo().getReasonPhrase()); } } catch (final Exception re) { logger.error("{} Failed to request account due to {}", this, re.toString()); if (logger.isDebugEnabled()) { logger.error("", re); } } authorizationIssue = e.getDescription(); } else if (e.getResponseCode() == FORBIDDEN_STATUS_CODE) { authorizationIssue = e.getDescription(); } else { final String message = e.getDescription(); logger.warn("{} When communicating with remote instance, got unexpected result. {}", new Object[]{this, message}); authorizationIssue = "Unable to determine Site-to-Site availability."; } } } catch (final Exception e) { logger.warn(String.format("Unable to connect to %s due to %s", StandardRemoteProcessGroup.this, e)); getEventReporter().reportEvent(Severity.WARNING, "Site to Site", String.format("Unable to connect to %s due to %s", StandardRemoteProcessGroup.this.getTargetUris(), e)); } } } @Override public void setYieldDuration(final String yieldDuration) { // verify the syntax if (!FormatUtils.TIME_DURATION_PATTERN.matcher(yieldDuration).matches()) { throw new IllegalArgumentException("Improperly formatted Time Period; should be of syntax <number> <unit> where " + "<number> is a positive integer and unit is one of the valid Time Units, such as nanos, millis, sec, min, hour, day"); } this.yieldDuration = yieldDuration; } @Override public String getYieldDuration() { return yieldDuration; } @Override public void verifyCanDelete() { verifyCanDelete(false); } @Override public void verifyCanDelete(final boolean ignoreConnections) { readLock.lock(); try { if (isTransmitting()) { throw new IllegalStateException(this.getIdentifier() + " is transmitting"); } for (final Port port : inputPorts.values()) { if (!ignoreConnections && port.hasIncomingConnection()) { throw new IllegalStateException(this.getIdentifier() + " is the destination of another component"); } if (port.isRunning()) { throw new IllegalStateException(this.getIdentifier() + " has running Port: " + port.getIdentifier()); } } for (final Port port : outputPorts.values()) { if (!ignoreConnections) { for (final Connection connection : port.getConnections()) { connection.verifyCanDelete(); } } if (port.isRunning()) { throw new IllegalStateException(this.getIdentifier() + " has running Port: " + port.getIdentifier()); } } } finally { readLock.unlock(); } } @Override public void verifyCanStartTransmitting() { readLock.lock(); try { if (isTransmitting()) { throw new IllegalStateException(this.getIdentifier() + " is already transmitting"); } for (final StandardRemoteGroupPort port : inputPorts.values()) { if (port.isRunning()) { throw new IllegalStateException(this.getIdentifier() + " has running Port: " + port.getIdentifier()); } if (port.hasIncomingConnection() && !port.getTargetExists()) { throw new IllegalStateException(this.getIdentifier() + " has a Connection to Port " + port.getIdentifier() + ", but that Port no longer exists on the remote system"); } if (port.hasIncomingConnection()) { port.verifyCanStart(); } } for (final StandardRemoteGroupPort port : outputPorts.values()) { if (port.isRunning()) { throw new IllegalStateException(this.getIdentifier() + " has running Port: " + port.getIdentifier()); } if (!port.getConnections().isEmpty() && !port.getTargetExists()) { throw new IllegalStateException(this.getIdentifier() + " has a Connection to Port " + port.getIdentifier() + ", but that Port no longer exists on the remote system"); } if (!port.getConnections().isEmpty()) { port.verifyCanStart(); } } } finally { readLock.unlock(); } } @Override public void verifyCanStopTransmitting() { if (!isTransmitting()) { throw new IllegalStateException(this.getIdentifier() + " is not transmitting"); } } @Override public void verifyCanUpdate() { readLock.lock(); try { if (isTransmitting()) { throw new IllegalStateException(this.getIdentifier() + " is currently transmitting"); } for (final Port port : inputPorts.values()) { if (port.isRunning()) { throw new IllegalStateException(this.getIdentifier() + " has running Port: " + port.getIdentifier()); } } for (final Port port : outputPorts.values()) { if (port.isRunning()) { throw new IllegalStateException(this.getIdentifier() + " has running Port: " + port.getIdentifier()); } } } finally { readLock.unlock(); } } private File getPeerPersistenceFile() { final File stateDir = nifiProperties.getPersistentStateDirectory(); return new File(stateDir, getIdentifier() + ".peers"); } }