com.parallels.desktopcloud.ParallelsDesktopConnectorSlaveComputer.java Source code

Java tutorial

Introduction

Here is the source code for com.parallels.desktopcloud.ParallelsDesktopConnectorSlaveComputer.java

Source

/*
 * The MIT License
 *
 * (c) 2004-2015. Parallels IP Holdings GmbH. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.parallels.desktopcloud;

import hudson.model.Node;
import hudson.model.Computer;
import hudson.remoting.Channel;
import hudson.security.Permission;
import hudson.slaves.AbstractCloudComputer;
import hudson.slaves.OfflineCause;
import jenkins.model.Jenkins;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.lang.management.ManagementFactory;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.management.JMException;
import jenkins.security.MasterToSlaveCallable;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import net.sf.json.JSONSerializer;
import org.apache.commons.validator.routines.InetAddressValidator;

public class ParallelsDesktopConnectorSlaveComputer extends AbstractCloudComputer<ParallelsDesktopConnectorSlave> {
    private static final ParallelsLogger LOGGER = ParallelsLogger.getLogger("PDConnectorSlaveComputer");
    private int numSlavesToStop = 0;
    private VMResources hostResources;

    public ParallelsDesktopConnectorSlaveComputer(ParallelsDesktopConnectorSlave slave) {
        super(slave);
    }

    private String getVmIPAddress(String vmId) throws Exception {
        int TIMEOUT = 180;
        InetAddressValidator validator = InetAddressValidator.getInstance();
        for (int i = 0; i < TIMEOUT; ++i) {
            RunVmCallable command = new RunVmCallable("list", "-f", "--json", vmId);
            String callResult = forceGetChannel().call(command);
            LOGGER.log(Level.SEVERE, " - (%d/%d) calling for IP. Result: %s", i, TIMEOUT, callResult);
            JSONArray vms = (JSONArray) JSONSerializer.toJSON(callResult);
            JSONObject vmInfo = vms.getJSONObject(0);
            String ip = vmInfo.getString("ip_configured");
            if (validator.isValidInet4Address(ip))
                return ip;
            Thread.sleep(1000);
        }
        throw new Exception("Failed to get IP for VM '" + vmId + "'");
    }

    private JSONObject getVMInfo(String vmId) throws Exception {
        RunVmCallable command = new RunVmCallable("list", "-i", "--json");
        String callResult = forceGetChannel().call(command);
        JSONArray vms = (JSONArray) JSONSerializer.toJSON(callResult);
        for (int i = 0; i < vms.size(); i++) {
            JSONObject vmInfo = vms.getJSONObject(i);
            if (vmId.equals(vmInfo.getString("ID")) || vmId.equals(vmInfo.getString("Name")))
                return vmInfo;
        }
        return null;
    }

    private long memSizeStringToLong(String memSize) {
        // XXX It is expected that memSize ends with "Mb"
        return Long.parseLong(memSize.substring(0, memSize.length() - 2)) * (1 << 20);
    }

    private static class VMResources implements Serializable {
        private static final long serialVersionUID = 1L;
        public int cpus;
        public long ram; // in bytes
        private static final long mb = 1 << 20;

        public VMResources(int cpus, long ram) {
            this.cpus = cpus;
            this.ram = ram;
        }

        public void append(VMResources newResources) {
            cpus += newResources.cpus;
            ram += newResources.ram;
        }

        public static boolean check(VMResources host, VMResources used, VMResources vm) {
            if ((vm.cpus + used.cpus) > host.cpus) {
                LOGGER.log(Level.SEVERE, "Exceeding CPU limit: vm=%d used=%d host=%d", vm.cpus, used.cpus,
                        host.cpus);
                return false;
            }
            if ((vm.ram + used.ram) > host.ram) {
                LOGGER.log(Level.SEVERE, "Exceeding RAM limit (Mb): vm=%d used=%d host=%d", vm.ram / mb,
                        used.ram / mb, host.ram / mb);
                return false;
            }
            return true;
        }

        public String toLogString() {
            return String.format("CPU=%d RAM=%d", cpus, ram);
        }
    }

    private VMResources parseVMResources(JSONObject vmInfo) {
        JSONObject vmHw = vmInfo.getJSONObject("Hardware");
        JSONObject vmCpuInfo = vmHw.getJSONObject("cpu");
        int cpus = vmCpuInfo.getInt("cpus");
        JSONObject vmMemInfo = vmHw.getJSONObject("memory");
        long ram = memSizeStringToLong(vmMemInfo.getString("size"));
        JSONObject vmVideoInfo = vmHw.getJSONObject("video");
        ram += memSizeStringToLong(vmVideoInfo.getString("size"));
        ram += 500 * (1 << 20); // +500Mb for each VM for virtualization overhead
        return new VMResources(cpus, ram);
    }

    private static VMResources getHostResources(Channel ch) throws Exception {
        return ch.call(new MasterToSlaveCallable<VMResources, Exception>() {
            private long getHostPhysicalMemory() {
                try {
                    MBeanServer server = ManagementFactory.getPlatformMBeanServer();
                    Object attribute = server.getAttribute(new ObjectName("java.lang", "type", "OperatingSystem"),
                            "TotalPhysicalMemorySize");
                    return (Long) attribute;
                } catch (JMException e) {
                    LOGGER.log(Level.SEVERE, "Failed to get host RAM size: %s", e);
                    return Long.MAX_VALUE;
                }
            }

            @Override
            public VMResources call() throws Exception {
                int cpus = Runtime.getRuntime().availableProcessors();
                long ram = getHostPhysicalMemory();
                return new VMResources(cpus, ram);
            }
        });
    }

    private boolean checkResourceLimitsForVm(String vmId) {
        try {
            if (hostResources == null) {
                hostResources = getHostResources(forceGetChannel());
                LOGGER.log(Level.SEVERE, "Host '%s' resources: %s", getName(), hostResources.toLogString());
            }

            VMResources vmResources = null;
            VMResources usedResources = new VMResources(0, 1 << 30); // +1Gb for host OS and apps
            RunVmCallable command = new RunVmCallable("list", "-i", "-a", "--json");
            String callResult = forceGetChannel().call(command);
            JSONArray vms = (JSONArray) JSONSerializer.toJSON(callResult);
            for (int i = 0; i < vms.size(); i++) {
                JSONObject vmInfo = vms.getJSONObject(i);
                String vmStatus = vmInfo.getString("State");
                if (vmStatus.equals("stopped") || vmStatus.equals("suspended")) {
                    if (vmInfo.getString("Name").equals(vmId) || vmInfo.getString("ID").equals(vmId))
                        vmResources = parseVMResources(vmInfo);
                } else if (!vmStatus.equals("invalid")) {
                    LOGGER.log(Level.FINE, "Accounting VM '%s'", vmInfo.getString("Name"));
                    usedResources.append(parseVMResources(vmInfo));
                }
            }
            if (vmResources == null)
                // This means that at this point VM of interest is already in running
                // state, somebody has started it. So there's no meaning to check something.
                return true;
            if (!VMResources.check(hostResources, usedResources, vmResources))
                return false;
            return true;
        } catch (Exception ex) {
            LOGGER.log(Level.SEVERE, "Error: %s\nFailed to check resource limits", ex);
        }
        return false;
    }

    public Node createSlaveOnVM(ParallelsDesktopVM vm) throws Exception {
        String vmId = vm.getVmid();
        LOGGER.log(Level.SEVERE, "Waiting for IP...");
        String ip;
        try {
            ip = getVmIPAddress(vmId);
            LOGGER.log(Level.SEVERE, "Got IP address for VM %s: %s", vmId, ip);
            vm.setLauncherIP(ip);
        } catch (Exception e) {
            if (vm.getLauncherIP() == null)
                throw e;
        }

        String slaveName = vm.getSlaveName();
        LOGGER.log(Level.FINE, "Starting slave '%s'", slaveName);
        Node n = new ParallelsDesktopVMSlave(vm, this);
        LOGGER.log(Level.SEVERE, "Slave %s provisioned.", slaveName);
        return n;
    }

    public boolean startVM(ParallelsDesktopVM vm) {
        String vmId = vm.getVmid();
        LOGGER.log(Level.SEVERE, "Looking for virtual machine '%s'...", vmId);
        try {
            JSONObject vmInfo = getVMInfo(vmId);
            if (vmInfo == null) {
                LOGGER.log(Level.SEVERE, "Failed to start virtual machine '%s': no such VM", vmId);
                return false;
            }

            String vmStatus = vmInfo.getString("State");
            ParallelsDesktopVM.VMStates state = ParallelsDesktopVM.parseVMState(vmStatus);
            if (state == null) {
                LOGGER.log(Level.SEVERE, "Unexpected VM '%s' state: %s", vmId, vmStatus);
                state = ParallelsDesktopVM.VMStates.Suspended;
            }
            if (vm.getPostBuildBehaviorValue() == ParallelsDesktopVM.PostBuildBehaviors.ReturnPrevState)
                vm.setPrevVMState(state);

            if (state != ParallelsDesktopVM.VMStates.Running) {
                if (!checkResourceLimitsForVm(vmId)) {
                    LOGGER.log(Level.SEVERE, "Not enough resources to start VM %s", vmId);
                    return false;
                }
                LOGGER.log(Level.SEVERE, "Starting virtual machine '%s'", vmId);
                RunVmCallable command = new RunVmCallable("start", vmId);
                forceGetChannel().call(command);
            }
            if (vm.getPostBuildCommand() != null)
                ++numSlavesToStop;
            vm.setProvisioned(true);
            return true;
        } catch (Exception ex) {
            LOGGER.log(Level.SEVERE, "Error: %s\nFailed to start VM '%s'", ex, vmId);
        }
        stopVM(vm);
        return false;
    }

    private void stopVM(ParallelsDesktopVM vm) {
        try {
            String action = vm.getPostBuildCommand();
            if (action == null) {
                LOGGER.log(Level.SEVERE, "Keep running VM %s", vm.getVmid());
                return;
            }
            LOGGER.log(Level.SEVERE, "Post build action for '%s': %s", vm.getVmid(), action);
            RunVmCallable command = new RunVmCallable(action, vm.getVmid());
            String res = forceGetChannel().call(command);
            LOGGER.log(Level.SEVERE, "Result: %s", res);
            if (numSlavesToStop > 0)
                --numSlavesToStop;
            vm.setProvisioned(false);
        } catch (Exception ex) {
            LOGGER.log(Level.SEVERE, "Error: %s", ex);
        }
    }

    public void postBuildAction(ParallelsDesktopVM vm) {
        stopVM(vm);
    }

    public boolean isReadyToRestart() {
        if (isOffline())
            return true;
        return numSlavesToStop == 0;
    }

    private static final class PrlCtlFailedException extends Exception {
        private static String formatMessage(int rc, String output) {
            String msg = String.format("prlctl execution failed with code %d", rc);
            if (!output.isEmpty())
                msg += String.format(" , output:\n%s", output);
            return msg;
        }

        private PrlCtlFailedException(int rc, String output) {
            super(formatMessage(rc, output));
        }
    }

    public Channel forceGetChannel() throws InterruptedException, ExecutionException {
        Channel channel = getChannel();
        if (channel == null) {
            connect(true).get();
            channel = getChannel();
        }
        return channel;
    }

    private static final class RunVmCallable extends MasterToSlaveCallable<String, Exception> {
        private static final String cmd = "/usr/local/bin/prlctl";
        private final String[] params;

        public RunVmCallable(String... params) {
            this.params = params;
        }

        @Override
        public String call() throws IOException, PrlCtlFailedException {
            List<String> cmds = new ArrayList<String>();
            cmds.add(cmd);
            cmds.addAll(Arrays.asList(this.params));

            LOGGER.log(Level.SEVERE, "Running command:");
            for (String s : cmds)
                LOGGER.log(Level.SEVERE, " [%s]", s);
            ProcessBuilder pb = new ProcessBuilder(cmds);
            pb.redirectErrorStream(true);
            Process pr = pb.start();
            BufferedReader in = new BufferedReader(new InputStreamReader(pr.getInputStream()));
            String line;
            String result = "";
            while ((line = in.readLine()) != null) {
                result += line;
            }
            int rc = 0;
            try {
                rc = pr.waitFor();
            } catch (InterruptedException ex) {
                LOGGER.log(Level.SEVERE, "Error: %s", ex.toString());
            }
            if (rc != 0)
                throw new PrlCtlFailedException(rc, result);
            return result;
        }
    }

    @Override
    public boolean hasPermission(Permission permission) {
        if (permission == CONFIGURE)
            return false;
        return super.hasPermission(permission);
    }

    @Override
    public void setTemporarilyOffline(boolean temporarilyOffline, OfflineCause cause) {
        try {
            Jenkins jenkins = Jenkins.getInstance();
            for (ParallelsDesktopVM vm : this.getNode().getOwner().getVms()) {
                Node slaveNode = jenkins.getNode(vm.getSlaveName());
                if (slaveNode == null) {
                    continue;
                }
                Computer slaveComputer = slaveNode.toComputer();
                slaveComputer.setTemporarilyOffline(temporarilyOffline, cause);
            }
        } catch (NullPointerException ignore) {
            //noop
        } finally {
            super.setTemporarilyOffline(temporarilyOffline, cause);
        }
    }
}