package de.cinovo.cloudconductor.server.rest.impl; /* * #%L * cloudconductor-server * %% * Copyright (C) 2013 - 2014 Cinovo AG * %% * Licensed 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. * #L% */ import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.TreeSet; import org.joda.time.DateTime; import org.joda.time.Minutes; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import com.google.common.collect.ArrayListMultimap; import de.cinovo.cloudconductor.api.ServiceState; import de.cinovo.cloudconductor.api.interfaces.IAgent; import de.cinovo.cloudconductor.api.model.AgentOptions; import de.cinovo.cloudconductor.api.model.ConfigFile; import de.cinovo.cloudconductor.api.model.Dependency; import de.cinovo.cloudconductor.api.model.PackageState; import de.cinovo.cloudconductor.api.model.PackageStateChanges; import de.cinovo.cloudconductor.api.model.PackageVersion; import de.cinovo.cloudconductor.api.model.ServiceStates; import de.cinovo.cloudconductor.api.model.ServiceStatesChanges; import de.cinovo.cloudconductor.api.model.TaskState; import de.cinovo.cloudconductor.server.comparators.PackageVersionComparator; import de.cinovo.cloudconductor.server.comparators.VersionStringComparator; import de.cinovo.cloudconductor.server.dao.IAgentDAO; import de.cinovo.cloudconductor.server.dao.IAgentOptionsDAO; import de.cinovo.cloudconductor.server.dao.IHostDAO; import de.cinovo.cloudconductor.server.dao.IPackageDAO; import de.cinovo.cloudconductor.server.dao.IPackageStateDAO; import de.cinovo.cloudconductor.server.dao.IPackageVersionDAO; import de.cinovo.cloudconductor.server.dao.IServerOptionsDAO; import de.cinovo.cloudconductor.server.dao.IServiceDAO; import de.cinovo.cloudconductor.server.dao.IServiceDefaultStateDAO; import de.cinovo.cloudconductor.server.dao.IServiceStateDAO; import de.cinovo.cloudconductor.server.dao.ITemplateDAO; import de.cinovo.cloudconductor.server.model.EAgent; import de.cinovo.cloudconductor.server.model.EAgentAuthToken; import de.cinovo.cloudconductor.server.model.EAgentOption; import de.cinovo.cloudconductor.server.model.EDependency; import de.cinovo.cloudconductor.server.model.EFile; import de.cinovo.cloudconductor.server.model.EHost; import de.cinovo.cloudconductor.server.model.EPackage; import de.cinovo.cloudconductor.server.model.EPackageState; import de.cinovo.cloudconductor.server.model.EPackageVersion; import de.cinovo.cloudconductor.server.model.EServerOptions; import de.cinovo.cloudconductor.server.model.EService; import de.cinovo.cloudconductor.server.model.EServiceDefaultState; import de.cinovo.cloudconductor.server.model.EServiceState; import de.cinovo.cloudconductor.server.model.ETemplate; import de.cinovo.cloudconductor.server.model.enums.PackageCommand; import de.cinovo.cloudconductor.server.rest.helper.MAConverter; import de.taimos.restutils.RESTAssert; import de.taimos.springcxfdaemon.JaxRsComponent; /** * Copyright 2013 Cinovo AG<br> * <br> * * @author psigloch * */ @JaxRsComponent public class AgentImpl implements IAgent { private static final int MAX_TIMEOUT_HOST = 30; private static final int MAX_UPDATE_THRESHOLD = 15; @Autowired private IHostDAO dhost; @Autowired private ITemplateDAO dtemplate; @Autowired private IServiceDAO dsvc; @Autowired private IServiceStateDAO dsvcstate; @Autowired private IPackageVersionDAO drpm; @Autowired private IPackageDAO dpkg; @Autowired private IPackageStateDAO dpkgstate; @Autowired private IServiceDefaultStateDAO ddefss; @Autowired private IAgentOptionsDAO dagentoptions; @Autowired private IServerOptionsDAO dserveroptions; @Autowired private IAgentDAO dAgent; @Override @Transactional public Set<String> getAliveAgents() { List<EHost> hosts = this.dhost.findList(); DateTime now = new DateTime(); Set<String> result = new HashSet<>(); for (EHost host : hosts) { DateTime dt = new DateTime(host.getLastSeen()); int diff = Minutes.minutesBetween(dt, now).getMinutes(); if (diff < AgentImpl.MAX_TIMEOUT_HOST) { result.add(host.getName()); } } return result; } @Override @Transactional public PackageStateChanges notifyPackageState(String tname, String hname, PackageState rpmState) { RESTAssert.assertNotEmpty(hname); RESTAssert.assertNotEmpty(tname); EHost host = this.dhost.findByName(hname); ETemplate template = this.dtemplate.findByName(tname); RESTAssert.assertNotNull(template); if (host == null) { host = this.createNewHost(hname, template); } DateTime now = new DateTime(); host.setLastSeen(now.getMillis()); List<EPackage> packages = this.dpkg.findList(); HashSet<EPackageState> leftPackages = new HashSet<>(host.getPackages()); for (PackageVersion irpm : rpmState.getInstalledRpms()) { EPackage pkg = null; for (EPackage p : packages) { if (p.getName().equals(irpm.getName())) { pkg = p; break; } } if (pkg == null) { continue; } EPackageState state = this.updateExistingState(host, irpm, leftPackages); if (state == null) { state = this.createMissingState(host, irpm, pkg); host.getPackages().add(state); } } for (EPackageState pkg : leftPackages) { if (host.getPackages().contains(pkg)) { host.getPackages().remove(pkg); this.dpkgstate.delete(pkg); } } host = this.dhost.save(host); // check whether the host may update or has to wait for another host to finish updateing if (this.sendPackageChanges(template, host)) { // Compute instruction lists (install/update/erase) from difference between packages actually installed packages that should be // installed. Set<EPackageVersion> actual = new HashSet<>(); for (EPackageState state : host.getPackages()) { actual.add(state.getVersion()); } ArrayListMultimap<PackageCommand, PackageVersion> diff = this.computePackageDiff(template.getPackageVersions(), actual); if (!diff.get(PackageCommand.INSTALL).isEmpty() || !diff.get(PackageCommand.UPDATE).isEmpty() || !diff.get(PackageCommand.ERASE).isEmpty()) { host.setStartedUpdate(DateTime.now().getMillis()); } return new PackageStateChanges(diff.get(PackageCommand.INSTALL), diff.get(PackageCommand.UPDATE), diff.get(PackageCommand.ERASE)); } return new PackageStateChanges(new ArrayList<PackageVersion>(), new ArrayList<PackageVersion>(), new ArrayList<PackageVersion>()); } private EHost createNewHost(String hname, ETemplate template) { EHost host; host = new EHost(); host.setName(hname); host.setTemplate(template); host = this.dhost.save(host); return host; } /** * @param template * @param host * @param now */ private boolean sendPackageChanges(ETemplate template, EHost host) { DateTime now = DateTime.now(); int maxHostsOnUpdate = template.getHosts().size() / 2; int hostsOnUpdate = 0; if ((template.getSmoothUpdate() == null) || !template.getSmoothUpdate() || (maxHostsOnUpdate < 1)) { return true; } if (host.getStartedUpdate() != null) { return true; } for (EHost h : template.getHosts()) { if (h.getStartedUpdate() != null) { int timeElapsed = Minutes.minutesBetween(new DateTime(h.getStartedUpdate()), now).getMinutes(); if (timeElapsed > AgentImpl.MAX_UPDATE_THRESHOLD) { continue; } hostsOnUpdate++; } } if (maxHostsOnUpdate > hostsOnUpdate) { return true; } return false; } /** * @param host * @param irpm * @param pkg * @return */ private EPackageState createMissingState(EHost host, PackageVersion irpm, EPackage pkg) { EPackageState state; EPackageVersion rpm = this.drpm.find(irpm.getName(), irpm.getVersion()); if (rpm == null) { rpm = new EPackageVersion(); rpm.setPkg(pkg); rpm.setVersion(irpm.getVersion()); rpm.setDeprecated(true); rpm = this.drpm.save(rpm); } state = new EPackageState(); state.setHost(host); state.setVersion(rpm); state = this.dpkgstate.save(state); return state; } /** * @param host * @param irpm * @param leftPackages * @return */ private EPackageState updateExistingState(EHost host, PackageVersion irpm, HashSet<EPackageState> leftPackages) { VersionStringComparator vsc = new VersionStringComparator(); for (EPackageState state : host.getPackages()) { if (state.getVersion().getPkg().getName().equals(irpm.getName())) { int comp = vsc.compare(state.getVersion().getVersion(), irpm.getVersion()); if (comp == 0) { break; } EPackageVersion rpm = this.drpm.find(irpm.getName(), irpm.getVersion()); if (rpm == null) { rpm = new EPackageVersion(); rpm.setPkg(state.getVersion().getPkg()); rpm.setVersion(irpm.getVersion()); rpm.setDeprecated(true); rpm = this.drpm.save(rpm); } leftPackages.remove(state); state.setVersion(rpm); return this.dpkgstate.save(state); } } return null; } private boolean asserHostServices(ETemplate template, EHost host) { List<EService> services = this.dsvc.findList(); Set<EService> templateServices = new HashSet<>(); for (EService s : services) { for (EPackageVersion p : template.getPackageVersions()) { if (s.getPackages().contains(p.getPkg())) { templateServices.add(s); } } } Set<EService> missingServices = new HashSet<>(templateServices); Set<EServiceState> nonUsedServiceStates = new HashSet<>(host.getServices()); for (EServiceState state : host.getServices()) { for (EService service : templateServices) { if (service.getName().equals(state.getService().getName())) { missingServices.remove(service); for (EServiceState ss : nonUsedServiceStates) { if (ss.getService().getId().equals(service.getId())) { nonUsedServiceStates.remove(ss); break; } } break; } } } boolean changes = false; // add new service states for (EService service : missingServices) { EServiceState state = new EServiceState(); state.setService(service); state.setHost(host); EServiceDefaultState dss = this.ddefss.findByName(service.getName(), template.getName()); if ((dss != null)) { state.setState(dss.getState()); } this.dsvcstate.save(state); changes = true; } // clean up old no more used service states for (EServiceState ss : nonUsedServiceStates) { this.dsvcstate.delete(ss); } return changes; } @Override @Transactional public ServiceStatesChanges notifyServiceState(String tname, String hname, ServiceStates serviceState) { RESTAssert.assertNotEmpty(hname); RESTAssert.assertNotEmpty(tname); EHost host = this.dhost.findByName(hname); ETemplate template = this.dtemplate.findByName(tname); RESTAssert.assertNotNull(template); if (host == null) { host = this.createNewHost(hname, template); } if (this.asserHostServices(template, host)) { host = this.dhost.findByName(hname); } Set<String> toStop = new HashSet<>(); Set<String> toStart = new HashSet<>(); Set<String> toRestart = new HashSet<>(); Set<EServiceState> stateList = new HashSet<>(host.getServices()); // agent sends running services for (String sname : serviceState.getRunningServices()) { for (EServiceState state : host.getServices()) { if (state.getService().getName().equals(sname)) { stateList.remove(state); switch (state.getState()) { case RESTARTING_STARTING: case STARTING: state.nextState(); this.dsvcstate.save(state); break; case STOPPING: toStop.add(state.getService().getInitScript()); break; case RESTARTING_STOPPING: toRestart.add(state.getService().getInitScript()); state.nextState(); this.dsvcstate.save(state); break; case STOPPED: state.nextState(); toStop.add(state.getService().getInitScript()); this.dsvcstate.save(state); break; default: break; } } } } // agent sends stopped services for (EServiceState state : stateList) { switch (state.getState()) { case STARTING: toStart.add(state.getService().getInitScript()); break; case STOPPING: state.nextState(); this.dsvcstate.save(state); break; case STARTED: toStart.add(state.getService().getInitScript()); state.setState(ServiceState.STARTING); this.dsvcstate.save(state); break; default: break; } } HashSet<ConfigFile> configFiles = new HashSet<>(); for (EFile file : template.getConfigFiles()) { configFiles.add(MAConverter.fromModel(file)); } if (toStart.isEmpty() && toStop.isEmpty() && toRestart.isEmpty() && (host.getStartedUpdate() != null)) { host.setStartedUpdate(null); this.dhost.save(host); } ServiceStatesChanges serviceStatesChanges = new ServiceStatesChanges(toStart, toStop, toRestart); serviceStatesChanges.setConfigFiles(configFiles); return serviceStatesChanges; } @Override @Transactional public AgentOptions heartBeat(String tname, String hname, String agentN) { RESTAssert.assertNotEmpty(hname); RESTAssert.assertNotEmpty(tname); RESTAssert.assertNotEmpty(agentN); EAgent agent = this.dAgent.findAgentByName(agentN); if (agent == null) { agent = this.createNewAgent(agentN, null); } EHost host = this.dhost.findByName(hname); if (host == null) { host = this.createNewHost(hname, this.dtemplate.findByName(tname)); } DateTime now = new DateTime(); host.setLastSeen(now.getMillis()); host.setAgent(agent); host = this.dhost.save(host); EAgentOption options = this.dagentoptions.findByTemplate(host.getTemplate()); if (options == null) { options = new EAgentOption(); options.setTemplate(host.getTemplate()); options = this.dagentoptions.save(options); } AgentOptions result = MAConverter.fromModel(options); result.setTemplateName(host.getTemplate().getName()); boolean onceExecuted = false; if (options.getDoSshKeys() == TaskState.ONCE) { if (host.getExecutedSSH()) { result.setDoSshKeys(TaskState.OFF); } else { onceExecuted = true; host.setExecutedSSH(true); } } if (options.getDoPackageManagement() == TaskState.ONCE) { if (host.getExecutedPkg()) { result.setDoPackageManagement(TaskState.OFF); } else { onceExecuted = true; host.setExecutedPkg(true); } } if (options.getDoFileManagement() == TaskState.ONCE) { if (host.getExecutedFiles()) { result.setDoFileManagement(TaskState.OFF); } else { onceExecuted = true; host.setExecutedFiles(true); } } if (onceExecuted) { this.dhost.save(host); } return result; } private EAgent createNewAgent(String agentName, EAgentAuthToken authToken) { EAgent agent = new EAgent(); agent.setName(agentName); if (authToken != null) { agent.setToken(authToken); agent.setTokenAssociationDate(DateTime.now().getMillis()); } return this.dAgent.save(agent); } private ArrayListMultimap<PackageCommand, PackageVersion> computePackageDiff(Collection<EPackageVersion> nominal, Collection<EPackageVersion> actual) { // Determine which package versions need to be erased and which are to be installed. TreeSet<EPackageVersion> toInstall = new TreeSet<EPackageVersion>(new PackageVersionComparator()); toInstall.addAll(nominal); toInstall.removeAll(actual); TreeSet<EPackageVersion> toErase = new TreeSet<EPackageVersion>(new PackageVersionComparator()); toErase.addAll(actual); toErase.removeAll(nominal); // Resolve the removal of an older version and the installation of a newer one to an update instruction. TreeSet<EPackageVersion> toUpdate = new TreeSet<EPackageVersion>(new PackageVersionComparator()); for (EPackageVersion i : toInstall) { EPackageVersion e = toErase.lower(i); if ((e != null) && e.getPkg().getName().equals(i.getPkg().getName())) { toErase.remove(e); toUpdate.add(i); } } toInstall.removeAll(toUpdate); // get rid of reserved packages on erase Set<EPackageVersion> keep = new HashSet<>(); EServerOptions eServerOptions = this.dserveroptions.get(); for (String pkg : eServerOptions.getDisallowUninstall()) { for (EPackageVersion erase : toErase) { if (erase.getPkg().getName().equals(pkg)) { keep.add(erase); break; } } } toErase.removeAll(keep); // remove dependencies from erase list Set<EPackageVersion> dependencies = new HashSet<>(); for (EPackageVersion i : nominal){ for (EDependency d : i.getDependencies()) { for(EPackageVersion e : toErase) { if(e.getPkg().getName().equals(d.getName())){ dependencies.add(e); } } } } toErase.removeAll(dependencies); // Convert the lists of package versions to lists of RPM descriptions (RPM name, release, and version). ArrayListMultimap<PackageCommand, PackageVersion> result = ArrayListMultimap.create(); result = this.fillPackageDiff(result, PackageCommand.INSTALL, toInstall); result = this.fillPackageDiff(result, PackageCommand.UPDATE, toUpdate); result = this.fillPackageDiff(result, PackageCommand.ERASE, toErase); return result; } private ArrayListMultimap<PackageCommand, PackageVersion> fillPackageDiff(ArrayListMultimap<PackageCommand, PackageVersion> map, PackageCommand command, Collection<EPackageVersion> packageVersions) { for (EPackageVersion pv : packageVersions) { String rpmName = pv.getPkg().getName(); Set<Dependency> dep = new HashSet<>(); for (EDependency edep : pv.getDependencies()) { dep.add(MAConverter.fromModel(edep)); } map.put(command, new PackageVersion(rpmName, pv.getVersion(), dep)); } return map; } @Override @Transactional public boolean isServerAlive() { return true; } }