package io.fathom.cloud.lbaas.services; import io.fathom.cloud.CloudException; import io.fathom.cloud.dns.DnsService; import io.fathom.cloud.dns.DnsService.DnsRecordSpec; import io.fathom.cloud.dns.DnsService.DnsRecordsetSpec; import io.fathom.cloud.lbaas.backend.LbaasBackend; import io.fathom.cloud.lbaas.backend.LbaasBackends; import io.fathom.cloud.lbaas.state.LbaasRepository; import io.fathom.cloud.loadbalancer.LoadBalanceService; import io.fathom.cloud.openstack.client.loadbalance.model.LbaasMapping; import io.fathom.cloud.openstack.client.loadbalance.model.LbaasServer; import io.fathom.cloud.protobuf.LbaasModel.LbaasMappingData; import io.fathom.cloud.protobuf.LbaasModel.LbaasServerData; import io.fathom.cloud.server.model.Project; import io.fathom.cloud.state.NumberedItemCollection; import java.net.Inet6Address; import java.net.InetAddress; import java.util.List; import java.util.Map; import java.util.Set; import javax.inject.Inject; import javax.inject.Singleton; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.net.InetAddresses; import com.google.inject.persist.Transactional; @Singleton @Transactional public class LoadBalanceServiceImpl implements LoadBalanceService { private static final Logger log = LoggerFactory.getLogger(LoadBalanceServiceImpl.class); @Inject LbaasBackends backends; @Inject LbaasRepository repository; @Inject DnsService dns; @Override public void setMappings(String systemKey, Project project, List<LbaasMapping> mappings) throws CloudException { List<LbaasMappingData> records = LbaasMappingMapper.INSTANCE.toData(project, systemKey, mappings); setLbaasRecords(systemKey, project, records); } @Override public void setServers(String systemKey, Project project, List<LbaasServer> servers) throws CloudException { List<LbaasServerData> records = LbaasServerMapper.INSTANCE.toData(project, systemKey, servers); setLbaasServers(systemKey, project, records); } private void setLbaasRecords(String systemKey, Project project, List<LbaasMappingData> requests) throws CloudException { // This is where a database would be awesome! LbaasMappingMapper mapper = LbaasMappingMapper.INSTANCE; NumberedItemCollection<LbaasMappingData> collection = repository.getMappings(project); Map<LbaasMappingData, LbaasMappingData> rawToDbMap = Maps.newHashMap(); Map<Long, LbaasMappingData> dbMap = Maps.newHashMap(); for (LbaasMappingData mapping : collection.list()) { if (!mapping.hasSystemKey()) { continue; } if (!systemKey.equals(mapping.getSystemKey())) { continue; } rawToDbMap.put(mapper.toComparable(mapping), mapping); dbMap.put(mapping.getId(), mapping); } List<LbaasMappingData> add = Lists.newArrayList(); Map<Long, LbaasMappingData> remove = Maps.newHashMap(dbMap); for (LbaasMappingData mapping : requests) { LbaasMappingData db = rawToDbMap.get(mapping); if (db == null) { add.add(mapping); } else { remove.remove(db.getId()); } } Set<String> dirtyHosts = Sets.newHashSet(); for (LbaasMappingData a : add) { LbaasMappingData.Builder b = LbaasMappingData.newBuilder(a); collection.create(b); dirtyHosts.add(b.getHost()); } for (LbaasMappingData r : remove.values()) { collection.delete(r.getId()); dirtyHosts.add(r.getHost()); } for (String host : dirtyHosts) { updateHost(project, host); } } private void setLbaasServers(String systemKey, Project project, List<LbaasServerData> requests) throws CloudException { // This is where a database would be awesome! LbaasServerMapper mapper = LbaasServerMapper.INSTANCE; NumberedItemCollection<LbaasServerData> collection = repository.getServers(project); Map<LbaasServerData, LbaasServerData> rawToDbMap = Maps.newHashMap(); Map<Long, LbaasServerData> dbMap = Maps.newHashMap(); for (LbaasServerData mapping : collection.list()) { if (!mapping.hasSystemKey()) { continue; } if (!systemKey.equals(mapping.getSystemKey())) { continue; } rawToDbMap.put(mapper.toComparable(mapping), mapping); dbMap.put(mapping.getId(), mapping); } List<LbaasServerData> add = Lists.newArrayList(); Map<Long, LbaasServerData> remove = Maps.newHashMap(dbMap); for (LbaasServerData mapping : requests) { LbaasServerData db = rawToDbMap.get(mapping); if (db == null) { add.add(mapping); } else { remove.remove(db.getId()); } } Set<LbaasServerData> dirtyServers = Sets.newHashSet(); for (LbaasServerData a : add) { log.info("Add LB: {}", a); LbaasServerData.Builder b = LbaasServerData.newBuilder(a); LbaasServerData created = collection.create(b); dirtyServers.add(created); } for (LbaasServerData r : remove.values()) { log.info("Remove LB: {}", r); LbaasServerData deleted = collection.delete(r.getId()); if (deleted == null) { throw new IllegalStateException("Did not find server during delete"); } dirtyServers.add(deleted); } updateServers(project, dirtyServers); } private void updateServers(Project project, Set<LbaasServerData> dirtyServers) throws CloudException { // We need to update all the DNS records to point to the correct servers Set<String> hosts = Sets.newHashSet(); for (LbaasMappingData mapping : repository.getMappings(project).list()) { // If we map hosts to specific servers, we'll do that here... hosts.add(mapping.getHost()); } List<DnsRecordSpec> ipv4 = Lists.newArrayList(); List<DnsRecordSpec> ipv6 = Lists.newArrayList(); for (LbaasServerData server : repository.getServers(project).list()) { String ip = server.getIp(); DnsRecordSpec record = new DnsRecordSpec(); record.address = ip; InetAddress address = InetAddresses.forString(ip); if (address instanceof Inet6Address) { ipv6.add(record); } else { ipv4.add(record); } } List<DnsRecordsetSpec> dnsRecordsets = Lists.newArrayList(); for (String host : hosts) { if (!ipv4.isEmpty()) { DnsRecordsetSpec dnsRecord = new DnsRecordsetSpec(); dnsRecord.fqdn = host; dnsRecord.type = "A"; dnsRecord.records = Lists.newArrayList(ipv4); dnsRecordsets.add(dnsRecord); } if (!ipv6.isEmpty()) { DnsRecordsetSpec dnsRecord = new DnsRecordsetSpec(); dnsRecord.fqdn = host; dnsRecord.type = "AAAA"; dnsRecord.records = Lists.newArrayList(ipv6); dnsRecordsets.add(dnsRecord); } } String systemKey = "__lb__/" + project.getId(); dns.setDnsRecordsets(systemKey, project, dnsRecordsets); } private void updateHost(Project project, String host) throws CloudException { LbaasBackend backend = getBackend(); backend.updateHost(project, host); } private LbaasBackend getBackend() throws CloudException { return backends.getBackend(); } public List<LbaasMapping> listMappings(Project project, String host) throws CloudException { LbaasMappingMapper mapper = LbaasMappingMapper.INSTANCE; List<LbaasMapping> ret = Lists.newArrayList(); for (LbaasMappingData mapping : repository.getMappings(project).list()) { if (!host.equals(mapping.getHost())) { continue; } ret.add(mapper.toModel(mapping)); } return ret; } }