/******************************************************************************* * Copyright (c) 2017 Synopsys, Inc * 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: * Synopsys, Inc - initial implementation and documentation *******************************************************************************/ package jenkins.plugins.coverity; import com.coverity.ws.v9.CovRemoteServiceException_Exception; import com.coverity.ws.v9.StreamDataObj; import com.coverity.ws.v9.StreamFilterSpecDataObj; import hudson.Extension; import hudson.FilePath; import hudson.Launcher; import hudson.model.AbstractBuild; import hudson.model.AbstractDescribableImpl; import hudson.model.BuildListener; import hudson.model.Descriptor; import hudson.model.Executor; import hudson.model.Node; import hudson.model.TaskListener; import hudson.remoting.VirtualChannel; import hudson.util.FormValidation; import hudson.util.FormValidation.Kind; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.logging.Logger; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.jenkinsci.remoting.RoleChecker; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; import org.xml.sax.InputSource; import org.xml.sax.XMLReader; /** * A configuration checker, and the results of such a check. */ public class CheckConfig extends AbstractDescribableImpl<CheckConfig> { private static final Logger logger = Logger.getLogger(CIMStream.class.getName()); private CoverityPublisher publisher; private final List<Status> status; private Launcher launcher; private AbstractBuild<?, ?> build; private BuildListener listener; /** * Create a new check. * * @param publisher required * @param build optional (without this, node checking will be skipped) * @param launcher optional (without this, node checking will be skipped) * @param listener optional (without this, node checking will be skipped) */ public CheckConfig(CoverityPublisher publisher, AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) { this.publisher = publisher; this.launcher = launcher; this.build = build; this.listener = listener; this.status = new ArrayList<Status>(); } /** * Returns true if all checked aspects of the configuration are valid. If this is true, then it's almost certain * that the corresponding build will succeed. */ public boolean isValid() { for(Status s : status) { if(!s.isValid()) { return false; } } return true; } public List<Status> getStatus() { return status; } public CoverityPublisher getPublisher() { return publisher; } public void check() { try { Thread.sleep(1000); } catch(InterruptedException e) { e.printStackTrace(); } status.clear(); status.add(checkStream(publisher, publisher.getCimStream())); if(launcher != null) { NodeStatus ns = checkNode(publisher, build, launcher, listener); status.add(ns); //don't bother continuing unless we're all valid so far if(!isValid()) { return; } CoverityVersion analysisVersion = ns.getVersion(); //lots of checks can only happen if we know the analysis version //is any target CIM version < analysis version? { List<Status> newStatus = new ArrayList<Status>(); for(Status s : status) { if(s instanceof StreamStatus) { StreamStatus ss = (StreamStatus) s; CIMStream stream = ss.getStream(); if(!ss.getVersion().compareToAnalysis(analysisVersion) && stream != null) { newStatus.add(new Status(false, "Connect instance " + stream.toPrettyString() + " (version " + ss.getVersion() + "|" + ss.getVersion().getEffectiveVersion() + ") is incompatible with analysis version " + analysisVersion)); } } } status.addAll(newStatus); } //is there a mixed stream, and analysis < fresno? { if(analysisVersion.compareTo(CoverityVersion.codeNameEquivalents.get("fresno")) < 0) { List<Status> newStatus = new ArrayList<Status>(); for(Status s : status) { if(s instanceof StreamStatus) { StreamStatus ss = (StreamStatus) s; CIMStream stream = ss.getStream(); if(stream != null && stream.getDomain().equals("MIXED")) { newStatus.add(new Status(false, "Stream " + stream.toPrettyString() + " (any language) is incompatible with analysis version " + analysisVersion)); } } } status.addAll(newStatus); } } } TaOptionBlock taOptionBlock = publisher.getTaOptionBlock(); if (taOptionBlock != null) { status.add(checkTaOptionBlock(taOptionBlock)); } ScmOptionBlock scmOptionBlock = publisher.getScmOptionBlock(); if (scmOptionBlock != null) { status.add(checkScmOptionBlock(scmOptionBlock)); } } public static StreamStatus checkStream(CoverityPublisher publisher, CIMStream cs) { if(cs == null || cs.getInstance() == null){ return new StreamStatus(false, "Could not connect to a Coverity instance. \n " + "Verify that a Coverity instance has been configured for this job", cs, null); } CIMInstance ci = publisher.getDescriptor().getInstance(cs.getInstance()); if(ci == null){ return new StreamStatus(false, "Could not connect to a Coverity instance. \n " + "Verify that a Coverity instance has been configured for this job", cs, null); } if(cs.getStream() == null){ return new StreamStatus(false, "Could not find any Stream that matches the given configuration for this job.", cs, null); } //check if instance is valid { try { FormValidation fv = ci.doCheck(); if(fv.kind == Kind.ERROR) { return new StreamStatus(false, "Could not connect to instance: " + fv, cs, null); } } catch(Exception e) { e.printStackTrace(); return new StreamStatus(false, "Could not connect to instance: " + e, cs, null); } } //check instance version CoverityVersion version = null; { try { version = CoverityVersion.parse(ci.getConfigurationService().getVersion().getExternalVersion()); } catch(CovRemoteServiceException_Exception e) { e.printStackTrace(); return new StreamStatus(false, "Could not retrieve version info: " + e, cs, null); } catch(IOException e) { e.printStackTrace(); return new StreamStatus(false, "Could not retrieve version info: " + e, cs, null); } } //check stream { try { StreamFilterSpecDataObj sf = new StreamFilterSpecDataObj(); sf.setNamePattern(cs.getStream()); List<StreamDataObj> ls = ci.getConfigurationService().getStreams(sf); if(ls.size() == 0) { return new StreamStatus(false, "Stream does not exist", cs, version); } } catch(CovRemoteServiceException_Exception e) { e.printStackTrace(); return new StreamStatus(false, "Could not find stream: " + e, cs, version); } catch(IOException e) { e.printStackTrace(); return new StreamStatus(false, "Could not find stream: " + e, cs, version); } } return new StreamStatus(true, "OK (version: " + version + ")", cs, version); } public static NodeStatus checkNode(CoverityPublisher publisher, AbstractBuild<?, ?> build, Launcher launcher, TaskListener listener) { Node node = Executor.currentExecutor().getOwner().getNode(); try { String home = publisher.getDescriptor().getHome(node, build.getEnvironment(listener)); InvocationAssistance ia = publisher.getInvocationAssistance(); if(ia != null && ia.getSaOverride() != null && !ia.getSaOverride().isEmpty()) { home = new CoverityInstallation(ia.getSaOverride()).forEnvironment(build.getEnvironment(listener)).getHome(); } if(home == null) { return new NodeStatus(false, "Could not find Coverity Analysis home directory. [" + home + "]", node, null); } try { CoverityUtils.checkDir(launcher.getChannel(), home); } catch (Exception e) { e.printStackTrace(); return new NodeStatus(false, "Could not find Coverity Analysis home directory. [" + home + "]", node, null); } FilePath homePath = new FilePath(launcher.getChannel(), home); final TaskListener listen = listener; // Final copy of listner to help print debugging messages CoverityVersion version = getVersion(homePath, listen); if(version.compareTo(CoverityVersion.MINIMUM_SUPPORTED_VERSION) < 0) { return new NodeStatus(false, "\"Coverity Static Analysis\" version " + version.toString() + " is not supported. " + "The minimum supported version is " + CoverityVersion.MINIMUM_SUPPORTED_VERSION.getEffectiveVersion().toString(), node, version); } return new NodeStatus(true, "version " + version, node, version); } catch(IOException e) { e.printStackTrace(); return new NodeStatus(false, "Error checking node: " + e.toString(), node, null); } catch(InterruptedException e) { e.printStackTrace(); return new NodeStatus(false, "Interrupted while checking node.", node, null); } } /* Performs the validation on TaOptionBlock */ public static Status checkTaOptionBlock(TaOptionBlock taOptionBlock) { String taCheck = taOptionBlock.checkTaConfig(); if(!taCheck.equals("Pass")){ return new Status(false, taCheck); } return new Status(true, "[Test Advisor] Configuration is valid!"); } /* Performs the validation on ScmOptionBlock */ public static Status checkScmOptionBlock(ScmOptionBlock scmOptionBlock) { String scmCheck = scmOptionBlock.checkScmConfig(); if(!scmCheck.equals("Pass")){ return new Status(false, scmCheck); } return new Status(true, "[SCM] Configuration is valid!"); } /* * Gets the {@link CoverityVersion} given a static analysis tools home directory by finding the VERSION.xml file, * then reading the version number */ public static CoverityVersion getVersion(FilePath homePath, final TaskListener listener) throws IOException, InterruptedException { CoverityVersion version = homePath.child("VERSION.xml").act(new FilePath.FileCallable<CoverityVersion>() { @Override public void checkRoles(RoleChecker roleChecker) throws SecurityException { } public CoverityVersion invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { InputStream fis = new FileInputStream(f); // Setting up reader into UTF-8 format since xml document is that format Reader reader = new InputStreamReader(fis, "UTF-8"); InputSource is = new InputSource(reader); is.setEncoding("UTF-8"); CoverityVersion cv = parseVersionXML(is, listener); fis.close(); return cv; } }); return version; } /** * Parse Version XML File * We use SAX Parser to go thought the VERSION.xml file, and extract the Major, Minor, Revision, and Beta elements. * These parts make up the version of Analysis, and is later used to compare with the cim version * @param path * @param listener * @return {@link CoverityVersion} */ private static CoverityVersion parseVersionXML(InputSource path, TaskListener listener){ String errorMessage; try{ // Setting up SAX Parser SAXParserFactory factory = SAXParserFactory.newInstance(); factory.setValidating(false); SAXParser xmlParser = factory.newSAXParser(); XMLReader xmlReader = xmlParser.getXMLReader(); ConnectorParser connectorParser = new ConnectorParser(); // Setting up XML Reader so that it ignores the <!DOCTYPE xmlReader.setContentHandler(connectorParser); xmlReader.setFeature("http://xml.org/sax/features/validation", false); xmlReader.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false); xmlReader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd",false); // Parse the xml xmlReader.parse(path); // Checks to see if beta was set or not, since its not required on releases. try{ if(connectorParser.beta != null){ return new CoverityVersion(Integer.parseInt(connectorParser.major), Integer.parseInt(connectorParser.minor), Integer.parseInt(connectorParser.revision), Integer.parseInt(connectorParser.beta)); }else{ return new CoverityVersion(Integer.parseInt(connectorParser.major), Integer.parseInt(connectorParser.minor), Integer.parseInt(connectorParser.revision)); } }catch(NumberFormatException e){ return new CoverityVersion(connectorParser.major); } }catch(ParserConfigurationException x){ errorMessage = "Unable to configure XML parser: " + x.getMessage(); }catch(SAXException x){ errorMessage = "Unable to parse VERSION.xml: " + x.getMessage(); }catch(FileNotFoundException x){ errorMessage = "Could not find VERSION.xml file at: " + path.toString(); }catch(IOException x){ errorMessage = "IOException reading VERSION.xml: " + x.getMessage(); } if (listener != null) { listener.fatalError(errorMessage); } else { logger.warning(errorMessage); } return null; } /** * Custom Handler that is ran when the XML is parse and find when major, minor, revision, and beta occurs * within the xml, and then store its number */ private static class ConnectorParser extends DefaultHandler{ public String major = null; public String minor = null; public String revision = null; public String beta = null; public boolean bmajor =false; public boolean bminor =false; public boolean brevision =false; public boolean bbeta =false; // Checks the start of each element it sees, then flags that specific keywords are found public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException{ if(qName.equalsIgnoreCase("major")){ bmajor = true; } if(qName.equalsIgnoreCase("minor")){ bminor = true; } if(qName.equalsIgnoreCase("revision")){ brevision = true; } if(qName.equalsIgnoreCase("beta")){ bbeta = true; } } // At each entry, it will check if specific flags are set and then store the values of the set flags public void characters(char ch[],int start, int length){ if(this.bmajor){ major = new String(ch,start,length); bmajor = false; } if(this.bminor){ minor = new String(ch,start,length); bminor = false; } if(this.brevision){ revision = new String(ch,start,length); brevision = false; } if(this.bbeta){ beta = new String(ch,start,length); bbeta = false; } } } /** * The result of a single check. Non-subclasses are general configuration statuses, not associated with a Node or * Stream, for example. */ public static class Status { boolean valid; String status; private Status(boolean valid, String status) { this.valid = valid; this.status = status; } @Override public String toString() { return status; } public boolean isValid() { return valid; } public String getStatus() { return status; } } public static class StreamStatus extends Status { private final CIMStream stream; private final CoverityVersion version; public StreamStatus(boolean valid, String status, CIMStream stream, CoverityVersion version) { super(valid, status); this.stream = stream; this.version = version; } public CIMStream getStream() { return stream; } public CoverityVersion getVersion() { return version; } @Override public String getStatus() { if (stream != null) { return "[Stream] " + stream.toPrettyString() + " : " + status; } return status; } } public static class NodeStatus extends Status { private final Node node; private final CoverityVersion version; public NodeStatus(boolean valid, String status, Node node, CoverityVersion version) { super(valid, status); this.version = version; this.node = node; } public Node getNode() { return node; } public CoverityVersion getVersion() { return version; } @Override public String getStatus() { if (node != null) { return "[Node] " + node.getDisplayName() + " : " + status; } return status; } } @Extension public static class DescriptorImpl extends Descriptor<CheckConfig> { @Override public String getDisplayName() { return ""; } } }