package io.fathom.cloud.dns.backend;
import io.fathom.cloud.Clock;
import io.fathom.cloud.CloudException;
import io.fathom.cloud.dns.DnsService;
import io.fathom.cloud.dns.model.DnsRecord;
import io.fathom.cloud.dns.model.DnsRecordset;
import io.fathom.cloud.dns.model.DnsZone;
import io.fathom.cloud.dns.services.DnsServiceImpl;
import io.fathom.cloud.dns.state.DnsRepository;
import io.fathom.cloud.openstack.client.dns.model.Record;
import io.fathom.cloud.openstack.client.dns.model.Recordset;
import io.fathom.cloud.protobuf.DnsModel.DnsRecordData;
import io.fathom.cloud.protobuf.DnsModel.DnsRecordsetData;
import io.fathom.cloud.server.model.Project;
import io.fathom.cloud.state.StoreOptions;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Callable;
import javax.inject.Inject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
public abstract class DnsBackendBase implements DnsBackend {
private static final Logger log = LoggerFactory.getLogger(DnsBackendBase.class);
@Inject
protected DnsRepository repository;
@Inject
protected DnsServiceImpl dns;
public abstract class UpdateDnsDomainBase implements Callable<Void> {
protected final Project project;
protected final DnsZone zone;
public UpdateDnsDomainBase(Project project, DnsZone zone) {
this.project = project;
this.zone = zone;
}
public class Changes {
public List<Recordset> remove = Lists.newArrayList();
public List<Recordset> create = Lists.newArrayList();
}
protected Changes computeChanges(List<Recordset> current, List<Recordset> desired) {
Multimap<String, Recordset> currentMap = HashMultimap.create();
for (Recordset currentRecordset : current) {
String key = currentRecordset.type + ":" + currentRecordset.name;
currentMap.put(key, currentRecordset);
}
Changes changes = new Changes();
Set<String> managedNames = Sets.newConcurrentHashSet();
Multimap<String, Recordset> matches = HashMultimap.create();
for (Recordset desiredRecordset : desired) {
String key = desiredRecordset.type + ":" + desiredRecordset.name;
managedNames.add(key);
Recordset found = null;
for (Recordset rrs : currentMap.get(key)) {
if (!rrs.matches(desiredRecordset)) {
continue;
}
found = rrs;
break;
}
if (found == null) {
// Not found; need to create
log.debug("Creating record: {}", desiredRecordset);
if (!desiredRecordset.isDeleted()) {
changes.create.add(desiredRecordset);
}
} else {
log.debug("Matched existing record: {}", found);
matches.put(key, found);
}
}
for (Entry<String, Recordset> entry : currentMap.entries()) {
String key = entry.getKey();
if (!managedNames.contains(key)) {
// Note: we rely on state to keep around deleted records
// (currently forever!)
// Also, we can't do record rename / retype
log.debug("Record not managed by this domain: {}", key);
continue;
}
Recordset rrs = entry.getValue();
boolean found = false;
for (Recordset i : matches.get(entry.getKey())) {
if (rrs.matches(i)) {
found = true;
break;
}
}
if (!found) {
log.debug("Deleting record: {}", rrs);
changes.remove.add(rrs);
} else {
log.debug("Found matching record: {}", rrs);
}
}
return changes;
}
protected List<Recordset> readFromDatabase(boolean createSoa) throws CloudException {
String zoneName = zone.getName();
List<DnsRecordset> records = dns.listRecordsets(project, zone, StoreOptions.ShowDeleted);
List<Recordset> recordsets = Lists.newArrayList();
boolean hasSoa = false;
Set<String> ids = Sets.newHashSet();
for (DnsRecordset recordset : records) {
DnsRecordsetData data = recordset.getData();
Recordset r = new Recordset();
String fqdn = recordset.getFqdn();
r.name = fqdn;
r.id = "" + data.getId();
if (ids.contains(r.id)) {
throw new IllegalStateException();
}
ids.add(r.id);
r.zone_id = "" + data.getZoneId();
r.weight = null;
r.ttl = null;
r.type = data.getType();
if (data.hasState()) {
r.deleted_at = Clock.toDate(data.getState().getDeletedAt());
}
if (DnsService.TYPE_SOA.equalsIgnoreCase(r.type)) {
hasSoa = true;
}
r.records = Lists.newArrayList();
for (DnsRecord dnsRecord : recordset.getRecords()) {
Record record = new Record();
DnsRecordData recordData = dnsRecord.getData();
record.value = recordData.getTarget();
if (recordData.hasWeight()) {
record.weight = recordData.getWeight();
}
if (recordData.hasPort()) {
record.port = recordData.getPort();
}
if (recordData.hasPriority()) {
record.priority = recordData.getPriority();
}
r.records.add(record);
}
recordsets.add(r);
}
if (createSoa && !hasSoa) {
Recordset r = new Recordset();
r.name = zoneName;
// primary hostmaster serial refresh retry expire default_ttl
String primary = "ns." + zoneName;
String hostmaster = "hostmaster@" + zoneName;
String serial = Long.toString(System.currentTimeMillis() / 1000L);
int refresh = 7200;
int retry = 900;
int expire = 1209600;
int defaultTtl = 86400;
r.records = Lists.newArrayList();
{
Record record = new Record();
record.value = primary + " " + hostmaster + " " + serial + " " + refresh + " " + retry + " "
+ expire + " " + defaultTtl;
r.records.add(record);
}
int id = 1;
while (ids.contains(Integer.toString(id))) {
id++;
}
r.id = Integer.toString(id);
ids.add(r.id);
r.zone_id = "" + zone.getId();
r.weight = null;
r.ttl = null;
r.type = DnsService.TYPE_SOA;
log.info("Adding synthetic SOA record: {}", r);
recordsets.add(r);
}
return recordsets;
}
}
}