View Javadoc
1   package org.kuali.common.devops.logic;
2   
3   import static com.google.common.collect.Iterables.filter;
4   import static com.google.common.collect.Lists.newArrayList;
5   import static com.google.common.collect.Maps.newHashMap;
6   import static com.google.common.collect.Maps.newTreeMap;
7   import static com.google.common.collect.Sets.newHashSet;
8   import static java.lang.String.format;
9   import static org.apache.commons.io.FileUtils.readFileToString;
10  import static org.apache.commons.io.FileUtils.write;
11  import static org.apache.commons.lang3.StringUtils.removeEnd;
12  import static org.kuali.common.devops.archive.sweep.Functions.hostnameToKey;
13  import static org.kuali.common.devops.logic.Auth.getDNSMECredentials;
14  import static org.kuali.common.dns.dnsme.DNSME.DNSME_REST_API_URL_PRODUCTION;
15  import static org.kuali.common.dns.model.DnsRecordType.CNAME;
16  import static org.kuali.common.util.FormatUtils.getCount;
17  import static org.kuali.common.util.FormatUtils.getTime;
18  import static org.kuali.common.util.base.Exceptions.illegalState;
19  import static org.kuali.common.util.log.Loggers.newLogger;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.Properties;
26  import java.util.Set;
27  
28  import org.kuali.common.core.json.api.JsonService;
29  import org.kuali.common.core.json.jackson.JacksonJsonService;
30  import org.kuali.common.dns.api.DnsService;
31  import org.kuali.common.dns.dnsme.DNSMadeEasyDnsService;
32  import org.kuali.common.dns.dnsme.model.DNSMadeEasyCredentials;
33  import org.kuali.common.dns.dnsme.model.DNSMadeEasyServiceContext;
34  import org.kuali.common.dns.model.SimpleDnsRecord;
35  import org.kuali.common.util.file.CanonicalFile;
36  import org.kuali.common.util.property.ImmutableProperties;
37  import org.slf4j.Logger;
38  
39  import com.google.common.base.Function;
40  import com.google.common.base.Predicate;
41  import com.google.common.base.Stopwatch;
42  import com.google.common.collect.BiMap;
43  import com.google.common.collect.HashBiMap;
44  import com.google.common.collect.HashMultiset;
45  import com.google.common.collect.ImmutableBiMap;
46  import com.google.common.collect.ImmutableList;
47  import com.google.common.collect.ImmutableMap;
48  import com.google.common.collect.Multiset;
49  import com.google.common.collect.Ordering;
50  import com.google.common.collect.Sets;
51  
52  public class DNS {
53  
54  	private static final Logger logger = newLogger();
55  	private static final String DOMAIN = "kuali.org";
56  	private static final File CACHE = new CanonicalFile("./target/cache/dns/records.json");
57  
58  	/**
59  	 * Return a mapping of aliases to canonical name records. There can be multiple aliases pointing to the same canonical name record.
60  	 * 
61  	 * <pre>
62  	 * alias                  canonical
63  	 * env1.rice.kuali.org -> ec2-174-129-109-246.compute-1.amazonaws.com
64  	 * </pre>
65  	 */
66  	public static Map<String, String> getAliasMap(boolean refresh) {
67  		List<SimpleDnsRecord> records = getDnsRecords(refresh);
68  		info("records -> %s", getCount(records));
69  		return asCNAMEAliasFQDNMap(records);
70  	}
71  
72  	private static List<SimpleDnsRecord> getDnsRecords(boolean refresh) {
73  		if (refresh || !CACHE.exists()) {
74  			info("domain  -> %s", DOMAIN);
75  			List<SimpleDnsRecord> records = queryProvider();
76  			store(records);
77  			return records;
78  		} else {
79  			return load();
80  		}
81  	}
82  
83  	/**
84  	 * Return a mapping of canonical name records to aliases. The only canonical name records present in this map are those that contain one (and only one) alias.
85  	 * 
86  	 * This allows you to deterministically find the correct alias if you have the canonical name record.
87  	 * 
88  	 * <pre>
89  	 * canonical                                      alias
90  	 * ec2-174-129-109-246.compute-1.amazonaws.com -> env1.rice.kuali.org
91  	 * </pre>
92  	 */
93  	public static BiMap<String, String> getCanonicalMap(boolean refresh) {
94  		Map<String, String> map = newHashMap(getAliasMap(refresh));
95  		removeAllKeysWithDuplicateValues(map);
96  		BiMap<String, String> aliases = HashBiMap.create(map);
97  		BiMap<String, String> canonical = aliases.inverse();
98  		info("using   -> %s uniquely aliased CNAME records", getCount(canonical));
99  		return ImmutableBiMap.copyOf(canonical);
100 	}
101 
102 	protected static <T> void removeAllKeysWithDuplicateValues(Map<?, T> map) {
103 		int oldSize = map.size();
104 		Set<T> duplicates = getDuplicateValues(map);
105 		Set<?> keys = newHashSet(map.keySet());
106 		for (Object key : keys) {
107 			T value = map.get(key);
108 			if (duplicates.contains(value)) {
109 				debug("remove  -> %s", key);
110 				map.remove(key);
111 			}
112 		}
113 		int newSize = map.size();
114 		int removed = oldSize - newSize;
115 		info("removed -> %s CNAME records with duplicate aliases", getCount(removed));
116 	}
117 
118 	protected static <T> Set<T> getDuplicateValues(Map<?, T> map) {
119 		Multiset<T> multi = HashMultiset.create();
120 		multi.addAll(map.values());
121 		Set<T> duplicates = Sets.newHashSet();
122 		Set<T> elements = multi.elementSet();
123 		for (T element : elements) {
124 			if (multi.count(element) > 1) {
125 				duplicates.add(element);
126 			}
127 		}
128 		return duplicates;
129 	}
130 
131 	/**
132 	 * Grab all the DNS records known to this provider
133 	 */
134 	protected static List<SimpleDnsRecord> queryProvider() {
135 		DNSMadeEasyCredentials credentials = getDNSMECredentials();
136 		String url = DNSME_REST_API_URL_PRODUCTION;
137 		String domain = DOMAIN;
138 		DNSMadeEasyServiceContext context = new DNSMadeEasyServiceContext(credentials, url, domain);
139 		DnsService dns = new DNSMadeEasyDnsService(context);
140 		return dns.getRecords();
141 	}
142 
143 	/**
144 	 * Filter the list down to just CNAME records.<br>
145 	 * Convert the DNS record name into a valid FQDN by appending the domain name.<br>
146 	 * Convert the DNS record value into a valid FQDN by remove the trailing dot (if there is one)<br>
147 	 */
148 	protected static Map<String, String> asCNAMEAliasFQDNMap(List<SimpleDnsRecord> records) {
149 		List<SimpleDnsRecord> filtered = newArrayList(filter(records, CNAMEPredicate.INSTANCE));
150 		Map<String, String> map = newTreeMap();
151 		for (SimpleDnsRecord record : filtered) {
152 			String alias = record.getName() + "." + DOMAIN;
153 			String cname = removeEnd(record.getValue(), ".");
154 			map.put(alias, cname);
155 		}
156 		int removed = records.size() - filtered.size();
157 		info("removed -> %s records that were not CNAME's", getCount(removed));
158 		return ImmutableMap.copyOf(map);
159 	}
160 
161 	protected static List<SimpleDnsRecord> load() {
162 		JsonService json = new JacksonJsonService();
163 		try {
164 			info("load    -> %s", CACHE);
165 			String content = readFileToString(CACHE);
166 			return ImmutableList.copyOf(json.readString(content, SimpleDnsRecord[].class));
167 		} catch (IOException e) {
168 			throw illegalState(e);
169 		}
170 	}
171 
172 	protected static void store(List<SimpleDnsRecord> records) {
173 		JsonService json = new JacksonJsonService();
174 		Function<SimpleDnsRecord, String> sorter = ReverseHostnameFunction.INSTANCE;
175 		List<SimpleDnsRecord> sorted = Ordering.natural().onResultOf(sorter).sortedCopy(records);
176 		String data = json.writeString(sorted);
177 		try {
178 			info("create  -> %s", CACHE);
179 			write(CACHE, data);
180 		} catch (IOException e) {
181 			throw illegalState(e);
182 		}
183 	}
184 
185 	protected static Properties convert(Map<String, String> map) {
186 		Properties props = new Properties();
187 		props.putAll(map);
188 		return ImmutableProperties.copyOf(props);
189 	}
190 
191 	protected static Map<String, String> convert(Properties props) {
192 		Map<String, String> map = newTreeMap();
193 		for (String key : props.stringPropertyNames()) {
194 			map.put(key, props.getProperty(key));
195 		}
196 		return ImmutableMap.copyOf(map);
197 	}
198 
199 	private enum CNAMEPredicate implements Predicate<SimpleDnsRecord> {
200 		INSTANCE;
201 
202 		@Override
203 		public boolean apply(SimpleDnsRecord record) {
204 			return record.getType().equals(CNAME);
205 		}
206 	}
207 
208 	private enum ReverseHostnameFunction implements Function<SimpleDnsRecord, String> {
209 		INSTANCE;
210 
211 		@Override
212 		public String apply(SimpleDnsRecord record) {
213 			return hostnameToKey().apply(record.getName());
214 		}
215 	}
216 
217 	protected static void elapsed(Stopwatch sw) {
218 		info("elapsed -> %s", getTime(sw));
219 	}
220 
221 	protected static void debug(String msg, Object... args) {
222 		logger.debug((args == null || args.length == 0) ? msg : format(msg, args));
223 	}
224 
225 	protected static void info(String msg, Object... args) {
226 		logger.info((args == null || args.length == 0) ? msg : format(msg, args));
227 	}
228 
229 }