/*
* Copyright (c) 2012-2013, CloudBees, Inc., SOASTA, Inc.
* All Rights Reserved.
*/
package com.soasta.jenkins;
import hudson.CopyOnWrite;
import hudson.Extension;
import hudson.model.AbstractDescribableImpl;
import hudson.model.Descriptor;
import hudson.util.FormValidation;
import hudson.util.Secret;
import hudson.util.VersionNumber;
import jenkins.model.Jenkins;
import net.sf.json.JSONObject;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.StringEntity;
import org.jsoup.Jsoup;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import com.soasta.jenkins.httpclient.GenericSelfClosingHttpClient;
import com.soasta.jenkins.httpclient.HttpClientSettings;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jsoup.nodes.Document;
import org.jsoup.select.Elements;
/**
* Information about a specific CloudTest Server and access credential.
*
* @author Kohsuke Kawaguchi
*/
public class CloudTestServer extends AbstractDescribableImpl<CloudTestServer> {
/**
* URL like "http://touchtestlite.soasta.com/concerto/"
*/
private final String url;
private final String apitoken;
private final String username;
private final Secret password;
private final String id;
private final String name;
private final String keyStoreLocation;
private final Secret keyStorePassword;
private final boolean trustSelfSigned;
private static final String REPOSITORY_SERVICE_BASE_URL = "/services/rest/RepositoryService/v1/Tokens";
private transient boolean generatedIdOrName;
@DataBoundConstructor
public CloudTestServer(String url, String username, Secret password, String id, String name, String apitoken, String keyStoreLocation, Secret keyStorePassword, boolean trustSelfSigned) throws MalformedURLException {
this.keyStoreLocation = keyStoreLocation;
this.keyStorePassword = keyStorePassword;
this.trustSelfSigned = trustSelfSigned;
if (url == null || url.isEmpty()) {
// This is not really a valid case, but we have to store something.
this.url = null;
}
else {
// normalization
// TODO: can the service be running outside the /concerto/ URL?
if (!url.endsWith("/")) url+='/';
if (!url.endsWith("/concerto/"))
url+="concerto/";
this.url = url;
}
if (username == null || username.isEmpty()) {
this.username = "";
}
else {
this.username = username;
}
if (password == null || password.getPlainText() == null || password.getPlainText().isEmpty()) {
this.password = null;
}
else {
this.password = password;
}
if (apitoken == null || apitoken.isEmpty()) {
this.apitoken = "";
}
else {
this.apitoken = apitoken;
}
// If the ID is empty, auto-generate one.
if (id == null || id.isEmpty()) {
this.id = UUID.randomUUID().toString();
// This is probably a configuration created using
// an older version of the plug-in (before ID and name
// existed). Set a flag so we can write the new
// values after initialization (see DescriptorImpl).
generatedIdOrName = true;
}
else {
this.id = id;
}
// If the name is empty, default to URL + user name.
if (name == null || name.isEmpty()) {
if (this.url == null) {
this.name = "";
}
else {
this.name = url + " (" + username + ")";
// This is probably a configuration created using
// an older version of the plug-in (before ID and name
// existed). Set a flag so we can write the new
// values after initialization (see DescriptorImpl).
generatedIdOrName = true;
}
}
else {
this.name = name;
}
}
public String getUrl() {
return url;
}
public String getUsername() {
return username;
}
public Secret getPassword() {
return password;
}
public String getId() {
return id;
}
public String getKeyStoreLocation()
{
return keyStoreLocation;
}
public Secret getKeyStorePassword()
{
return keyStorePassword;
}
public boolean isTrustSelfSigned()
{
return trustSelfSigned;
}
public String getName() {
return name;
}
public String getApitoken() {
return apitoken;
}
public Object readResolve() throws IOException {
if (id != null &&
id.trim().length() > 0 &&
name != null &&
name.trim().length() > 0)
return this;
// Either the name or ID is missing.
// This means the config is based an older version the plug-in.
// The constructor handles this, but XStream doesn't go
// through the same code path (as far as I can tell). Instead,
// we create a new CloudTestServer object, which will include an
// auto-generated name and ID, and return that instead.
// When Jenkins is finished loading everything, we'll go back
// and write the auto-generated values to disk, so this logic
// should only execute once. See DescriptorImpl constructor.
LOGGER.info("Re-creating object to generate a new server ID and name.");
return new CloudTestServer(url, username, password, id, name, apitoken, keyStoreLocation, keyStorePassword, trustSelfSigned);
}
public FormValidation validate() throws IOException {
try
{
GenericSelfClosingHttpClient client = createClient();
// to validate the credentials we will request a token from the repository.
JSONObject obj = new JSONObject();
if(apitoken.trim().isEmpty() && !username.trim().isEmpty() && password != null) {
obj.put("userName", username);
obj.put("password", password.getPlainText());
}
else if(!apitoken.trim().isEmpty() && username.trim().isEmpty() && password == null) {
if(apitoken.length() != 36)
throw new IOException("Invalid API Token");
else
obj.put("apiToken", apitoken);
}
HttpPut put = new HttpPut(url + REPOSITORY_SERVICE_BASE_URL);
StringEntity jsonEntity = new StringEntity(obj.toString(), "UTF-8");
jsonEntity.setContentType("application/json");
put.setEntity(jsonEntity);
client.sendRequest(put);
return FormValidation.ok("Success!");
}
catch (Exception e)
{
LOGGER.log(Level.SEVERE, "Failed to valdiate", e);
return FormValidation.error(e.getMessage());
}
}
/**
* Retrieves the build number of this CloudTest server.
* Postcondition: The build number returned is never null.
*/
public VersionNumber getBuildNumber() throws IOException {
if (url == null) {
// User didn't enter a value in the Configure Jenkins page.
// Nothing we can do.
throw new IllegalStateException("No URL has been configured for this CloudTest server.");
}
GenericSelfClosingHttpClient client = createClient();
HttpGet get = new HttpGet(url);
String responseBody = client.sendRequest(get);
Document doc = Jsoup.parse(responseBody);
Elements elements = doc.select("meta[name=buildnumber]");
if (elements != null && elements.size() >= 1)
{
String buildNumber = elements.get(0).attr("content");
if (buildNumber != null)
{
return new VersionNumber(buildNumber);
}
}
throw new Error("failed to find build number");
}
private GenericSelfClosingHttpClient createClient() throws IOException {
return new GenericSelfClosingHttpClient(new HttpClientSettings()
.setKeyStore(HttpClientSettings.loadKeyStore(keyStoreLocation, Secret.toString(keyStorePassword)))
.setKeyStorePassword(keyStorePassword == null || keyStorePassword.getPlainText().isEmpty() ? null : keyStorePassword.getPlainText())
.setUrl(url)
.setTrustSelfSigned(trustSelfSigned));
}
public static CloudTestServer getByURL(String url) {
List<CloudTestServer> servers = Jenkins.getInstance().getDescriptorByType(DescriptorImpl.class).getServers();
for (CloudTestServer s : servers) {
if (s.getUrl().equals(url))
return s;
}
// if we can't find any, fall back to the default one
if (!servers.isEmpty())
return servers.get(0);
return null;
}
public static CloudTestServer getByID(String id) {
List<CloudTestServer> servers = Jenkins.getInstance().getDescriptorByType(DescriptorImpl.class).getServers();
for (CloudTestServer s : servers) {
if (s.getId().equals(id))
return s;
}
// if we can't find any, fall back to the default one
if (!servers.isEmpty())
return servers.get(0);
return null;
}
@Extension
public static class DescriptorImpl extends Descriptor<CloudTestServer> {
@CopyOnWrite
private volatile List<CloudTestServer> servers;
boolean setUsername;
boolean setApiToken;
public DescriptorImpl() {
load();
if (servers == null) {
servers = new ArrayList<CloudTestServer>();
} else {
// If any of the servers that we loaded was
// missing a name or ID, and had to auto-generate
// it, then persist the auto-generated values now.
for (CloudTestServer s : servers) {
if (s.generatedIdOrName) {
LOGGER.info("Persisting generated server IDs and/or names.");
save();
// Calling save() once covers all servers,
// so we can stop looping.
break;
}
}
}
}
@Override
public String getDisplayName() {
return "CloudTest Server";
}
public List<CloudTestServer> getServers() {
return servers;
}
public void setServers(Collection<? extends CloudTestServer> servers) {
this.servers = new ArrayList<CloudTestServer>(servers);
}
@Override
public boolean configure(StaplerRequest req, JSONObject json) throws FormException {
setServers(req.bindJSONToList(CloudTestServer.class,json.get("servers")));
save();
return true;
}
public FormValidation doValidate(@QueryParameter String url, @QueryParameter String username, @QueryParameter String password, @QueryParameter String id, @QueryParameter String name, @QueryParameter String apitoken,
@QueryParameter String keyStoreLocation, @QueryParameter String keyStorePassword, @QueryParameter boolean trustSelfSigned) throws IOException {
return new CloudTestServer(url,username,Secret.fromString(password), id, name, apitoken, keyStoreLocation, Secret.fromString(keyStorePassword), trustSelfSigned).validate();
}
public FormValidation doCheckName(@QueryParameter String value) {
if (value == null || value.trim().isEmpty()) {
return FormValidation.error("Required.");
} else {
return FormValidation.ok();
}
}
public FormValidation doCheckUrl(@QueryParameter String value) {
if (value == null || value.trim().isEmpty()) {
return FormValidation.error("Required.");
} else if (!isValidURL(value)) {
return FormValidation.error("Invalid URL syntax (did you mean http://" + value + " ?");
} else {
return FormValidation.ok();
}
}
public FormValidation doCheckUsername(@QueryParameter String value) {
if(setApiToken == true && (value == null || value.trim().isEmpty())) {
setUsername = false;
return FormValidation.ok();
} else if(setApiToken == false && (value == null || value.trim().isEmpty())) {
setUsername = false;
return FormValidation.error("Username/Password or API Token Required.");
} else if(setApiToken == false && (value != null || !(value.trim().isEmpty()))) {
setUsername = true;
return FormValidation.ok();
} else {
setUsername = true;
return FormValidation.error("Cannot use both Username/Password and API Token");
}
}
public FormValidation doCheckPassword(@QueryParameter String value) {
if(setApiToken == true && setUsername == false && (value == null || value.trim().isEmpty())) {
return FormValidation.ok();
} else if(setApiToken == false && setUsername == true && (value == null || value.trim().isEmpty())) {
return FormValidation.error("Password Required.");
} else if(setApiToken == false && setUsername == true && (value != null || !(value.trim().isEmpty()))) {
return FormValidation.ok();
} else if(setApiToken == false && setUsername == false) {
return FormValidation.ok();
} else {
return FormValidation.error("Cannot use both Username/Password and API Token");
}
}
public FormValidation doCheckApitoken(@QueryParameter String value) {
if(setUsername == true && (value == null || value.trim().isEmpty())) {
setApiToken = false;
return FormValidation.ok();
} else if(setUsername == false && (value == null || value.trim().isEmpty())) {
setApiToken = false;
return FormValidation.error("Username/Password or API Token Required.");
} else if(setUsername == false && (value != null || !(value.trim().isEmpty()))) {
if(value.length() != 36)
return FormValidation.error("Invalid API Token");
else {
setApiToken = true;
return FormValidation.ok();
}
} else {
setApiToken = true;
return FormValidation.error("Cannot use both Username/Password and API Token");
}
}
private static boolean isValidURL(String url) {
try {
new URL(url);
return true;
}
catch (MalformedURLException e) {
return false;
}
}
}
private static final Logger LOGGER = Logger.getLogger(CloudTestServer.class.getName());
}