001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2008-2010 Sun Microsystems, Inc.
015 * Portions Copyright 2011-2015 ForgeRock AS.
016 */
017package org.opends.admin.ads;
018
019import java.util.Collection;
020import java.util.Date;
021import java.util.HashMap;
022import java.util.HashSet;
023import java.util.Iterator;
024import java.util.LinkedHashSet;
025import java.util.Map;
026import java.util.Set;
027
028import javax.naming.NameNotFoundException;
029import javax.naming.NamingEnumeration;
030import javax.naming.NamingException;
031import javax.naming.directory.SearchControls;
032import javax.naming.directory.SearchResult;
033import javax.naming.ldap.InitialLdapContext;
034import javax.naming.ldap.LdapName;
035
036import org.forgerock.i18n.LocalizableMessage;
037import org.forgerock.i18n.slf4j.LocalizedLogger;
038import org.opends.admin.ads.ADSContext.ServerProperty;
039import org.opends.admin.ads.util.ApplicationTrustManager;
040import org.opends.admin.ads.util.ConnectionUtils;
041import org.opends.admin.ads.util.PreferredConnection;
042import org.opends.admin.ads.util.ServerLoader;
043import org.opends.quicksetup.util.Utils;
044
045import static com.forgerock.opendj.cli.Utils.*;
046
047import static org.opends.messages.QuickSetupMessages.*;
048
049/**
050 * This class allows to read the configuration of the different servers that are
051 * registered in a given ADS server. It provides a read only view of the
052 * configuration of the servers and of the replication topologies that might be
053 * configured between them.
054 */
055public class TopologyCache
056{
057
058  private final ADSContext adsContext;
059  private final ApplicationTrustManager trustManager;
060  private final int timeout;
061  private final String bindDN;
062  private final String bindPwd;
063  private final Set<ServerDescriptor> servers = new HashSet<>();
064  private final Set<SuffixDescriptor> suffixes = new HashSet<>();
065  private final Set<PreferredConnection> preferredConnections = new LinkedHashSet<>();
066  private final TopologyCacheFilter filter = new TopologyCacheFilter();
067  private static final int MULTITHREAD_TIMEOUT = 90 * 1000;
068  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
069
070  /**
071   * Constructor of the TopologyCache.
072   *
073   * @param adsContext the adsContext to the ADS registry.
074   * @param trustManager the ApplicationTrustManager that must be used to trust
075   * certificates when we create connections to the registered servers to read
076   * their configuration.
077   * @param timeout the timeout to establish the connection in milliseconds.
078   * Use {@code 0} to express no timeout.
079   */
080  public TopologyCache(ADSContext adsContext,
081                       ApplicationTrustManager trustManager,
082                       int timeout)
083  {
084    this.adsContext = adsContext;
085    this.trustManager = trustManager;
086    this.timeout = timeout;
087    bindDN = ConnectionUtils.getBindDN(adsContext.getDirContext());
088    bindPwd = ConnectionUtils.getBindPassword(adsContext.getDirContext());
089  }
090
091  /**
092   * Reads the configuration of the registered servers.
093   *
094   * @throws TopologyCacheException if there is an issue reading the
095   * configuration of the registered servers.
096   */
097  public void reloadTopology() throws TopologyCacheException
098  {
099    suffixes.clear();
100    servers.clear();
101    try
102    {
103      Set<Map<ServerProperty, Object>> adsServers =
104          adsContext.readServerRegistry();
105
106      Set<ServerLoader> threadSet = new HashSet<>();
107      for (Map<ServerProperty, Object> serverProperties : adsServers)
108      {
109        ServerLoader t = getServerLoader(serverProperties);
110        t.start();
111        threadSet.add(t);
112      }
113      joinThreadSet(threadSet);
114      /*
115       * Try to consolidate things (even if the data is not complete).
116       */
117
118      HashMap<LdapName, Set<SuffixDescriptor>> hmSuffixes = new HashMap<>();
119      for (ServerLoader loader : threadSet)
120      {
121        ServerDescriptor descriptor = loader.getServerDescriptor();
122        for (ReplicaDescriptor replica : descriptor.getReplicas())
123        {
124          logger.info(LocalizableMessage.raw("Handling replica with dn: "
125              + replica.getSuffix().getDN()));
126
127          boolean suffixFound = false;
128          LdapName dn = new LdapName(replica.getSuffix().getDN());
129          Set<SuffixDescriptor> sufs = hmSuffixes.get(dn);
130          if (sufs != null)
131          {
132            Iterator<SuffixDescriptor> it = sufs.iterator();
133            while (it.hasNext() && !suffixFound)
134            {
135              SuffixDescriptor suffix = it.next();
136              Iterator<String> it2 = suffix.getReplicationServers().iterator();
137              while (it2.hasNext() && !suffixFound)
138              {
139                if (replica.getReplicationServers().contains(it2.next()))
140                {
141                  suffixFound = true;
142                  Set<ReplicaDescriptor> replicas = suffix.getReplicas();
143                  replicas.add(replica);
144                  suffix.setReplicas(replicas);
145                  replica.setSuffix(suffix);
146                }
147              }
148            }
149          }
150          if (!suffixFound)
151          {
152            if (sufs == null)
153            {
154              sufs = new HashSet<>();
155              hmSuffixes.put(dn, sufs);
156            }
157            sufs.add(replica.getSuffix());
158            suffixes.add(replica.getSuffix());
159          }
160        }
161        servers.add(descriptor);
162      }
163
164      // Figure out the replication monitoring if it is required.
165      if (getFilter().searchMonitoringInformation())
166      {
167        readReplicationMonitoring();
168      }
169    }
170    catch (ADSContextException ade)
171    {
172      throw new TopologyCacheException(ade);
173    }
174    catch (Throwable t)
175    {
176      throw new TopologyCacheException(TopologyCacheException.Type.BUG, t);
177    }
178  }
179
180  /**
181   * Returns the trust manager used by this class.
182   *
183   * @return the trust manager used by this class.
184   */
185  public ApplicationTrustManager getTrustManager()
186  {
187    return trustManager;
188  }
189
190  /**
191   * Returns the timeout to establish the connection in milliseconds.
192   *
193   * @return the timeout to establish the connection in milliseconds. Returns
194   * {@code 0} to express no timeout.
195   */
196  public int getConnectTimeout()
197  {
198    return timeout;
199  }
200
201  /**
202   * Reads the replication monitoring.
203   */
204  private void readReplicationMonitoring()
205  {
206    Set<ReplicaDescriptor> replicasToUpdate = getReplicasToUpdate();
207    for (ServerDescriptor server : getServers())
208    {
209      if (server.isReplicationServer())
210      {
211        // If is replication server, then at least we were able to read the
212        // configuration, so assume that we might be able to read monitoring
213        // (even if an exception occurred before).
214        Set<ReplicaDescriptor> candidateReplicas = getCandidateReplicas(server);
215        if (!candidateReplicas.isEmpty())
216        {
217          Set<ReplicaDescriptor> updatedReplicas = new HashSet<>();
218          try
219          {
220            updateReplicas(server, candidateReplicas, updatedReplicas);
221          }
222          catch (NamingException ne)
223          {
224            server.setLastException(new TopologyCacheException(
225                TopologyCacheException.Type.GENERIC_READING_SERVER, ne));
226          }
227          replicasToUpdate.removeAll(updatedReplicas);
228        }
229      }
230
231      if (replicasToUpdate.isEmpty())
232      {
233        break;
234      }
235    }
236  }
237
238  private Set<ReplicaDescriptor> getReplicasToUpdate()
239  {
240    Set<ReplicaDescriptor> replicasToUpdate = new HashSet<>();
241    for (ServerDescriptor server : getServers())
242    {
243      for (ReplicaDescriptor replica : server.getReplicas())
244      {
245        if (replica.isReplicated())
246        {
247          replicasToUpdate.add(replica);
248        }
249      }
250    }
251    return replicasToUpdate;
252  }
253
254  private Set<ReplicaDescriptor> getCandidateReplicas(ServerDescriptor server)
255  {
256    Set<ReplicaDescriptor> candidateReplicas = new HashSet<>();
257    // It contains replication information: analyze it.
258    String repServer = server.getReplicationServerHostPort();
259    for (SuffixDescriptor suffix : getSuffixes())
260    {
261      if (containsIgnoreCase(suffix.getReplicationServers(), repServer))
262      {
263        candidateReplicas.addAll(suffix.getReplicas());
264      }
265    }
266    return candidateReplicas;
267  }
268
269  private boolean containsIgnoreCase(Set<String> col, String toFind)
270  {
271    for (String s : col)
272    {
273      if (s.equalsIgnoreCase(toFind))
274      {
275        return true;
276      }
277    }
278    return false;
279  }
280
281  /**
282   * Sets the list of LDAP URLs and connection type that are preferred to be
283   * used to connect to the servers. When we have a server to which we can
284   * connect using a URL on the list we will try to use it.
285   *
286   * @param cnx the list of preferred connections.
287   */
288  public void setPreferredConnections(Set<PreferredConnection> cnx)
289  {
290    preferredConnections.clear();
291    preferredConnections.addAll(cnx);
292  }
293
294  /**
295   * Returns the list of LDAP URLs and connection type that are preferred to be
296   * used to connect to the servers. If a URL is on this list, when we have a
297   * server to which we can connect using that URL and the associated connection
298   * type we will try to use it.
299   *
300   * @return the list of preferred connections.
301   */
302  public LinkedHashSet<PreferredConnection> getPreferredConnections()
303  {
304    return new LinkedHashSet<>(preferredConnections);
305  }
306
307  /**
308   * Returns a Set containing all the servers that are registered in the ADS.
309   *
310   * @return a Set containing all the servers that are registered in the ADS.
311   */
312  public Set<ServerDescriptor> getServers()
313  {
314    return new HashSet<>(servers);
315  }
316
317  /**
318   * Returns a Set containing the suffixes (replication topologies) that could
319   * be retrieved after the last call to reloadTopology.
320   *
321   * @return a Set containing the suffixes (replication topologies) that could
322   * be retrieved after the last call to reloadTopology.
323   */
324  public Set<SuffixDescriptor> getSuffixes()
325  {
326    return new HashSet<>(suffixes);
327  }
328
329  /**
330   * Returns the filter to be used when retrieving information.
331   *
332   * @return the filter to be used when retrieving information.
333   */
334  public TopologyCacheFilter getFilter()
335  {
336    return filter;
337  }
338
339  /**
340   * Method used to wait at most a certain time (MULTITHREAD_TIMEOUT) for the
341   * different threads to finish.
342   *
343   * @param threadSet the list of threads (we assume that they are started) that
344   * we must wait for.
345   */
346  private void joinThreadSet(Set<ServerLoader> threadSet)
347  {
348    Date startDate = new Date();
349    for (ServerLoader t : threadSet)
350    {
351      long timeToJoin = MULTITHREAD_TIMEOUT - System.currentTimeMillis()
352          + startDate.getTime();
353      try
354      {
355        if (timeToJoin > 0)
356        {
357          t.join(MULTITHREAD_TIMEOUT);
358        }
359      }
360      catch (InterruptedException ie)
361      {
362        logger.info(LocalizableMessage.raw(ie + " caught and ignored", ie));
363      }
364      if (t.isAlive())
365      {
366        t.interrupt();
367      }
368    }
369    Date endDate = new Date();
370    long workingTime = endDate.getTime() - startDate.getTime();
371    logger.info(LocalizableMessage.raw("Loading ended at " + workingTime + " ms"));
372  }
373
374  /**
375   * Creates a ServerLoader object based on the provided server properties.
376   *
377   * @param serverProperties the server properties to be used to generate the
378   * ServerLoader.
379   * @return a ServerLoader object based on the provided server properties.
380   */
381  private ServerLoader getServerLoader(
382      Map<ServerProperty, Object> serverProperties)
383  {
384    return new ServerLoader(serverProperties, bindDN, bindPwd,
385        trustManager == null ? null : trustManager.createCopy(),
386        timeout,
387        getPreferredConnections(), getFilter());
388  }
389
390  /**
391   * Returns the adsContext used by this TopologyCache.
392   *
393   * @return the adsContext used by this TopologyCache.
394   */
395  public ADSContext getAdsContext()
396  {
397    return adsContext;
398  }
399
400  /**
401   * Returns a set of error messages encountered in the TopologyCache.
402   *
403   * @return a set of error messages encountered in the TopologyCache.
404   */
405  public Set<LocalizableMessage> getErrorMessages()
406  {
407    Set<TopologyCacheException> exceptions = new HashSet<>();
408    Set<ServerDescriptor> theServers = getServers();
409    Set<LocalizableMessage> exceptionMsgs = new LinkedHashSet<>();
410    for (ServerDescriptor server : theServers)
411    {
412      TopologyCacheException e = server.getLastException();
413      if (e != null)
414      {
415        exceptions.add(e);
416      }
417    }
418    /*
419     * Check the exceptions and see if we throw them or not.
420     */
421    for (TopologyCacheException e : exceptions)
422    {
423      switch (e.getType())
424      {
425        case NOT_GLOBAL_ADMINISTRATOR:
426          exceptionMsgs.add(INFO_NOT_GLOBAL_ADMINISTRATOR_PROVIDED.get());
427
428          break;
429        case GENERIC_CREATING_CONNECTION:
430          if (isCertificateException(e.getCause()))
431          {
432            exceptionMsgs.add(
433                INFO_ERROR_READING_CONFIG_LDAP_CERTIFICATE_SERVER.get(
434                e.getHostPort(), e.getCause().getMessage()));
435          }
436          else
437          {
438            exceptionMsgs.add(Utils.getMessage(e));
439          }
440          break;
441        default:
442          exceptionMsgs.add(Utils.getMessage(e));
443      }
444    }
445    return exceptionMsgs;
446  }
447
448  /**
449   * Updates the monitoring information of the provided replicas using the
450   * information located in cn=monitor of a given replication server.
451   *
452   * @param replicationServer the replication server.
453   * @param candidateReplicas the collection of replicas that must be updated.
454   * @param updatedReplicas the collection of replicas that are actually
455   * updated. This list is updated by the method.
456   */
457  private void updateReplicas(ServerDescriptor replicationServer,
458                              Collection<ReplicaDescriptor> candidateReplicas,
459                              Collection<ReplicaDescriptor> updatedReplicas)
460      throws NamingException
461  {
462    SearchControls ctls = new SearchControls();
463    ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
464    ctls.setReturningAttributes(
465        new String[]
466        {
467          "approx-older-change-not-synchronized-millis", "missing-changes",
468          "domain-name", "server-id"
469        });
470
471    InitialLdapContext ctx = null;
472    NamingEnumeration<SearchResult> monitorEntries = null;
473    try
474    {
475      ServerLoader loader =
476          getServerLoader(replicationServer.getAdsProperties());
477      ctx = loader.createContext();
478
479      monitorEntries = ctx.search(
480          new LdapName("cn=monitor"), "(missing-changes=*)", ctls);
481
482      while (monitorEntries.hasMore())
483      {
484        SearchResult sr = monitorEntries.next();
485
486        String dn = ConnectionUtils.getFirstValue(sr, "domain-name");
487        int replicaId = -1;
488        try
489        {
490          String sid = ConnectionUtils.getFirstValue(sr, "server-id");
491          if (sid == null)
492          {
493            // This is not a replica, but a replication server. Skip it
494            continue;
495          }
496          replicaId = Integer.valueOf(sid);
497        }
498        catch (Throwable t)
499        {
500          logger.warn(LocalizableMessage.raw("Unexpected error reading replica ID: " + t,
501              t));
502        }
503
504        for (ReplicaDescriptor replica : candidateReplicas)
505        {
506          if (Utils.areDnsEqual(dn, replica.getSuffix().getDN())
507              && replica.isReplicated()
508              && replica.getReplicationId() == replicaId)
509          {
510            // This statistic is optional.
511            setAgeOfOldestMissingChange(replica, sr);
512            setMissingChanges(replica, sr);
513            updatedReplicas.add(replica);
514          }
515        }
516      }
517    }
518    catch (NameNotFoundException nse)
519    {
520    }
521    finally
522    {
523      if (monitorEntries != null)
524      {
525        try
526        {
527          monitorEntries.close();
528        }
529        catch (Throwable t)
530        {
531          logger.warn(LocalizableMessage.raw(
532              "Unexpected error closing enumeration on monitor entries" + t, t));
533        }
534      }
535      if (ctx != null)
536      {
537        ctx.close();
538      }
539    }
540  }
541
542  private void setMissingChanges(ReplicaDescriptor replica, SearchResult sr) throws NamingException
543  {
544    String s = ConnectionUtils.getFirstValue(sr, "missing-changes");
545    if (s != null)
546    {
547      try
548      {
549        replica.setMissingChanges(Integer.valueOf(s));
550      }
551      catch (Throwable t)
552      {
553        logger.warn(LocalizableMessage.raw(
554            "Unexpected error reading missing changes: " + t, t));
555      }
556    }
557  }
558
559  private void setAgeOfOldestMissingChange(ReplicaDescriptor replica, SearchResult sr) throws NamingException
560  {
561    String s = ConnectionUtils.getFirstValue(sr, "approx-older-change-not-synchronized-millis");
562    if (s != null)
563    {
564      try
565      {
566        replica.setAgeOfOldestMissingChange(Long.valueOf(s));
567      }
568      catch (Throwable t)
569      {
570        logger.warn(LocalizableMessage.raw(
571            "Unexpected error reading age of oldest change: " + t, t));
572      }
573    }
574  }
575}