/******************************************************************************* * Copyright (c) 2015, 2016 Red Hat. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Red Hat - Initial Contribution *******************************************************************************/ package org.eclipse.linuxtools.internal.vagrant.core; import java.io.BufferedReader; import java.io.Closeable; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.ListenerList; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.preferences.DefaultScope; import org.eclipse.core.runtime.preferences.InstanceScope; import org.eclipse.debug.core.DebugPlugin; import org.eclipse.debug.core.ILaunchConfigurationType; import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; import org.eclipse.debug.core.ILaunchManager; import org.eclipse.linuxtools.vagrant.core.EnumVMStatus; import org.eclipse.linuxtools.vagrant.core.IVagrantBox; import org.eclipse.linuxtools.vagrant.core.IVagrantBoxListener; import org.eclipse.linuxtools.vagrant.core.IVagrantConnection; import org.eclipse.linuxtools.vagrant.core.IVagrantVM; import org.eclipse.linuxtools.vagrant.core.IVagrantVMListener; import org.osgi.framework.Version; public class VagrantConnection implements IVagrantConnection, Closeable { private static final String JSCH_ID = "org.eclipse.jsch.core"; //$NON-NLS-1$ private static final String KEY = "PRIVATEKEY"; //$NON-NLS-1$ private static final String VG = "vagrant"; //$NON-NLS-1$ private final Object imageLock = new Object(); private final Object containerLock = new Object(); private List<IVagrantVM> vms; private boolean containersLoaded = false; private List<IVagrantBox> boxes; private boolean boxesLoaded = false; private Set<String> trackedKeys = new HashSet<>(); ListenerList<IVagrantVMListener> vmListeners; ListenerList<IVagrantBoxListener> boxListeners; public VagrantConnection() { // Add the box/vm refresh manager to watch the containers list VagrantBoxRefreshManager vbrm = VagrantBoxRefreshManager.getInstance(); VagrantVMRefreshManager vvrm = VagrantVMRefreshManager.getInstance(); addBoxListener(vbrm); addVMListener(vvrm); } @Override public void close() { } @Override public void addVMListener(IVagrantVMListener listener) { if (vmListeners == null) vmListeners = new ListenerList<>(ListenerList.IDENTITY); vmListeners.add(listener); } @Override public void removeVMListener(IVagrantVMListener listener) { if (vmListeners != null) vmListeners.remove(listener); } public void notifyContainerListeners(List<IVagrantVM> list) { if (vmListeners != null) { for (IVagrantVMListener listener : vmListeners) { listener.listChanged(this, list); } } } @Override public void addBoxListener(IVagrantBoxListener listener) { if (boxListeners == null) boxListeners = new ListenerList<>(ListenerList.IDENTITY); boxListeners.add(listener); } @Override public void removeBoxListener(IVagrantBoxListener listener) { if (boxListeners != null) boxListeners.remove(listener); } public void notifyBoxListeners(List<IVagrantBox> list) { if (boxListeners != null) { for (IVagrantBoxListener listener : boxListeners) { listener.listChanged(this, list); } } } @Override public List<IVagrantVM> getVMs(boolean force) { if (force || !isVMsLoaded()) { refreshVMs(); } return this.vms; } protected void refreshVMs() { String[] res = call(new String[] { "global-status" }); //$NON-NLS-1$ List<String> vmIDs = new LinkedList<>(); List<String> vmDirs = new LinkedList<>(); final List<IVagrantVM> containers = new LinkedList<>(); for (int i = 0; i < res.length; i++) { String[] items = res[i].split("\\s+"); //$NON-NLS-1$ if (items.length == 5 && i >= 2) { vmIDs.add(items[0]); vmDirs.add(items[items.length - 1]); } } List<String> completed = new ArrayList<>(); if (!vmIDs.isEmpty()) { Iterator<String> vmIterator = vmIDs.iterator(); Iterator<String> vmDirIterator = vmDirs.iterator(); while (vmIterator.hasNext()) { final String vmid = vmIterator.next(); final String vmDir = vmDirIterator.next(); new Thread("Checking ssh-config for vm " + vmid) { //$NON-NLS-1$ @Override public void run() { try { VagrantVM ret = createVagrantVM(vmid, vmDir); if (ret != null) { containers.add(ret); } } finally { // Ensure this gets called no matter what completed.add(vmid); } } }.start(); } } // Should set a max timeout? // Wait for completed to have same count as vmIDs while (completed.size() < vmIDs.size()) { try { Thread.sleep(100); } catch (InterruptedException ie) { // Ignore } } Collections.sort(containers, (o1, o2) -> o1.name().compareTo(o2.name())); this.containersLoaded = true; synchronized (containerLock) { this.vms = containers; } removeKeysFromInnactiveVMs(); notifyContainerListeners(this.vms); } private VagrantVM createVagrantVM(String vmid, String vmDir) { Map<String, String> env = EnvironmentsManager.getSingleton() .getEnvironment(new File(vmDir)); List<String> args = new LinkedList<>( Arrays.asList(new String[] { "ssh-config" })); //$NON-NLS-1$ args.add(vmid); List<String> sshConfig = null; // Run and handle ssh-config for this vm String[] res = call(args.toArray(new String[0]), new File(vmDir), env); for (int i = 0; i < res.length; i++) { String[] items = res[i].trim().split(" "); //$NON-NLS-1$ if (items[0].equals("HostName")) { //$NON-NLS-1$ List<String> tmp = new ArrayList<>(); tmp.add(items[1]); sshConfig = tmp; } else if (items[0].equals("User") || items[0].equals("Port")) { //$NON-NLS-1$ //$NON-NLS-2$ sshConfig.add(items[1]); } else if (items[0].equals("IdentityFile")) { sshConfig.add(items[1].replace("\"", "")); //$NON-NLS-1$ //$NON-NLS-2$ } } // Run and handle status for this vm VagrantVM vm = null; args = new LinkedList<>( Arrays.asList(new String[] { "--machine-readable", "status" })); //$NON-NLS-1$ //$NON-NLS-2$ args.add(vmid); res = call(args.toArray(new String[0]), new File(vmDir), env); String name, provider, state, state_desc; name = provider = state = state_desc = ""; //$NON-NLS-1$ for (int i = 0; i < res.length; i++) { String[] items = res[i].split(","); //$NON-NLS-1$ if (items[2].equals("provider-name")) { //$NON-NLS-1$ name = items[1]; provider = items[3]; } else if (items[2].equals("state")) { //$NON-NLS-1$ state = items[3]; } else if (items[2].equals("state-human-long")) { //$NON-NLS-1$ state_desc = items[3]; if (sshConfig == null || sshConfig.isEmpty()) { // VM exists but ssh is not configured vm = new VagrantVM(vmid, name, provider, state, state_desc, new File(vmDir), null, null, 0, null); } else { vm = new VagrantVM(vmid, name, provider, state, state_desc, new File(vmDir), sshConfig.get(0), sshConfig.get(1), Integer.parseInt(sshConfig.get(2)), sshConfig.get(3)); } } } return vm; } /** * If a key stored in preferences comes from a non-running VM * or came from a Vagrant VM (tracked) but is not longer * associated with one, then it is safe to remove it. */ private void removeKeysFromInnactiveVMs() { // org.eclipse.jsch.internal.core.IConstants.KEY_PRIVATEKEY String newKeys = ""; //$NON-NLS-1$ String keys = InstanceScope.INSTANCE.getNode(JSCH_ID).get(KEY, ""); //$NON-NLS-1$ if (keys.isEmpty()) { keys = DefaultScope.INSTANCE.getNode(JSCH_ID).get(KEY, ""); //$NON-NLS-1$ } boolean vmFound = false; for (String key : keys.split(",")) { //$NON-NLS-1$ for (IVagrantVM vm : vms) { if (key.equals(vm.identityFile())) { vmFound = true; if (!EnumVMStatus.RUNNING.equals(EnumVMStatus.fromStatusMessage(vm.state()))) { newKeys = removeFromKeys(keys, key); removeFromTrackedKeys(key); break; } } } if (!vmFound && isTrackedKey(key)) { newKeys = removeFromKeys(keys, key); removeFromTrackedKeys(key); } } if (!newKeys.isEmpty() && !newKeys.equals(keys)) { InstanceScope.INSTANCE.getNode(JSCH_ID).put(KEY, newKeys); } } @Override public void addToTrackedKeys(String key) { trackedKeys.add(key); } private void removeFromTrackedKeys(String key) { trackedKeys.remove(key); } private String removeFromKeys(String keys, String key) { StringBuffer res = new StringBuffer(); for (String k : keys.split(",")) { if (!key.equals(k)) { res.append(","); //$NON-NLS-1$ res.append(k); } } return res.substring(1); } public boolean isTrackedKey(String key) { return trackedKeys.contains(key); } @Override public List<IVagrantVM> getVMs() { return getVMs(false); } @Override public boolean isVMsLoaded() { return containersLoaded; } @Override public boolean isBoxesLoaded() { return boxesLoaded; } @Override public List<IVagrantBox> getBoxes() { return getBoxes(false); } @Override public List<IVagrantBox> getBoxes(boolean force) { if (force || !isBoxesLoaded()) { String [] res = call(new String[] { "--machine-readable", "box", "list" }); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ List<IVagrantBox> images = new LinkedList<>(); String name = ""; //$NON-NLS-1$ String provider = ""; //$NON-NLS-1$ String version = "0"; //$NON-NLS-1$ for (int i = 0; i < res.length; i++) { String[] items = res[i].split(","); //$NON-NLS-1$ if (items[2].equals("box-name")) { //$NON-NLS-1$ name = items[3]; } else if (items[2].equals("box-provider")) { //$NON-NLS-1$ provider = items[3]; } else if (items[2].equals("box-version")) { //$NON-NLS-1$ version = items[3]; images.add(new VagrantBox(name, provider, Version.parseVersion(version))); name = ""; //$NON-NLS-1$ provider = ""; //$NON-NLS-1$ version = "0"; //$NON-NLS-1$ } } this.boxesLoaded = true; synchronized (imageLock) { this.boxes = images; } notifyBoxListeners(this.boxes); } return this.boxes; } @Override public void init(File vagrantDir) { call(new String [] {"init"}, vagrantDir); //$NON-NLS-1$ } @Override public void up(File vagrantDir, String provider) { up(vagrantDir, provider, EnvironmentsManager.getSingleton().getEnvironment(vagrantDir)); } private void up(File vagrantDir, String provider, Map<String, String> environment) { if (provider != null) { rtCall(new String[] { "up", "--provider", provider }, //$NON-NLS-1$ //$NON-NLS-2$ vagrantDir, environment); } else { rtCall(new String[] { "up" }, vagrantDir, environment); //$NON-NLS-1$ } } @Override public void addBox(String name, String location, boolean progress) { if (progress) { rtCall(new String[] { "box", "add", name, location }, null, null); //$NON-NLS-1$ //$NON-NLS-2$ } else { call(new String [] {"--machine-readable", "box", "add", name, location}); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } } @Override public void destroyVM(IVagrantVM vm) { call(new String[] { "destroy", "-f", vm.id() }, vm.directory(), //$NON-NLS-1$ //$NON-NLS-2$ EnvironmentsManager.getSingleton() .getEnvironment(vm.directory())); } @Override public void haltVM(IVagrantVM vm) { call(new String[] { "--machine-readable", "halt", vm.id() }, //$NON-NLS-1$ //$NON-NLS-2$ vm.directory(), EnvironmentsManager.getSingleton() .getEnvironment(vm.directory())); } @Override public void startVM(IVagrantVM vm) { up(vm.directory(), vm.provider(), EnvironmentsManager.getSingleton() .getEnvironment(vm.directory())); } @Override public void removeBox(String name) { call(new String[] { "--machine-readable", "box", "remove", name }); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } @Override public void packageVM(IVagrantVM vm, String name) { rtCall(new String[] { "package", vm.id(), "--output", name}, vm.directory(), null); //$NON-NLS-1$ //$NON-NLS-2$ } @Override public String getName() { return Messages.VagrantConnection_sys_vagrant_conn; } private static String[] call(String[] args) { return call(args, null); } private static String[] call(String[] args, File vagrantDir) { return call(args, vagrantDir, null); } private static String[] call(String[] args, File vagrantDir, Map<String, String> env) { final String[] envp = EnvironmentsManager.convertEnvironment(env); List<String> result = new ArrayList<>(); try { List<String> cmd = new ArrayList<>(); cmd.add(VG); cmd.addAll(Arrays.asList(args)); Process p = Runtime.getRuntime().exec(cmd.toArray(new String[0]), envp, vagrantDir); BufferedReader buff = new BufferedReader( new InputStreamReader(p.getInputStream())); if (p.waitFor() == 0) { String line; while ((line = buff.readLine()) != null) { result.add(line); } } else { return new String[0]; } } catch (IOException e) { } catch (InterruptedException e) { } return result.toArray(new String[0]); } private static void rtCall(String[] args, File vagrantDir, Map<String, String> environment) { // org.eclipse.core.externaltools.internal.IExternalToolConstants final String EXTERNAL_TOOLS = "org.eclipse.ui.externaltools.ProgramLaunchConfigurationType"; //$NON-NLS-1$ final String UI_PLUGIN_ID = "org.eclipse.ui.externaltools"; //$NON-NLS-1$ final String ATTR_LOCATION = UI_PLUGIN_ID + ".ATTR_LOCATION"; //$NON-NLS-1$ final String ATTR_TOOL_ARGUMENTS = UI_PLUGIN_ID + ".ATTR_TOOL_ARGUMENTS"; //$NON-NLS-1$ final String ATTR_WORKING_DIRECTORY = UI_PLUGIN_ID + ".ATTR_WORKING_DIRECTORY"; //$NON-NLS-1$ String arguments = Arrays.asList(args).stream().map(u -> u.toString()) .collect(Collectors.joining(" ")); //$NON-NLS-1$ ILaunchManager manager = DebugPlugin.getDefault().getLaunchManager(); ILaunchConfigurationType type = manager.getLaunchConfigurationType(EXTERNAL_TOOLS); try { // TODO: worth handling 'vagrant' (not on PATH) as an alias ? String vagrantPath = findVagrantPath(); ILaunchConfigurationWorkingCopy wc = type.newInstance(null, VG); wc.setAttribute(ATTR_LOCATION, vagrantPath); wc.setAttribute(ATTR_TOOL_ARGUMENTS, arguments); wc.setAttribute(ATTR_WORKING_DIRECTORY, vagrantDir != null ? vagrantDir.getAbsolutePath() : null); wc.setAttribute(ILaunchManager.ATTR_ENVIRONMENT_VARIABLES, environment); wc.launch(ILaunchManager.RUN_MODE, new NullProgressMonitor()); } catch (CoreException e1) { Activator.log(e1); } } /** * Find the location of 'vagrant' on the system by looking under the * environment PATH. * * @return The location of 'vagrant' as a string if it exists under * the PATH, or null if it could not be found. */ public static String findVagrantPath() { final String vgName = Platform.OS_WIN32.equals(Platform.getOS()) ? VG + ".exe" : VG; //$NON-NLS-1$ String userDefinedVagrantPath = getUserDefinedVagrantPath(); if (userDefinedVagrantPath != null) { Path vgPath = Paths.get(userDefinedVagrantPath, vgName); if (vgPath.toFile().exists()) { return vgPath.toString(); } } final String envPath = System.getenv("PATH"); //$NON-NLS-1$ if (envPath != null) { for (String dir : envPath.split(File.pathSeparator)) { Path vgPath = Paths.get(dir, vgName); if (vgPath.toFile().exists()) { return vgPath.toString(); } } } return null; } public static String getUserDefinedVagrantPath() { String userDefinedVagrantPath = InstanceScope.INSTANCE .getNode("org.eclipse.linuxtools.vagrant.ui") //$NON-NLS-1$ .get("vagrantPath", null); //$NON-NLS-1$ String defaultDefinedVagrantPath = DefaultScope.INSTANCE .getNode("org.eclipse.linuxtools.vagrant.ui") //$NON-NLS-1$ .get("vagrantPath", null); //$NON-NLS-1$ return userDefinedVagrantPath != null ? userDefinedVagrantPath : defaultDefinedVagrantPath; } }